@@ -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 ();
0 commit comments