Skip to content

Commit c8dafdd

Browse files
committed
introduce VMA -> module coalescing via ModuleAccumulator
1 parent c3be389 commit c8dafdd

File tree

4 files changed

+327
-20
lines changed

4 files changed

+327
-20
lines changed

sentry-android-core/src/main/java/io/sentry/android/core/internal/tombstone/TombstoneParser.java

Lines changed: 91 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -210,29 +210,105 @@ private Message constructMessage(@NonNull final TombstoneProtos.Tombstone tombst
210210
return message;
211211
}
212212

213+
/**
214+
* Helper class to accumulate memory mappings into a single module. Modules in the Sentry sense
215+
* are the entire readable memory map for a file, not just the executable segment. This is
216+
* important to maintain the file-offset contract of map entries, which is necessary to resolve
217+
* runtime instruction addresses in the files uploaded for symbolication.
218+
*/
219+
private static class ModuleAccumulator {
220+
String mappingName;
221+
String buildId;
222+
long beginAddress;
223+
long endAddress;
224+
225+
ModuleAccumulator(TombstoneProtos.MemoryMapping mapping) {
226+
this.mappingName = mapping.getMappingName();
227+
this.buildId = mapping.getBuildId();
228+
this.beginAddress = mapping.getBeginAddress();
229+
this.endAddress = mapping.getEndAddress();
230+
}
231+
232+
void extendTo(long newEndAddress) {
233+
this.endAddress = newEndAddress;
234+
}
235+
236+
DebugImage toDebugImage() {
237+
if (buildId.isEmpty()) {
238+
return null;
239+
}
240+
final DebugImage image = new DebugImage();
241+
image.setCodeId(buildId);
242+
image.setCodeFile(mappingName);
243+
244+
final String debugId = NativeEventUtils.buildIdToDebugId(buildId);
245+
image.setDebugId(debugId != null ? debugId : buildId);
246+
247+
image.setImageAddr(formatHex(beginAddress));
248+
image.setImageSize(endAddress - beginAddress);
249+
image.setType("elf");
250+
251+
return image;
252+
}
253+
}
254+
213255
private DebugMeta createDebugMeta(@NonNull final TombstoneProtos.Tombstone tombstone) {
214256
final List<DebugImage> images = new ArrayList<>();
215257

216-
for (TombstoneProtos.MemoryMapping module : tombstone.getMemoryMappingsList()) {
217-
// exclude anonymous and non-executable maps
218-
if (module.getBuildId().isEmpty()
219-
|| module.getMappingName().isEmpty()
220-
|| !module.getExecute()) {
258+
// Coalesce memory mappings into modules similar to how sentry-native does it.
259+
// A module consists of all readable mappings for the same file, starting from
260+
// the first mapping that has a valid ELF header (indicated by offset 0 with build_id).
261+
// In sentry-native, is_valid_elf_header() reads the ELF magic bytes from memory,
262+
// which is only present at the start of the file (offset 0). We use offset == 0
263+
// combined with non-empty build_id as a proxy for this check.
264+
ModuleAccumulator currentModule = null;
265+
266+
for (TombstoneProtos.MemoryMapping mapping : tombstone.getMemoryMappingsList()) {
267+
// Skip mappings that are not readable
268+
if (!mapping.getRead()) {
221269
continue;
222270
}
223-
final DebugImage image = new DebugImage();
224-
final String codeId = module.getBuildId();
225-
image.setCodeId(codeId);
226-
image.setCodeFile(module.getMappingName());
227271

228-
final String debugId = NativeEventUtils.buildIdToDebugId(codeId);
229-
image.setDebugId(debugId != null ? debugId : codeId);
272+
// Skip mappings with empty name or in /dev/
273+
final String mappingName = mapping.getMappingName();
274+
if (mappingName.isEmpty() || mappingName.startsWith("/dev/")) {
275+
continue;
276+
}
230277

231-
image.setImageAddr(formatHex(module.getBeginAddress()));
232-
image.setImageSize(module.getEndAddress() - module.getBeginAddress());
233-
image.setType("elf");
278+
final boolean hasBuildId = !mapping.getBuildId().isEmpty();
279+
final boolean isFileStart = mapping.getOffset() == 0;
280+
281+
if (hasBuildId && isFileStart) {
282+
// Check for duplicated mappings: On Android, the same ELF can have multiple
283+
// mappings at offset 0 with different permissions (r--p, r-xp, r--p).
284+
// If it's the same file as the current module, just extend it.
285+
if (currentModule != null && mappingName.equals(currentModule.mappingName)) {
286+
currentModule.extendTo(mapping.getEndAddress());
287+
continue;
288+
}
289+
290+
// Flush the previous module (different file)
291+
if (currentModule != null) {
292+
final DebugImage image = currentModule.toDebugImage();
293+
if (image != null) {
294+
images.add(image);
295+
}
296+
}
297+
298+
// Start a new module
299+
currentModule = new ModuleAccumulator(mapping);
300+
} else if (currentModule != null && mappingName.equals(currentModule.mappingName)) {
301+
// Extend the current module with this mapping (same file, continuation)
302+
currentModule.extendTo(mapping.getEndAddress());
303+
}
304+
}
234305

235-
images.add(image);
306+
// Flush the last module
307+
if (currentModule != null) {
308+
final DebugImage image = currentModule.toDebugImage();
309+
if (image != null) {
310+
images.add(image);
311+
}
236312
}
237313

238314
final DebugMeta debugMeta = new DebugMeta();

sentry-android-core/src/test/java/io/sentry/android/core/TombstoneIntegrationTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,8 @@ class TombstoneIntegrationTest : ApplicationExitIntegrationTestBase<TombstoneHin
8282
assertEquals("744b0bf6-5f00-fb33-3ef3-b98aa4546008", image!!.debugId)
8383
assertNotNull(image)
8484
assertEquals("/system/lib64/libcompiler_rt.so", image.codeFile)
85-
assertEquals("0x764c32a000", image.imageAddr)
86-
assertEquals(32768, image.imageSize)
85+
assertEquals("0x764c325000", image.imageAddr)
86+
assertEquals(57344, image.imageSize)
8787
}
8888

8989
@Test

0 commit comments

Comments
 (0)