Skip to content

Commit 6be3488

Browse files
lealanko-rtLauri Alankomarkushiromtsn
authored
Enable symbolication of native stack frames in ANR events (#4061)
* Add native ANR button to sentry-samples-android Add a button to trigger ANR by holding a lock too long in native code. This can be used to test native stack frames in ANR events. * Improve native stack frame parsing Handle offsets and deleted files, recognize "???" as a marker for unknown functions. Use named capturing groups for better readability and editability. * Add PC value and platform to native stack frames * Mark JNI method frames as "native" Use the "native" attribute of stack frames to indicate JNI invocation frames, like SentryStackTraceFactory does. * Add debug images to ANR events The images are parsed from the build ids and filenames in the thread dump's stack frames. * Add addr_mode attributes to ANR stack frames The instruction addresses of native stack frames in thread dumps are relative to the image file from which the code is loaded, and there are no absolute mapping addresses of images available. So explicitly inform the Sentry server about the correct images by using a relative "addr_mode" attribute. Also add the attribute to the SentryStackFrame class since it was not yet supported by it. The field documentation is converted from event.schema.json in the sentry server repo. * Add ChangeLog entry for ANR native symbolication * Fix code formatting, make API 21 level compatible, minor improvements * Add ADDR_MODE entries to sentry.api * Update sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/ThreadDumpParser.java * Update sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/ThreadDumpParser.java --------- Co-authored-by: Lauri Alanko <[email protected]> Co-authored-by: Markus Hintersteiner <[email protected]> Co-authored-by: Roman Zavarnitsyn <[email protected]>
1 parent 70c11a0 commit 6be3488

File tree

14 files changed

+304
-34
lines changed

14 files changed

+304
-34
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
## Unreleased
44

5+
### Features
6+
7+
- Add native stack frame address information and debug image metadata to ANR events ([#4061](https://github.com/getsentry/sentry-java/pull/4061))
8+
- This enables symbolication for stripped native code in ANRs
9+
510
### Fixes
611

712
- Reduce excessive CPU usage when serializing breadcrumbs to disk for ANRs ([#4181](https://github.com/getsentry/sentry-java/pull/4181))

sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import io.sentry.hints.AbnormalExit;
2424
import io.sentry.hints.Backfillable;
2525
import io.sentry.hints.BlockingFlushHint;
26+
import io.sentry.protocol.DebugImage;
27+
import io.sentry.protocol.DebugMeta;
2628
import io.sentry.protocol.Message;
2729
import io.sentry.protocol.SentryId;
2830
import io.sentry.protocol.SentryThread;
@@ -267,6 +269,11 @@ private void reportAsSentryEvent(
267269
event.setMessage(sentryMessage);
268270
} else if (result.type == ParseResult.Type.DUMP) {
269271
event.setThreads(result.threads);
272+
if (result.debugImages != null) {
273+
final DebugMeta debugMeta = new DebugMeta();
274+
debugMeta.setImages(result.debugImages);
275+
event.setDebugMeta(debugMeta);
276+
}
270277
}
271278
event.setLevel(SentryLevel.FATAL);
272279
event.setTimestamp(DateUtils.getDateTime(anrTimestamp));
@@ -311,15 +318,19 @@ private void reportAsSentryEvent(
311318
final Lines lines = Lines.readLines(reader);
312319

313320
final ThreadDumpParser threadDumpParser = new ThreadDumpParser(options, isBackground);
314-
final List<SentryThread> threads = threadDumpParser.parse(lines);
321+
threadDumpParser.parse(lines);
322+
323+
final @NotNull List<SentryThread> threads = threadDumpParser.getThreads();
324+
final @NotNull List<DebugImage> debugImages = threadDumpParser.getDebugImages();
325+
315326
if (threads.isEmpty()) {
316327
// if the list is empty this means the system failed to capture a proper thread dump of
317328
// the android threads, and only contains kernel-level threads and statuses, those ANRs
318329
// are not actionable and neither they are reported by Google Play Console, so we just
319330
// fall back to not reporting them
320331
return new ParseResult(ParseResult.Type.NO_DUMP);
321332
}
322-
return new ParseResult(ParseResult.Type.DUMP, dump, threads);
333+
return new ParseResult(ParseResult.Type.DUMP, dump, threads, debugImages);
323334
} catch (Throwable e) {
324335
options.getLogger().log(SentryLevel.WARNING, "Failed to parse ANR thread dump", e);
325336
return new ParseResult(ParseResult.Type.ERROR, dump);
@@ -403,24 +414,31 @@ enum Type {
403414
final Type type;
404415
final byte[] dump;
405416
final @Nullable List<SentryThread> threads;
417+
final @Nullable List<DebugImage> debugImages;
406418

407419
ParseResult(final @NotNull Type type) {
408420
this.type = type;
409421
this.dump = null;
410422
this.threads = null;
423+
this.debugImages = null;
411424
}
412425

413426
ParseResult(final @NotNull Type type, final byte[] dump) {
414427
this.type = type;
415428
this.dump = dump;
416429
this.threads = null;
430+
this.debugImages = null;
417431
}
418432

419433
ParseResult(
420-
final @NotNull Type type, final byte[] dump, final @Nullable List<SentryThread> threads) {
434+
final @NotNull Type type,
435+
final byte[] dump,
436+
final @Nullable List<SentryThread> threads,
437+
final @Nullable List<DebugImage> debugImages) {
421438
this.type = type;
422439
this.dump = dump;
423440
this.threads = threads;
441+
this.debugImages = debugImages;
424442
}
425443
}
426444
}

sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/ThreadDumpParser.java

Lines changed: 103 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,14 @@
2222
import io.sentry.SentryLockReason;
2323
import io.sentry.SentryOptions;
2424
import io.sentry.SentryStackTraceFactory;
25+
import io.sentry.protocol.DebugImage;
2526
import io.sentry.protocol.SentryStackFrame;
2627
import io.sentry.protocol.SentryStackTrace;
2728
import io.sentry.protocol.SentryThread;
29+
import java.math.BigInteger;
30+
import java.nio.BufferUnderflowException;
31+
import java.nio.ByteBuffer;
32+
import java.nio.ByteOrder;
2833
import java.util.ArrayList;
2934
import java.util.Collections;
3035
import java.util.HashMap;
@@ -42,12 +47,40 @@ public class ThreadDumpParser {
4247
private static final Pattern BEGIN_UNMANAGED_NATIVE_THREAD_RE =
4348
Pattern.compile("\"(.*)\" (.*) ?sysTid=(\\d+)");
4449

50+
// For reference, see native_stack_dump.cc and tombstone_proto_to_text.cpp in Android sources
51+
// Groups
52+
// 0:entire regex
53+
// 1:index
54+
// 2:pc
55+
// 3:mapinfo
56+
// 4:filename
57+
// 5:mapoffset
58+
// 6:function
59+
// 7:fnoffset
60+
// 8:buildid
4561
private static final Pattern NATIVE_RE =
4662
Pattern.compile(
47-
" *(?:native: )?#\\d+ \\S+ [0-9a-fA-F]+\\s+(.*?)\\s+\\((.*)\\+(\\d+)\\)(?: \\(.*\\))?");
48-
private static final Pattern NATIVE_NO_LOC_RE =
49-
Pattern.compile(
50-
" *(?:native: )?#\\d+ \\S+ [0-9a-fA-F]+\\s+(.*)\\s*\\(?(.*)\\)?(?: \\(.*\\))?");
63+
// " native: #12 pc 0xabcd1234"
64+
" *(?:native: )?#(\\d+) \\S+ ([0-9a-fA-F]+)"
65+
// The map info includes a filename and an optional offset into the file
66+
+ ("\\s+("
67+
// "/path/to/file.ext",
68+
+ "(.*?)"
69+
// optional " (deleted)" suffix (deleted files) needed here to bias regex
70+
// correctly
71+
+ "(?:\\s+\\(deleted\\))?"
72+
// " (offset 0xabcd1234)", if the mapping is not into the beginning of the file
73+
+ "(?:\\s+\\(offset (.*?)\\))?"
74+
+ ")")
75+
// Optional function
76+
+ ("(?:\\s+\\((?:"
77+
+ "\\?\\?\\?" // " (???) marks a missing function, so don't capture it in a group
78+
+ "|(.*?)(?:\\+(\\d+))?" // " (func+1234)", offset is
79+
// optional
80+
+ ")\\))?")
81+
// Optional " (BuildId: abcd1234abcd1234abcd1234abcd1234abcd1234)"
82+
+ "(?:\\s+\\(BuildId: (.*?)\\))?");
83+
5184
private static final Pattern JAVA_RE =
5285
Pattern.compile(" *at (?:(.+)\\.)?([^.]+)\\.([^.]+)\\((.*):([\\d-]+)\\)");
5386
private static final Pattern JNI_RE =
@@ -75,15 +108,48 @@ public class ThreadDumpParser {
75108

76109
private final @NotNull SentryStackTraceFactory stackTraceFactory;
77110

111+
private final @NotNull Map<String, DebugImage> debugImages;
112+
113+
private final @NotNull List<SentryThread> threads;
114+
78115
public ThreadDumpParser(final @NotNull SentryOptions options, final boolean isBackground) {
79116
this.options = options;
80117
this.isBackground = isBackground;
81118
this.stackTraceFactory = new SentryStackTraceFactory(options);
119+
this.debugImages = new HashMap<>();
120+
this.threads = new ArrayList<>();
121+
}
122+
123+
@NotNull
124+
public List<DebugImage> getDebugImages() {
125+
return new ArrayList<>(debugImages.values());
82126
}
83127

84128
@NotNull
85-
public List<SentryThread> parse(final @NotNull Lines lines) {
86-
final List<SentryThread> sentryThreads = new ArrayList<>();
129+
public List<SentryThread> getThreads() {
130+
return threads;
131+
}
132+
133+
@Nullable
134+
private static String buildIdToDebugId(final @NotNull String buildId) {
135+
try {
136+
// Abuse BigInteger as a hex string parser. Extra byte needed to handle leading zeros.
137+
final ByteBuffer buf = ByteBuffer.wrap(new BigInteger("10" + buildId, 16).toByteArray());
138+
buf.get();
139+
return String.format(
140+
"%08x-%04x-%04x-%04x-%04x%08x",
141+
buf.order(ByteOrder.LITTLE_ENDIAN).getInt(),
142+
buf.getShort(),
143+
buf.getShort(),
144+
buf.order(ByteOrder.BIG_ENDIAN).getShort(),
145+
buf.getShort(),
146+
buf.getInt());
147+
} catch (NumberFormatException | BufferUnderflowException e) {
148+
return null;
149+
}
150+
}
151+
152+
public void parse(final @NotNull Lines lines) {
87153

88154
final Matcher beginManagedThreadRe = BEGIN_MANAGED_THREAD_RE.matcher("");
89155
final Matcher beginUnmanagedNativeThreadRe = BEGIN_UNMANAGED_NATIVE_THREAD_RE.matcher("");
@@ -92,7 +158,7 @@ public List<SentryThread> parse(final @NotNull Lines lines) {
92158
final Line line = lines.next();
93159
if (line == null) {
94160
options.getLogger().log(SentryLevel.WARNING, "Internal error while parsing thread dump.");
95-
return sentryThreads;
161+
return;
96162
}
97163
final String text = line.text;
98164
// we only handle managed threads, as unmanaged/not attached do not have the thread id and
@@ -102,11 +168,10 @@ public List<SentryThread> parse(final @NotNull Lines lines) {
102168

103169
final SentryThread thread = parseThread(lines);
104170
if (thread != null) {
105-
sentryThreads.add(thread);
171+
threads.add(thread);
106172
}
107173
}
108174
}
109-
return sentryThreads;
110175
}
111176

112177
private SentryThread parseThread(final @NotNull Lines lines) {
@@ -176,7 +241,6 @@ private SentryStackTrace parseStacktrace(
176241
SentryStackFrame lastJavaFrame = null;
177242

178243
final Matcher nativeRe = NATIVE_RE.matcher("");
179-
final Matcher nativeNoLocRe = NATIVE_NO_LOC_RE.matcher("");
180244
final Matcher javaRe = JAVA_RE.matcher("");
181245
final Matcher jniRe = JNI_RE.matcher("");
182246
final Matcher lockedRe = LOCKED_RE.matcher("");
@@ -194,20 +258,7 @@ private SentryStackTrace parseStacktrace(
194258
break;
195259
}
196260
final String text = line.text;
197-
if (matches(nativeRe, text)) {
198-
final SentryStackFrame frame = new SentryStackFrame();
199-
frame.setPackage(nativeRe.group(1));
200-
frame.setFunction(nativeRe.group(2));
201-
frame.setLineno(getInteger(nativeRe, 3, null));
202-
frames.add(frame);
203-
lastJavaFrame = null;
204-
} else if (matches(nativeNoLocRe, text)) {
205-
final SentryStackFrame frame = new SentryStackFrame();
206-
frame.setPackage(nativeNoLocRe.group(1));
207-
frame.setFunction(nativeNoLocRe.group(2));
208-
frames.add(frame);
209-
lastJavaFrame = null;
210-
} else if (matches(javaRe, text)) {
261+
if (matches(javaRe, text)) {
211262
final SentryStackFrame frame = new SentryStackFrame();
212263
final String packageName = javaRe.group(1);
213264
final String className = javaRe.group(2);
@@ -219,6 +270,31 @@ private SentryStackTrace parseStacktrace(
219270
frame.setInApp(stackTraceFactory.isInApp(module));
220271
frames.add(frame);
221272
lastJavaFrame = frame;
273+
} else if (matches(nativeRe, text)) {
274+
final SentryStackFrame frame = new SentryStackFrame();
275+
frame.setPackage(nativeRe.group(3));
276+
frame.setFunction(nativeRe.group(6));
277+
frame.setLineno(getInteger(nativeRe, 7, null));
278+
frame.setInstructionAddr("0x" + nativeRe.group(2));
279+
frame.setPlatform("native");
280+
281+
final String buildId = nativeRe.group(8);
282+
final String debugId = buildId == null ? null : buildIdToDebugId(buildId);
283+
if (debugId != null) {
284+
if (!debugImages.containsKey(debugId)) {
285+
final DebugImage debugImage = new DebugImage();
286+
debugImage.setDebugId(debugId);
287+
debugImage.setType("elf");
288+
debugImage.setCodeFile(nativeRe.group(4));
289+
debugImage.setCodeId(buildId);
290+
debugImages.put(debugId, debugImage);
291+
}
292+
// The addresses in the thread dump are relative to the image
293+
frame.setAddrMode("rel:" + debugId);
294+
}
295+
296+
frames.add(frame);
297+
lastJavaFrame = null;
222298
} else if (matches(jniRe, text)) {
223299
final SentryStackFrame frame = new SentryStackFrame();
224300
final String packageName = jniRe.group(1);
@@ -227,6 +303,7 @@ private SentryStackTrace parseStacktrace(
227303
frame.setModule(module);
228304
frame.setFunction(jniRe.group(3));
229305
frame.setInApp(stackTraceFactory.isInApp(module));
306+
frame.setNative(true);
230307
frames.add(frame);
231308
lastJavaFrame = frame;
232309
} else if (matches(lockedRe, text)) {
@@ -334,8 +411,8 @@ private Long getLong(
334411

335412
@Nullable
336413
private Integer getInteger(
337-
final @NotNull Matcher matcher, final int group, final @Nullable Integer defaultValue) {
338-
final String str = matcher.group(group);
414+
final @NotNull Matcher matcher, final int groupIndex, final @Nullable Integer defaultValue) {
415+
final String str = matcher.group(groupIndex);
339416
if (str == null || str.length() == 0) {
340417
return defaultValue;
341418
} else {

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,15 @@ class AnrV2IntegrationTest {
305305
)
306306
assertEquals("__start_thread", firstFrame.function)
307307
assertEquals(64, firstFrame.lineno)
308+
assertEquals("0x00000000000530b8", firstFrame.instructionAddr)
309+
assertEquals("native", firstFrame.platform)
310+
assertEquals("rel:741f3301-bbb0-b92c-58bd-c15282b8ec7b", firstFrame.addrMode)
311+
312+
val image = it.debugMeta?.images?.find {
313+
it.debugId == "741f3301-bbb0-b92c-58bd-c15282b8ec7b"
314+
}
315+
assertNotNull(image)
316+
assertEquals("/apex/com.android.runtime/lib64/bionic/libc.so", image.codeFile)
308317
},
309318
argThat<Hint> {
310319
val hint = HintUtils.getSentrySdkHint(this)

0 commit comments

Comments
 (0)