Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## Unreleased

### Features

- Add native stack frame address information and debug image metadata to ANR events ([#4061](https://github.com/getsentry/sentry-java/pull/4061))
- This enables symbolication for stripped native code in ANRs

## 8.1.0

### Features
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import io.sentry.hints.AbnormalExit;
import io.sentry.hints.Backfillable;
import io.sentry.hints.BlockingFlushHint;
import io.sentry.protocol.DebugImage;
import io.sentry.protocol.DebugMeta;
import io.sentry.protocol.Message;
import io.sentry.protocol.SentryId;
import io.sentry.protocol.SentryThread;
Expand Down Expand Up @@ -267,6 +269,11 @@ private void reportAsSentryEvent(
event.setMessage(sentryMessage);
} else if (result.type == ParseResult.Type.DUMP) {
event.setThreads(result.threads);
if (result.debugImages != null) {
final DebugMeta debugMeta = new DebugMeta();
debugMeta.setImages(result.debugImages);
event.setDebugMeta(debugMeta);
}
}
event.setLevel(SentryLevel.FATAL);
event.setTimestamp(DateUtils.getDateTime(anrTimestamp));
Expand Down Expand Up @@ -311,15 +318,19 @@ private void reportAsSentryEvent(
final Lines lines = Lines.readLines(reader);

final ThreadDumpParser threadDumpParser = new ThreadDumpParser(options, isBackground);
final List<SentryThread> threads = threadDumpParser.parse(lines);
threadDumpParser.parse(lines);

final @NotNull List<SentryThread> threads = threadDumpParser.getThreads();
final @NotNull List<DebugImage> debugImages = threadDumpParser.getDebugImages();

if (threads.isEmpty()) {
// if the list is empty this means the system failed to capture a proper thread dump of
// the android threads, and only contains kernel-level threads and statuses, those ANRs
// are not actionable and neither they are reported by Google Play Console, so we just
// fall back to not reporting them
return new ParseResult(ParseResult.Type.NO_DUMP);
}
return new ParseResult(ParseResult.Type.DUMP, dump, threads);
return new ParseResult(ParseResult.Type.DUMP, dump, threads, debugImages);
} catch (Throwable e) {
options.getLogger().log(SentryLevel.WARNING, "Failed to parse ANR thread dump", e);
return new ParseResult(ParseResult.Type.ERROR, dump);
Expand Down Expand Up @@ -403,24 +414,31 @@ enum Type {
final Type type;
final byte[] dump;
final @Nullable List<SentryThread> threads;
final @Nullable List<DebugImage> debugImages;

ParseResult(final @NotNull Type type) {
this.type = type;
this.dump = null;
this.threads = null;
this.debugImages = null;
}

ParseResult(final @NotNull Type type, final byte[] dump) {
this.type = type;
this.dump = dump;
this.threads = null;
this.debugImages = null;
}

ParseResult(
final @NotNull Type type, final byte[] dump, final @Nullable List<SentryThread> threads) {
final @NotNull Type type,
final byte[] dump,
final @Nullable List<SentryThread> threads,
final @Nullable List<DebugImage> debugImages) {
this.type = type;
this.dump = dump;
this.threads = threads;
this.debugImages = debugImages;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,14 @@
import io.sentry.SentryLockReason;
import io.sentry.SentryOptions;
import io.sentry.SentryStackTraceFactory;
import io.sentry.protocol.DebugImage;
import io.sentry.protocol.SentryStackFrame;
import io.sentry.protocol.SentryStackTrace;
import io.sentry.protocol.SentryThread;
import java.math.BigInteger;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
Expand All @@ -42,12 +47,40 @@ public class ThreadDumpParser {
private static final Pattern BEGIN_UNMANAGED_NATIVE_THREAD_RE =
Pattern.compile("\"(.*)\" (.*) ?sysTid=(\\d+)");

// For reference, see native_stack_dump.cc and tombstone_proto_to_text.cpp in Android sources
// Groups
// 0:entire regex
// 1:index
// 2:pc
// 3:mapinfo
// 4:filename
// 5:mapoffset
// 6:function
// 7:fnoffset
// 7:buildid
private static final Pattern NATIVE_RE =
Pattern.compile(
" *(?:native: )?#\\d+ \\S+ [0-9a-fA-F]+\\s+(.*?)\\s+\\((.*)\\+(\\d+)\\)(?: \\(.*\\))?");
private static final Pattern NATIVE_NO_LOC_RE =
Pattern.compile(
" *(?:native: )?#\\d+ \\S+ [0-9a-fA-F]+\\s+(.*)\\s*\\(?(.*)\\)?(?: \\(.*\\))?");
// " native: #12 pc 0xabcd1234"
" *(?:native: )?#(\\d+) \\S+ ([0-9a-fA-F]+)"
// The map info includes a filename and an optional offset into the file
+ ("\\s+("
// "/path/to/file.ext",
+ "(.*?)"
// optional " (deleted)" suffix (deleted files) needed here to bias regex
// correctly
+ "(?:\\s+\\(deleted\\))?"
// " (offset 0xabcd1234)", if the mapping is not into the beginning of the file
+ "(?:\\s+\\(offset (.*?)\\))?"
+ ")")
// Optional function
+ ("(?:\\s+\\((?:"
+ "\\?\\?\\?" // " (???) marks a missing function, so don't capture it in a group
+ "|(.*?)(?:\\+(\\d+))?" // " (func+1234)", offset is
// optional
+ ")\\))?")
// Optional " (BuildId: abcd1234abcd1234abcd1234abcd1234abcd1234)"
+ "(?:\\s+\\(BuildId: (.*?)\\))?");

private static final Pattern JAVA_RE =
Pattern.compile(" *at (?:(.+)\\.)?([^.]+)\\.([^.]+)\\((.*):([\\d-]+)\\)");
private static final Pattern JNI_RE =
Expand Down Expand Up @@ -75,15 +108,48 @@ public class ThreadDumpParser {

private final @NotNull SentryStackTraceFactory stackTraceFactory;

private final @NotNull Map<String, DebugImage> debugImages;

private final @NotNull List<SentryThread> threads;

public ThreadDumpParser(final @NotNull SentryOptions options, final boolean isBackground) {
this.options = options;
this.isBackground = isBackground;
this.stackTraceFactory = new SentryStackTraceFactory(options);
this.debugImages = new HashMap<>();
this.threads = new ArrayList<>();
}

@NotNull
public List<DebugImage> getDebugImages() {
return new ArrayList<>(debugImages.values());
}

@NotNull
public List<SentryThread> parse(final @NotNull Lines lines) {
final List<SentryThread> sentryThreads = new ArrayList<>();
public List<SentryThread> getThreads() {
return threads;
}

@Nullable
private static String buildIdToDebugId(String buildId) {
try {
// Abuse BigInteger as a hex string parser. Extra byte needed to handle leading zeros.
final ByteBuffer buf = ByteBuffer.wrap(new BigInteger("10" + buildId, 16).toByteArray());
buf.get();
return String.format(
"%08x-%04x-%04x-%04x-%04x%08x",
buf.order(ByteOrder.LITTLE_ENDIAN).getInt(),
buf.getShort(),
buf.getShort(),
buf.order(ByteOrder.BIG_ENDIAN).getShort(),
buf.getShort(),
buf.getInt());
} catch (NumberFormatException | BufferUnderflowException e) {
return null;
}
}

public void parse(final @NotNull Lines lines) {

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

final SentryThread thread = parseThread(lines);
if (thread != null) {
sentryThreads.add(thread);
threads.add(thread);
}
}
}
return sentryThreads;
}

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

final Matcher nativeRe = NATIVE_RE.matcher("");
final Matcher nativeNoLocRe = NATIVE_NO_LOC_RE.matcher("");
final Matcher javaRe = JAVA_RE.matcher("");
final Matcher jniRe = JNI_RE.matcher("");
final Matcher lockedRe = LOCKED_RE.matcher("");
Expand All @@ -194,20 +258,7 @@ private SentryStackTrace parseStacktrace(
break;
}
final String text = line.text;
if (matches(nativeRe, text)) {
final SentryStackFrame frame = new SentryStackFrame();
frame.setPackage(nativeRe.group(1));
frame.setFunction(nativeRe.group(2));
frame.setLineno(getInteger(nativeRe, 3, null));
frames.add(frame);
lastJavaFrame = null;
} else if (matches(nativeNoLocRe, text)) {
final SentryStackFrame frame = new SentryStackFrame();
frame.setPackage(nativeNoLocRe.group(1));
frame.setFunction(nativeNoLocRe.group(2));
frames.add(frame);
lastJavaFrame = null;
} else if (matches(javaRe, text)) {
if (matches(javaRe, text)) {
final SentryStackFrame frame = new SentryStackFrame();
final String packageName = javaRe.group(1);
final String className = javaRe.group(2);
Expand All @@ -219,6 +270,31 @@ private SentryStackTrace parseStacktrace(
frame.setInApp(stackTraceFactory.isInApp(module));
frames.add(frame);
lastJavaFrame = frame;
} else if (matches(nativeRe, text)) {
final SentryStackFrame frame = new SentryStackFrame();
frame.setPackage(nativeRe.group(3));
frame.setFunction(nativeRe.group(6));
frame.setLineno(getInteger(nativeRe, 7, null));
frame.setInstructionAddr("0x" + nativeRe.group(2));
frame.setPlatform("native");

final String buildId = nativeRe.group(8);
final String debugId = buildId == null ? null : buildIdToDebugId(buildId);
if (debugId != null) {
if (!debugImages.containsKey(debugId)) {
final DebugImage debugImage = new DebugImage();
debugImage.setDebugId(debugId);
debugImage.setType("elf");
debugImage.setCodeFile(nativeRe.group(4));
debugImage.setCodeId(buildId);
debugImages.put(debugId, debugImage);
}
// The addresses in the thread dump are relative to the image
frame.setAddrMode("rel:" + debugId);
}

frames.add(frame);
lastJavaFrame = null;
} else if (matches(jniRe, text)) {
final SentryStackFrame frame = new SentryStackFrame();
final String packageName = jniRe.group(1);
Expand All @@ -227,6 +303,7 @@ private SentryStackTrace parseStacktrace(
frame.setModule(module);
frame.setFunction(jniRe.group(3));
frame.setInApp(stackTraceFactory.isInApp(module));
frame.setNative(true);
frames.add(frame);
lastJavaFrame = frame;
} else if (matches(lockedRe, text)) {
Expand Down Expand Up @@ -334,8 +411,8 @@ private Long getLong(

@Nullable
private Integer getInteger(
final @NotNull Matcher matcher, final int group, final @Nullable Integer defaultValue) {
final String str = matcher.group(group);
final @NotNull Matcher matcher, final int groupIndex, final @Nullable Integer defaultValue) {
final String str = matcher.group(groupIndex);
if (str == null || str.length() == 0) {
return defaultValue;
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,15 @@ class AnrV2IntegrationTest {
)
assertEquals("__start_thread", firstFrame.function)
assertEquals(64, firstFrame.lineno)
assertEquals("0x00000000000530b8", firstFrame.instructionAddr)
assertEquals("native", firstFrame.platform)
assertEquals("rel:741f3301-bbb0-b92c-58bd-c15282b8ec7b", firstFrame.addrMode)

val image = it.debugMeta?.images?.find {
it.debugId == "741f3301-bbb0-b92c-58bd-c15282b8ec7b"
}
assertNotNull(image)
assertEquals("/apex/com.android.runtime/lib64/bionic/libc.so", image.codeFile)
},
argThat<Hint> {
val hint = HintUtils.getSentrySdkHint(this)
Expand Down
Loading