diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 98b5cc37bfe..fc8219c4ec7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -41,6 +41,7 @@ spotless = "7.0.4" gummyBears = "0.12.0" camerax = "1.3.0" openfeature = "1.18.2" +protobuf = "4.33.1" [plugins] kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } @@ -60,6 +61,7 @@ spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } detekt = { id = "io.gitlab.arturbosch.detekt", version = "1.23.8" } jacoco-android = { id = "com.mxalbert.gradle.jacoco-android", version = "0.2.0" } kover = { id = "org.jetbrains.kotlinx.kover", version = "0.7.3" } +protobuf = { id = "com.google.protobuf", version = "0.9.5" } vanniktech-maven-publish = { id = "com.vanniktech.maven.publish", version = "0.30.0" } springboot2 = { id = "org.springframework.boot", version.ref = "springboot2" } springboot3 = { id = "org.springframework.boot", version.ref = "springboot3" } @@ -138,6 +140,8 @@ otel-javaagent-extension-api = { module = "io.opentelemetry.javaagent:openteleme otel-semconv = { module = "io.opentelemetry.semconv:opentelemetry-semconv", version.ref = "otelSemanticConventions" } otel-semconv-incubating = { module = "io.opentelemetry.semconv:opentelemetry-semconv-incubating", version.ref = "otelSemanticConventionsAlpha" } p6spy = { module = "p6spy:p6spy", version = "3.9.1" } +protobuf-javalite = { module = "com.google.protobuf:protobuf-javalite", version.ref = "protobuf"} +protoc = { module = "com.google.protobuf:protoc", version.ref = "protobuf" } quartz = { module = "org.quartz-scheduler:quartz", version = "2.3.0" } reactor-core = { module = "io.projectreactor:reactor-core", version = "3.5.3" } retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index 1c89d8524c0..6efbc5d4f70 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -339,6 +339,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr public fun isEnableSystemEventBreadcrumbs ()Z public fun isEnableSystemEventBreadcrumbsExtras ()Z public fun isReportHistoricalAnrs ()Z + public fun isTombstoneEnabled ()Z public fun setAnrEnabled (Z)V public fun setAnrReportInDebug (Z)V public fun setAnrTimeoutIntervalMillis (J)V @@ -367,6 +368,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr public fun setNativeHandlerStrategy (Lio/sentry/android/core/NdkHandlerStrategy;)V public fun setNativeSdkName (Ljava/lang/String;)V public fun setReportHistoricalAnrs (Z)V + public fun setTombstoneEnabled (Z)V } public abstract interface class io/sentry/android/core/SentryAndroidOptions$BeforeCaptureCallback { @@ -455,6 +457,17 @@ public final class io/sentry/android/core/SystemEventsBreadcrumbsIntegration : i public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V } +public class io/sentry/android/core/TombstoneIntegration : io/sentry/Integration, java/io/Closeable { + public fun (Landroid/content/Context;)V + public fun close ()V + public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V +} + +public class io/sentry/android/core/TombstoneIntegration$TombstoneProcessor : java/lang/Runnable { + public fun (Landroid/content/Context;Lio/sentry/IScopes;Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/transport/ICurrentDateProvider;)V + public fun run ()V +} + public final class io/sentry/android/core/UserInteractionIntegration : android/app/Application$ActivityLifecycleCallbacks, io/sentry/Integration, java/io/Closeable { public fun (Landroid/app/Application;Lio/sentry/util/LoadClass;)V public fun close ()V diff --git a/sentry-android-core/build.gradle.kts b/sentry-android-core/build.gradle.kts index 99d6b5115c8..e293fd76ee9 100644 --- a/sentry-android-core/build.gradle.kts +++ b/sentry-android-core/build.gradle.kts @@ -8,6 +8,7 @@ plugins { alias(libs.plugins.jacoco.android) alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) + alias(libs.plugins.protobuf) } android { @@ -83,6 +84,7 @@ dependencies { implementation(libs.androidx.lifecycle.common.java8) implementation(libs.androidx.lifecycle.process) implementation(libs.androidx.core) + implementation(libs.protobuf.javalite) errorprone(libs.errorprone.core) errorprone(libs.nopen.checker) @@ -109,3 +111,10 @@ dependencies { testRuntimeOnly(libs.androidx.fragment.ktx) testRuntimeOnly(libs.timber) } + +protobuf { + protoc { artifact = libs.protoc.get().toString() } + generateProtoTasks { + all().forEach { task -> task.builtins { create("java") { option("lite") } } } + } +} diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java index 4e679a22e96..ef60b406ecd 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java @@ -5,6 +5,7 @@ import android.app.Application; import android.content.Context; import android.content.pm.PackageInfo; +import android.os.Build; import io.sentry.CompositePerformanceCollector; import io.sentry.DeduplicateMultithreadedEventProcessor; import io.sentry.DefaultCompositePerformanceCollector; @@ -372,6 +373,10 @@ static void installDefaultIntegrations( final Class sentryNdkClass = loadClass.loadClass(SENTRY_NDK_CLASS_NAME, options.getLogger()); options.addIntegration(new NdkIntegration(sentryNdkClass)); + if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.R) { + options.addIntegration(new TombstoneIntegration(context)); + } + // this integration uses android.os.FileObserver, we can't move to sentry // before creating a pure java impl. options.addIntegration(EnvelopeFileObserverIntegration.getOutboxFileObserver()); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java index 221495172eb..5f21444933d 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java @@ -82,7 +82,7 @@ public final class SentryAndroidOptions extends SentryOptions { *
  • The transaction status will be {@link SpanStatus#OK} if none is set. * * - * The transaction is automatically bound to the {@link IScope}, but only if there's no + *

    The transaction is automatically bound to the {@link IScope}, but only if there's no * transaction already bound to the Scope. */ private boolean enableAutoActivityLifecycleTracing = true; @@ -227,6 +227,8 @@ public interface BeforeCaptureCallback { private @Nullable SentryFrameMetricsCollector frameMetricsCollector; + private boolean enableTombstone = false; + public SentryAndroidOptions() { setSentryClientName(BuildConfig.SENTRY_ANDROID_SDK_NAME + "/" + BuildConfig.VERSION_NAME); setSdkVersion(createSdkVersion()); @@ -300,6 +302,27 @@ public void setAnrReportInDebug(boolean anrReportInDebug) { this.anrReportInDebug = anrReportInDebug; } + /** + * Sets Tombstone reporting (ApplicationExitInfo.REASON_CRASH_NATIVE) to enabled or disabled. + * + * @param enableTombstone true for enabled and false for disabled + */ + @ApiStatus.Internal + public void setTombstoneEnabled(boolean enableTombstone) { + this.enableTombstone = enableTombstone; + } + + /** + * Checks if Tombstone reporting (ApplicationExitInfo.REASON_CRASH_NATIVE) is enabled or disabled + * Default is disabled + * + * @return true if enabled or false otherwise + */ + @ApiStatus.Internal + public boolean isTombstoneEnabled() { + return enableTombstone; + } + public boolean isEnableActivityLifecycleBreadcrumbs() { return enableActivityLifecycleBreadcrumbs; } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/TombstoneIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/TombstoneIntegration.java new file mode 100644 index 00000000000..0b20553495f --- /dev/null +++ b/sentry-android-core/src/main/java/io/sentry/android/core/TombstoneIntegration.java @@ -0,0 +1,195 @@ +package io.sentry.android.core; + +import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion; + +import android.app.ActivityManager; +import android.app.ApplicationExitInfo; +import android.content.Context; +import android.os.Build; +import androidx.annotation.RequiresApi; +import io.sentry.DateUtils; +import io.sentry.IScopes; +import io.sentry.Integration; +import io.sentry.SentryEvent; +import io.sentry.SentryLevel; +import io.sentry.SentryOptions; +import io.sentry.android.core.internal.tombstone.TombstoneParser; +import io.sentry.cache.EnvelopeCache; +import io.sentry.cache.IEnvelopeCache; +import io.sentry.transport.CurrentDateProvider; +import io.sentry.transport.ICurrentDateProvider; +import io.sentry.util.Objects; +import java.io.Closeable; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public class TombstoneIntegration implements Integration, Closeable { + static final long NINETY_DAYS_THRESHOLD = TimeUnit.DAYS.toMillis(91); + + private final @NotNull Context context; + private final @NotNull ICurrentDateProvider dateProvider; + private @Nullable SentryAndroidOptions options; + + public TombstoneIntegration(final @NotNull Context context) { + // using CurrentDateProvider instead of AndroidCurrentDateProvider as AppExitInfo uses + // System.currentTimeMillis + this(context, CurrentDateProvider.getInstance()); + } + + TombstoneIntegration( + final @NotNull Context context, final @NotNull ICurrentDateProvider dateProvider) { + this.context = ContextUtils.getApplicationContext(context); + this.dateProvider = dateProvider; + } + + @Override + public void register(@NotNull IScopes scopes, @NotNull SentryOptions options) { + this.options = + Objects.requireNonNull( + (options instanceof SentryAndroidOptions) ? (SentryAndroidOptions) options : null, + "SentryAndroidOptions is required"); + + this.options + .getLogger() + .log( + SentryLevel.DEBUG, + "TombstoneIntegration enabled: %s", + this.options.isTombstoneEnabled()); + + if (this.options.isTombstoneEnabled()) { + if (this.options.getCacheDirPath() == null) { + this.options + .getLogger() + .log(SentryLevel.INFO, "Cache dir is not set, unable to process Tombstones"); + return; + } + + try { + options + .getExecutorService() + .submit(new TombstoneProcessor(context, scopes, this.options, dateProvider)); + } catch (Throwable e) { + options.getLogger().log(SentryLevel.DEBUG, "Failed to start TombstoneProcessor.", e); + } + options.getLogger().log(SentryLevel.DEBUG, "TombstoneIntegration installed."); + addIntegrationToSdkVersion("Tombstone"); + } + } + + @Override + public void close() throws IOException { + if (options != null) { + options.getLogger().log(SentryLevel.DEBUG, "TombstoneIntegration removed."); + } + } + + @ApiStatus.Internal + public static class TombstoneProcessor implements Runnable { + + @NotNull private final Context context; + @NotNull private final IScopes scopes; + @NotNull private final SentryAndroidOptions options; + private final long threshold; + + public TombstoneProcessor( + @NotNull Context context, + @NotNull IScopes scopes, + @NotNull SentryAndroidOptions options, + @NotNull ICurrentDateProvider dateProvider) { + this.context = context; + this.scopes = scopes; + this.options = options; + + this.threshold = dateProvider.getCurrentTimeMillis() - NINETY_DAYS_THRESHOLD; + } + + @Override + @RequiresApi(api = Build.VERSION_CODES.R) + public void run() { + final ActivityManager activityManager = + (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + + final List applicationExitInfoList; + applicationExitInfoList = activityManager.getHistoricalProcessExitReasons(null, 0, 0); + + if (applicationExitInfoList.isEmpty()) { + options.getLogger().log(SentryLevel.DEBUG, "No records in historical exit reasons."); + return; + } + + final IEnvelopeCache cache = options.getEnvelopeDiskCache(); + if (cache instanceof EnvelopeCache) { + if (options.isEnableAutoSessionTracking() + && !((EnvelopeCache) cache).waitPreviousSessionFlush()) { + options + .getLogger() + .log( + SentryLevel.WARNING, + "Timed out waiting to flush previous session to its own file."); + + // if we timed out waiting here, we can already flush the latch, because the timeout is + // big + // enough to wait for it only once and we don't have to wait again in + // PreviousSessionFinalizer + ((EnvelopeCache) cache).flushPreviousSession(); + } + } + + // making a deep copy as we're modifying the list + final List exitInfos = new ArrayList<>(applicationExitInfoList); + + // search for the latest Tombstone to report it separately as we're gonna enrich it. The + // latest + // Tombstone will be first in the list, as it's filled last-to-first in order of appearance + ApplicationExitInfo latestTombstone = null; + for (ApplicationExitInfo applicationExitInfo : exitInfos) { + if (applicationExitInfo.getReason() == ApplicationExitInfo.REASON_CRASH_NATIVE) { + latestTombstone = applicationExitInfo; + // remove it, so it's not reported twice + // TODO: if we fail after this, we effectively lost the ApplicationExitInfo (maybe only + // remove after we reported it) + exitInfos.remove(applicationExitInfo); + break; + } + } + + if (latestTombstone == null) { + options + .getLogger() + .log( + SentryLevel.DEBUG, + "No Tombstones have been found in the historical exit reasons list."); + return; + } + + if (latestTombstone.getTimestamp() < threshold) { + options + .getLogger() + .log(SentryLevel.DEBUG, "Latest Tombstones happened too long ago, returning early."); + return; + } + + reportAsSentryEvent(latestTombstone); + } + + @RequiresApi(api = Build.VERSION_CODES.R) + private void reportAsSentryEvent(ApplicationExitInfo exitInfo) { + SentryEvent event; + try { + TombstoneParser parser = new TombstoneParser(exitInfo.getTraceInputStream()); + event = parser.parse(); + event.setTimestamp(DateUtils.getDateTime(exitInfo.getTimestamp())); + } catch (IOException e) { + throw new RuntimeException(e); + } + + scopes.captureEvent(event); + } + } +} diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/tombstone/TombstoneParser.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/tombstone/TombstoneParser.java new file mode 100644 index 00000000000..f1efc4bfaa6 --- /dev/null +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/tombstone/TombstoneParser.java @@ -0,0 +1,195 @@ +package io.sentry.android.core.internal.tombstone; + +import androidx.annotation.NonNull; +import io.sentry.SentryEvent; +import io.sentry.SentryLevel; +import io.sentry.protocol.DebugImage; +import io.sentry.protocol.DebugMeta; +import io.sentry.protocol.Mechanism; +import io.sentry.protocol.Message; +import io.sentry.protocol.SentryException; +import io.sentry.protocol.SentryStackFrame; +import io.sentry.protocol.SentryStackTrace; +import io.sentry.protocol.SentryThread; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +public class TombstoneParser { + + private final InputStream tombstoneStream; + private final Map excTypeValueMap = new HashMap<>(); + + public TombstoneParser(InputStream tombstoneStream) { + this.tombstoneStream = tombstoneStream; + + // keep the current signal type -> value mapping for compatibility + excTypeValueMap.put("SIGILL", "IllegalInstruction"); + excTypeValueMap.put("SIGTRAP", "Trap"); + excTypeValueMap.put("SIGABRT", "Abort"); + excTypeValueMap.put("SIGBUS", "BusError"); + excTypeValueMap.put("SIGFPE", "FloatingPointException"); + excTypeValueMap.put("SIGSEGV", "Segfault"); + } + + public SentryEvent parse() throws IOException { + TombstoneProtos.Tombstone tombstone = TombstoneProtos.Tombstone.parseFrom(tombstoneStream); + + SentryEvent event = new SentryEvent(); + event.setLevel(SentryLevel.FATAL); + + // must use the "native" platform because otherwise the stack-trace wouldn't be correctly parsed + event.setPlatform("native"); + + event.setMessage(constructMessage(tombstone)); + event.setDebugMeta(createDebugMeta(tombstone)); + event.setExceptions(createException(tombstone)); + assert event.getExceptions() != null; + event.setThreads(createThreads(tombstone, event.getExceptions().get(0))); + + return event; + } + + @NonNull + private List createThreads( + TombstoneProtos.Tombstone tombstone, SentryException exc) { + List threads = new ArrayList<>(); + for (Map.Entry threadEntry : + tombstone.getThreadsMap().entrySet()) { + + SentryThread thread = new SentryThread(); + thread.setId(Long.valueOf(threadEntry.getKey())); + thread.setName(threadEntry.getValue().getName()); + + SentryStackTrace stacktrace = createStackTrace(threadEntry); + thread.setStacktrace(stacktrace); + if (tombstone.getTid() == threadEntry.getValue().getId()) { + thread.setCrashed(true); + // even though we refer to the thread_id from the exception, + // the backend currently requires a stack-trace in exception + exc.setStacktrace(stacktrace); + } + threads.add(thread); + } + + return threads; + } + + @NonNull + private static SentryStackTrace createStackTrace( + Map.Entry threadEntry) { + List frames = new ArrayList<>(); + + for (TombstoneProtos.BacktraceFrame frame : threadEntry.getValue().getCurrentBacktraceList()) { + SentryStackFrame stackFrame = new SentryStackFrame(); + stackFrame.setPackage(frame.getFileName()); + stackFrame.setFunction(frame.getFunctionName()); + stackFrame.setInstructionAddr(String.format("0x%x", frame.getPc())); + frames.add(0, stackFrame); + } + + SentryStackTrace stacktrace = new SentryStackTrace(); + stacktrace.setFrames(frames); + + Map unknown = new HashMap<>(); + // `libunwindstack` used for tombstones already applies instruction address adjustment: + // https://android.googlesource.com/platform/system/unwinding/+/refs/heads/main/libunwindstack/Regs.cpp#175 + // prevent "processing" from doing it again. + unknown.put("instruction_addr_adjustment", "none"); + stacktrace.setUnknown(unknown); + + Map registers = new HashMap<>(); + for (TombstoneProtos.Register register : threadEntry.getValue().getRegistersList()) { + registers.put(register.getName(), String.format("0x%x", register.getU64())); + } + stacktrace.setRegisters(registers); + + return stacktrace; + } + + @NonNull + private List createException(TombstoneProtos.Tombstone tombstone) { + TombstoneProtos.Signal signalInfo = tombstone.getSignalInfo(); + + SentryException exception = new SentryException(); + exception.setType(signalInfo.getName()); + exception.setValue(excTypeValueMap.get(signalInfo.getName())); + exception.setMechanism(createMechanismFromSignalInfo(signalInfo)); + exception.setThreadId((long) tombstone.getTid()); + + List exceptions = new ArrayList<>(1); + exceptions.add(exception); + + return exceptions; + } + + @NonNull + private static Mechanism createMechanismFromSignalInfo(TombstoneProtos.Signal signalInfo) { + Map meta = new HashMap<>(); + meta.put("number", signalInfo.getNumber()); + meta.put("name", signalInfo.getName()); + meta.put("code", signalInfo.getCode()); + meta.put("code_name", signalInfo.getCodeName()); + + Mechanism mechanism = new Mechanism(); + // this follows the current processing triggers strictly, changing any of these + // alters grouping and name (long-term we might want to have a tombstone mechanism) + mechanism.setType("signalhandler"); + mechanism.setHandled(false); + mechanism.setSynthetic(true); + mechanism.setMeta(meta); + + return mechanism; + } + + @NonNull + private Message constructMessage(TombstoneProtos.Tombstone tombstone) { + Message message = new Message(); + TombstoneProtos.Signal signalInfo = tombstone.getSignalInfo(); + + // reproduce the message `debuggerd` would use to dump the stack trace in logcat + message.setFormatted( + String.format( + Locale.getDefault(), + "Fatal signal %s (%d), %s (%d), pid = %d (%s)", + signalInfo.getName(), + signalInfo.getNumber(), + signalInfo.getCodeName(), + signalInfo.getCode(), + tombstone.getPid(), + String.join(" ", tombstone.getCommandLineList()))); + + return message; + } + + private DebugMeta createDebugMeta(TombstoneProtos.Tombstone tombstone) { + List images = new ArrayList<>(); + + for (TombstoneProtos.MemoryMapping module : tombstone.getMemoryMappingsList()) { + // exclude anonymous and non-executable maps + if (module.getBuildId().isEmpty() + || module.getMappingName().isEmpty() + || !module.getExecute()) { + continue; + } + DebugImage image = new DebugImage(); + image.setCodeId(module.getBuildId()); + image.setCodeFile(module.getMappingName()); + image.setDebugId(module.getBuildId()); + image.setImageAddr(String.format("0x%x", module.getBeginAddress())); + image.setImageSize(module.getEndAddress() - module.getBeginAddress()); + image.setType("elf"); + + images.add(image); + } + + DebugMeta debugMeta = new DebugMeta(); + debugMeta.setImages(images); + + return debugMeta; + } +} diff --git a/sentry-android-core/src/main/proto/io/sentry/android/core/internal/tombstone/tombstone.proto b/sentry-android-core/src/main/proto/io/sentry/android/core/internal/tombstone/tombstone.proto new file mode 100644 index 00000000000..c75eae32688 --- /dev/null +++ b/sentry-android-core/src/main/proto/io/sentry/android/core/internal/tombstone/tombstone.proto @@ -0,0 +1,204 @@ +// Added and adapted from: https://android.googlesource.com/platform/system/core/+/refs/heads/main/debuggerd/proto/tombstone.proto +// Sentry changes: +// * change the java_package +// +// Protobuf definition for Android tombstones. +// +// An app can get hold of these for any `REASON_CRASH_NATIVE` instance of +// `android.app.ApplicationExitInfo`. +// +// https://developer.android.com/reference/android/app/ApplicationExitInfo#getTraceInputStream() +// +syntax = "proto3"; +option java_package = "io.sentry.android.core.internal.tombstone"; +option java_outer_classname = "TombstoneProtos"; +// NOTE TO OEMS: +// If you add custom fields to this proto, do not use numbers in the reserved range. +// NOTE TO CONSUMERS: +// With proto3 -- unlike proto2 -- HasValue is unreliable for any field +// where the default value for that type is also a valid value for the field. +// This means, for example, that a boolean that is false or an integer that +// is zero will appear to be missing --- but because they're not actually +// marked as `optional` in this schema, consumers should just use values +// without first checking whether or not they're "present". +// https://protobuf.dev/programming-guides/proto3/#default +message CrashDetail { + bytes name = 1; + bytes data = 2; + reserved 3 to 999; +} +message StackHistoryBufferEntry { + BacktraceFrame addr = 1; + uint64 fp = 2; + uint64 tag = 3; + reserved 4 to 999; +} +message StackHistoryBuffer { + uint64 tid = 1; + repeated StackHistoryBufferEntry entries = 2; + reserved 3 to 999; +} +message Tombstone { + Architecture arch = 1; + Architecture guest_arch = 24; + string build_fingerprint = 2; + string revision = 3; + string timestamp = 4; + uint32 pid = 5; + uint32 tid = 6; + uint32 uid = 7; + string selinux_label = 8; + repeated string command_line = 9; + // Process uptime in seconds. + uint32 process_uptime = 20; + Signal signal_info = 10; + string abort_message = 14; + repeated CrashDetail crash_details = 21; + repeated Cause causes = 15; + map threads = 16; + map guest_threads = 25; + repeated MemoryMapping memory_mappings = 17; + repeated LogBuffer log_buffers = 18; + repeated FD open_fds = 19; + uint32 page_size = 22; + bool has_been_16kb_mode = 23; + StackHistoryBuffer stack_history_buffer = 26; + reserved 27 to 999; +} +enum Architecture { + ARM32 = 0; + ARM64 = 1; + X86 = 2; + X86_64 = 3; + RISCV64 = 4; + NONE = 5; + reserved 6 to 999; +} +message Signal { + int32 number = 1; + string name = 2; + int32 code = 3; + string code_name = 4; + bool has_sender = 5; + int32 sender_uid = 6; + int32 sender_pid = 7; + bool has_fault_address = 8; + uint64 fault_address = 9; + // Note, may or may not contain the dump of the actual memory contents. Currently, on arm64, we + // only include metadata, and not the contents. + MemoryDump fault_adjacent_metadata = 10; + reserved 11 to 999; +} +message HeapObject { + uint64 address = 1; + uint64 size = 2; + uint64 allocation_tid = 3; + repeated BacktraceFrame allocation_backtrace = 4; + uint64 deallocation_tid = 5; + repeated BacktraceFrame deallocation_backtrace = 6; +} +message MemoryError { + enum Tool { + GWP_ASAN = 0; + SCUDO = 1; + reserved 2 to 999; + } + Tool tool = 1; + enum Type { + UNKNOWN = 0; + USE_AFTER_FREE = 1; + DOUBLE_FREE = 2; + INVALID_FREE = 3; + BUFFER_OVERFLOW = 4; + BUFFER_UNDERFLOW = 5; + reserved 6 to 999; + } + Type type = 2; + oneof location { + HeapObject heap = 3; + } + reserved 4 to 999; +} +message Cause { + string human_readable = 1; + oneof details { + MemoryError memory_error = 2; + } + reserved 3 to 999; +} +message Register { + string name = 1; + uint64 u64 = 2; + reserved 3 to 999; +} +message Thread { + int32 id = 1; + string name = 2; + repeated Register registers = 3; + repeated string backtrace_note = 7; + repeated string unreadable_elf_files = 9; + repeated BacktraceFrame current_backtrace = 4; + repeated MemoryDump memory_dump = 5; + int64 tagged_addr_ctrl = 6; + int64 pac_enabled_keys = 8; + reserved 10 to 999; +} +message BacktraceFrame { + uint64 rel_pc = 1; + uint64 pc = 2; + uint64 sp = 3; + string function_name = 4; + uint64 function_offset = 5; + string file_name = 6; + uint64 file_map_offset = 7; + string build_id = 8; + reserved 9 to 999; +} +message ArmMTEMetadata { + // One memory tag per granule (e.g. every 16 bytes) of regular memory. + bytes memory_tags = 1; + reserved 2 to 999; +} +message MemoryDump { + string register_name = 1; + string mapping_name = 2; + uint64 begin_address = 3; + bytes memory = 4; + oneof metadata { + ArmMTEMetadata arm_mte_metadata = 6; + } + reserved 5, 7 to 999; +} +message MemoryMapping { + uint64 begin_address = 1; + uint64 end_address = 2; + uint64 offset = 3; + bool read = 4; + bool write = 5; + bool execute = 6; + string mapping_name = 7; + string build_id = 8; + uint64 load_bias = 9; + reserved 10 to 999; +} +message FD { + int32 fd = 1; + string path = 2; + string owner = 3; + uint64 tag = 4; + reserved 5 to 999; +} +message LogBuffer { + string name = 1; + repeated LogMessage logs = 2; + reserved 3 to 999; +} +message LogMessage { + string timestamp = 1; + uint32 pid = 2; + uint32 tid = 3; + uint32 priority = 4; + string tag = 5; + string message = 6; + reserved 7 to 999; +} diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/tombstone/TombstoneParserTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/tombstone/TombstoneParserTest.kt new file mode 100644 index 00000000000..f4e17f04565 --- /dev/null +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/tombstone/TombstoneParserTest.kt @@ -0,0 +1,112 @@ +package io.sentry.android.core.internal.tombstone + +import java.io.File +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +class TombstoneParserTest { + val expectedRegisters = + setOf( + "x8", + "x9", + "esr", + "lr", + "pst", + "x10", + "x12", + "x11", + "x14", + "x13", + "x16", + "x15", + "sp", + "x18", + "x17", + "x19", + "pc", + "x21", + "x20", + "x0", + "x23", + "x1", + "x22", + "x2", + "x25", + "x3", + "x24", + "x4", + "x27", + "x5", + "x26", + "x6", + "x29", + "x7", + "x28", + ) + + @Test + fun `parses a snapshot tombstone into Event`() { + val tombstone = File("src/test/resources/tombstone.pb") + val parser = TombstoneParser(tombstone.inputStream()) + val event = parser.parse() + + // top-level data + assertNotNull(event.eventId) + assertEquals( + "Fatal signal SIGSEGV (11), SEGV_MAPERR (1), pid = 21891 (io.sentry.samples.android)", + event.message!!.formatted, + ) + assertEquals("native", event.platform) + assertEquals("FATAL", event.level!!.name) + + // exception + // we only track one native exception (no nesting, one crashed thread) + assertEquals(1, event.exceptions!!.size) + val exception = event.exceptions!![0] + assertEquals("SIGSEGV", exception.type) + assertEquals("Segfault", exception.value) + val crashedThreadId = exception.threadId + assertNotNull(crashedThreadId) + + val mechanism = exception.mechanism + assertEquals("signalhandler", mechanism!!.type) + assertEquals(false, mechanism.isHandled) + assertEquals(true, mechanism.synthetic) + assertEquals("SIGSEGV", mechanism.meta!!["name"]) + assertEquals(11, mechanism.meta!!["number"]) + assertEquals("SEGV_MAPERR", mechanism.meta!!["code_name"]) + assertEquals(1, mechanism.meta!!["code"]) + + // threads + assertEquals(62, event.threads!!.size) + for (thread in event.threads!!) { + assertNotNull(thread.id) + if (thread.id == crashedThreadId) { + assert(thread.isCrashed == true) + } + assert(thread.stacktrace!!.frames!!.isNotEmpty()) + + for (frame in thread.stacktrace!!.frames!!) { + assertNotNull(frame.function) + assertNotNull(frame.`package`) + assertNotNull(frame.instructionAddr) + } + + assert(thread.stacktrace!!.registers!!.keys.containsAll(expectedRegisters)) + } + + // debug-meta + assertEquals(357, event.debugMeta!!.images!!.size) + for (image in event.debugMeta!!.images!!) { + assertEquals("elf", image.type) + assertNotNull(image.debugId) + assertNotNull(image.codeId) + assertEquals(image.codeId, image.debugId) + assertNotNull(image.codeFile) + val imageAddress = image.imageAddr!!.removePrefix("0x").toLong(16) + assert(imageAddress > 0) + assert(image.imageSize!! > 0) + } + } +} diff --git a/sentry-android-core/src/test/resources/tombstone.pb b/sentry-android-core/src/test/resources/tombstone.pb new file mode 100644 index 00000000000..051356a8fef Binary files /dev/null and b/sentry-android-core/src/test/resources/tombstone.pb differ