diff --git a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/CrashLogParser.java b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/CrashLogParser.java index 02136fa2eb..269e0d1ee6 100644 --- a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/CrashLogParser.java +++ b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/CrashLogParser.java @@ -36,7 +36,7 @@ public static CrashLog parse(String uuid, String content) { if (isJ9Javacore(content)) { return fromJ9Javacore(uuid, content); } - return fromHotspotCrashLog(uuid, content); + return new HotspotCrashLogParser().parse(uuid, content); } /** Check if the content appears to be a J9 javacore file. */ diff --git a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/CrashUploader.java b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/CrashUploader.java index a8090459e6..76fd0e45f8 100644 --- a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/CrashUploader.java +++ b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/CrashUploader.java @@ -579,6 +579,7 @@ private RequestBody makeErrorTrackingRequestBody(@Nonnull CrashLog payload, bool // experimental if (payload.experimental != null && (payload.experimental.ucontext != null + || payload.experimental.registerToMemoryMapping != null || payload.experimental.runtimeArgs != null)) { writer.name("experimental"); writer.beginObject(); @@ -590,6 +591,16 @@ private RequestBody makeErrorTrackingRequestBody(@Nonnull CrashLog payload, bool } writer.endObject(); } + if (uploaderSettings.isExtendedInfoEnabled() + && payload.experimental.registerToMemoryMapping != null) { + writer.name("register_to_memory_mapping"); + writer.beginObject(); + for (Map.Entry entry : + payload.experimental.registerToMemoryMapping.entrySet()) { + writer.name(entry.getKey()).value(entry.getValue()); + } + writer.endObject(); + } if (uploaderSettings.isExtendedInfoEnabled() && payload.experimental.runtimeArgs != null) { writer.name("runtime_args"); diff --git a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/dto/Experimental.java b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/dto/Experimental.java index 914c6640dd..30d04bc3f7 100644 --- a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/dto/Experimental.java +++ b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/dto/Experimental.java @@ -8,15 +8,26 @@ public final class Experimental { public final Map ucontext; + @Json(name = "register_to_memory_mapping") + public final Map registerToMemoryMapping; + @Json(name = "runtime_args") public final List runtimeArgs; public Experimental(Map ucontext) { - this(ucontext, null); + this(ucontext, null, null); } public Experimental(Map ucontext, List runtimeArgs) { + this(ucontext, null, runtimeArgs); + } + + public Experimental( + Map ucontext, + Map registerToMemoryMapping, + List runtimeArgs) { this.ucontext = ucontext; + this.registerToMemoryMapping = registerToMemoryMapping; this.runtimeArgs = runtimeArgs; } @@ -24,11 +35,13 @@ public Experimental(Map ucontext, List runtimeArgs) { public boolean equals(Object o) { if (!(o instanceof Experimental)) return false; Experimental that = (Experimental) o; - return Objects.equals(ucontext, that.ucontext) && Objects.equals(runtimeArgs, that.runtimeArgs); + return Objects.equals(ucontext, that.ucontext) + && Objects.equals(registerToMemoryMapping, that.registerToMemoryMapping) + && Objects.equals(runtimeArgs, that.runtimeArgs); } @Override public int hashCode() { - return Objects.hash(ucontext, runtimeArgs); + return Objects.hash(ucontext, registerToMemoryMapping, runtimeArgs); } } diff --git a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/parsers/HotspotCrashLogParser.java b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/parsers/HotspotCrashLogParser.java index ff89656ab2..f5b7fe722e 100644 --- a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/parsers/HotspotCrashLogParser.java +++ b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/parsers/HotspotCrashLogParser.java @@ -62,6 +62,7 @@ enum State { SUMMARY, THREAD, STACKTRACE, + REGISTER_TO_MEMORY_MAPPING, REGISTERS, PROCESS, VM_ARGUMENTS, @@ -95,12 +96,15 @@ public HotspotCrashLogParser() { // per line. private static final Pattern REGISTER_ENTRY_PARSER = Pattern.compile("([A-Za-z][A-Za-z0-9]*)\\s*=\\s*(0x[0-9a-fA-F]+)"); + private static final Pattern REGISTER_TO_MEMORY_MAPPING_PARSER = + Pattern.compile("^\\s*([A-Za-z][A-Za-z0-9]*)\\s*="); // Used for the REGISTERS-state exit condition only: the register name must start the line // (after optional whitespace). This prevents lines like "Top of Stack: (sp=0x...)" and // "Instructions: (pc=0x...)" from being mistaken for register entries by REGISTER_ENTRY_PARSER's // find(), which would otherwise match the lowercase "sp"/"pc" tokens embedded in those lines. private static final Pattern REGISTER_LINE_START = Pattern.compile("^\\s*[A-Za-z][A-Za-z0-9]*\\s*=\\s*0x"); + private static final Pattern SUBSECTION_TITLE = Pattern.compile("^\\s*[A-Za-z][\\w ]*:.+$"); private static final Pattern COMPILED_JAVA_ADDRESS_PARSER = Pattern.compile("@\\s+(0x[0-9a-fA-F]+)\\s+\\[(0x[0-9a-fA-F]+)\\+(0x[0-9a-fA-F]+)\\]"); @@ -350,10 +354,14 @@ public CrashLog parse(String uuid, String crashLog) { String datetimeRaw = null; boolean incomplete = false; String oomMessage = null; - Map registers = null; + Map registers = new LinkedHashMap<>(); + Map registerToMemoryMapping = new LinkedHashMap<>(); + String currentRegisterToMemoryMapping = ""; List runtimeArgs = null; List dynamicLibraryLines = null; String dynamicLibraryKey = null; + boolean previousLineBlank = false; + State nextThreadSectionState = null; String[] lines = NEWLINE_SPLITTER.split(crashLog); outer: @@ -410,7 +418,10 @@ public CrashLog parse(String uuid, String crashLog) { } break; case STACKTRACE: - if (line.startsWith("siginfo:")) { + nextThreadSectionState = nextThreadSectionState(line, previousLineBlank); + if (nextThreadSectionState != null) { + state = nextThreadSectionState; + } else if (line.startsWith("siginfo:")) { // spotless:off // siginfo: si_signo: 11 (SIGSEGV), si_code: 1 (SEGV_MAPERR), si_addr: 0x70 // siginfo: si_signo: 11 (SIGSEGV), si_code: 0 (SI_USER), si_pid: 554848, si_uid: 1000 @@ -426,11 +437,6 @@ public CrashLog parse(String uuid, String crashLog) { Integer siUid = safelyParseInt(siginfoMatcher.group(7)); sigInfo = new SigInfo(number, name, siCode, sigAction, address, siPid, siUid); } - } else if (line.startsWith("Registers:")) { - registers = new LinkedHashMap<>(); - state = State.REGISTERS; - } else if (line.contains("P R O C E S S")) { - state = State.PROCESS; } else { // Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code) final StackFrame frame = parseLine(line); @@ -439,8 +445,28 @@ public CrashLog parse(String uuid, String crashLog) { } } break; + case REGISTER_TO_MEMORY_MAPPING: + nextThreadSectionState = nextThreadSectionState(line, previousLineBlank); + if (nextThreadSectionState != null) { + currentRegisterToMemoryMapping = ""; + state = nextThreadSectionState; + } else if (!line.isEmpty()) { + final Matcher m = REGISTER_TO_MEMORY_MAPPING_PARSER.matcher(line); + if (m.lookingAt()) { + currentRegisterToMemoryMapping = m.group(1); + registerToMemoryMapping.put( + currentRegisterToMemoryMapping, line.substring(line.indexOf('=') + 1)); + } else if (!currentRegisterToMemoryMapping.isEmpty()) { + registerToMemoryMapping.computeIfPresent( + currentRegisterToMemoryMapping, (key, value) -> value + "\n" + line); + } + } + break; case REGISTERS: - if (!line.isEmpty() && !REGISTER_LINE_START.matcher(line).find()) { + nextThreadSectionState = nextThreadSectionState(line, previousLineBlank); + if (nextThreadSectionState != null) { + state = nextThreadSectionState; + } else if (!line.isEmpty() && !REGISTER_LINE_START.matcher(line).find()) { // non-empty line that does not start with a register entry signals end of section state = State.STACKTRACE; } else { @@ -508,6 +534,7 @@ public CrashLog parse(String uuid, String crashLog) { // unexpected parser state; bail out break outer; } + previousLineBlank = line.isEmpty(); } // PROCESS and SYSTEM sections are late enough that all critical data is captured @@ -571,10 +598,16 @@ public CrashLog parse(String uuid, String crashLog) { Metadata metadata = new Metadata("dd-trace-java", VersionInfo.VERSION, "java", null); Integer parsedPid = safelyParseInt(pid); ProcInfo procInfo = parsedPid != null ? new ProcInfo(parsedPid) : null; + Map resolvedMapping = null; + if (!registerToMemoryMapping.isEmpty()) { + registerToMemoryMapping.replaceAll((k, v) -> RedactUtils.redactRegisterToMemoryMapping(v)); + resolvedMapping = registerToMemoryMapping; + } Experimental experimental = - (registers != null && !registers.isEmpty()) + !registers.isEmpty() + || resolvedMapping != null || (runtimeArgs != null && !runtimeArgs.isEmpty()) - ? new Experimental(registers, runtimeArgs) + ? new Experimental(registers, resolvedMapping, runtimeArgs) : null; DynamicLibs files = (dynamicLibraryLines != null && !dynamicLibraryLines.isEmpty()) @@ -608,6 +641,25 @@ static String dateTimeToISO(String datetime) { } } + private static State nextThreadSectionState(String line, boolean previousLineBlank) { + if (line.startsWith("Register to memory mapping:")) { + return State.REGISTER_TO_MEMORY_MAPPING; + } + if (line.startsWith("Registers:")) { + return State.REGISTERS; + } + if (line.startsWith("siginfo:")) { + return null; + } + if (line.contains("P R O C E S S")) { + return State.PROCESS; + } + if (previousLineBlank && SUBSECTION_TITLE.matcher(line).matches()) { + return State.STACKTRACE; + } + return null; + } + /** * Detects whether the Dynamic libraries section comes from Linux {@code /proc/self/maps} (address * range format {@code addr-addr perms ...}) or from the BSD/macOS dyld callback (format {@code diff --git a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/parsers/RedactUtils.java b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/parsers/RedactUtils.java new file mode 100644 index 0000000000..3e18fd2bef --- /dev/null +++ b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/parsers/RedactUtils.java @@ -0,0 +1,315 @@ +package datadog.crashtracking.parsers; + +import de.thetaphi.forbiddenapis.SuppressForbidden; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Utilities for redacting potentially sensitive data from JVM crash log register-to-memory mapping + * entries. + */ +public final class RedactUtils { + + static final String REDACTED = "redacted"; + static final String REDACTED_CLASS = "Redacted"; + private static final String REDACTED_STRING = "REDACTED"; + + private static final String[] KNOWN_PACKAGES_PREFIXES = { + // Java SE / JDK internals + "java/", + "jdk/", + "sun/", + "javax/", + // Jakarta EE (successor to javax) + "jakarta/", + // Oracle/Sun vendor packages + "com/sun/", + "com/oracle/", + // Datadog top-level and internal shorthand + "datadog/", + "com/dd/", + }; + + // " - string: "value"" in String oop dumps + private static final Pattern STRING_CONTENT = Pattern.compile("(\\s*- string: )\"[^\"]*\""); + + // Type descriptors like Lcom/company/Type; + private static final Pattern TYPE_DESCRIPTOR = Pattern.compile("L([A-Za-z$_][A-Za-z0-9$_/]*);"); + + // klass references: - klass: 'com/company/Class' + private static final Pattern KLASS_REF = Pattern.compile("(klass: ')([^']+)'"); + + // 'in 'class'' clause in {method} descriptor entries + private static final Pattern METHOD_IN_CLASS = Pattern.compile("( in ')([^']+)'"); + + // Object-reference field values in oop dumps: a 'com/company/Class'{0x...} + private static final Pattern OBJ_FIELD_REF = Pattern.compile("(a ')([A-Za-z$_][A-Za-z0-9$_/]*)'"); + + // Class name in nmethod compiled-method output (JDK 11+): + // "Compiled method (c2) ... com.company.Foo::methodName (N bytes)" (PRODUCT — dots) + // "Compiled method (c2) ... com/company/Foo::methodName (N bytes)" (debug — slashes) + private static final Pattern NMETHOD_CLASS = + Pattern.compile("([A-Za-z][A-Za-z0-9$]*(?:[./][A-Za-z][A-Za-z0-9$]*)+)::"); + + // Library path in two formats produced by os::print_location(): + // in /path/to/lib.so at 0x... (no dladdr symbol) + // symbol+offset in /path/to/lib.so at 0x... (dladdr resolved a symbol name) + private static final Pattern LIBRARY_PATH = + Pattern.compile("((?:<[^>]+>|\\S+\\+\\S+)\\s+in\\s+)(/\\S+)"); + + // Dotted class name followed by an OOP reference: "com.company.Type"{0x...} + // This specifically identifies the inline string value of a java.lang.Class 'name' field + private static final Pattern DOTTED_CLASS_OOP_REF = + Pattern.compile( + "\"([A-Za-z][A-Za-z0-9$]*(?:\\.[A-Za-z][A-Za-z0-9$]*)*)\"(\\{0x[0-9a-fA-F]+\\})"); + + // is an oop: com.company.Class + private static final Pattern IS_AN_OOP = + Pattern.compile("(is an oop: )([A-Za-z][A-Za-z0-9$]*(?:\\.[A-Za-z][A-Za-z0-9$]*)*)"); + + // Hex-dump bytes in "points into unknown readable memory:" lines. + // Two formats produced by os::print_location(): + // "memory: 0x | ff ff ff ff ..." (Linux/macOS amd64 — address + pipe + bytes) + // "memory: ff ff ff ff ..." (Linux aarch64 — bytes only) + // The address (when present) is kept; only the raw bytes are redacted. + private static final Pattern READABLE_MEMORY_HEX_DUMP = + Pattern.compile( + "(points into unknown readable memory: (?:0x[0-9a-fA-F]+ \\| )?)([0-9a-fA-F]{2}(?: [0-9a-fA-F]{2})*)"); + + private RedactUtils() {} + + /** + * Main entry point: redact sensitive data from a register-to-memory mapping value (possibly + * multiline). + */ + @SuppressForbidden // split on single-character uses a fast path without regex + public static String redactRegisterToMemoryMapping(String value) { + if (value == null || value.isEmpty()) return value; + String[] lines = value.split("\n", -1); + // java.lang.Class oop dumps: String fields hold class names, not arbitrary data. + // All other oop types: String fields are application data and must be fully redacted. + boolean isClassOop = isJavaLangClassOop(lines[0]); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < lines.length; i++) { + if (i > 0) sb.append('\n'); + sb.append(redactLine(lines[i], isClassOop)); + } + return sb.toString(); + } + + /** + * Returns true if the first line of a register-to-memory mapping value indicates a {@code + * java.lang.Class} oop (not a subclass or other type). + */ + private static boolean isJavaLangClassOop(String firstLine) { + int idx = firstLine.indexOf("is an oop: java.lang.Class"); + if (idx < 0) return false; + // Ensure the class name ends here — not a prefix of e.g. java.lang.ClassLoader + int end = idx + "is an oop: java.lang.Class".length(); + return end >= firstLine.length() || firstLine.charAt(end) == ' '; + } + + private static String redactLine(String line, boolean isClassOop) { + line = redactStringTypeValue(line); + line = redactTypeDescriptors(line); + line = redactKlassReference(line); + line = redactMethodClass(line); + line = redactObjFieldRef(line); + line = redactNmethodClass(line); + line = redactLibraryPath(line); + line = redactStringOopRef(line, isClassOop); + line = redactOopClassName(line); + line = redactReadableMemoryHexDump(line); + return line; + } + + /** + * Redacts {@code "value"\{0x...\}} OOP references in oop dump field lines. When {@code + * isClassOop} is true (inside a {@code java.lang.Class} oop dump) the value is treated as a class + * name and redacted to {@code "redacted.Redacted"} unless it belongs to a known package. + * Otherwise — any other oop type — the value is always fully redacted to {@code "REDACTED"} since + * it may be arbitrary application data. + */ + private static String redactStringOopRef(String line, boolean isClassOop) { + return replaceAll( + DOTTED_CLASS_OOP_REF, + line, + m -> + isClassOop + ? "\"" + redactDottedClassName(m.group(1)) + "\"" + m.group(2) + : "\"" + REDACTED_STRING + "\"" + m.group(2)); + } + + /** + * Redacts string content in String oop dump lines: - string: "Some string" to + * - string: "REDACTED" + */ + static String redactStringTypeValue(String line) { + return STRING_CONTENT.matcher(line).replaceAll("$1\"" + REDACTED_STRING + "\""); + } + + /** + * Redacts the package of type descriptors in a line: Lcom/company/Type; to + * Lredacted/Redacted; + */ + static String redactTypeDescriptors(String line) { + return replaceAll(TYPE_DESCRIPTOR, line, m -> "L" + redactJvmClassName(m.group(1)) + ";"); + } + + /** + * Redacts klass references in a line: klass: 'com/company/Class' to + * klass: 'redacted/Redacted' + */ + static String redactKlassReference(String line) { + return replaceAll(KLASS_REF, line, m -> m.group(1) + redactJvmClassName(m.group(2)) + "'"); + } + + /** + * Redacts the class in a method descriptor's {@code in 'class'} clause: + * in 'com/company/Class' to in 'redacted/Redacted' + */ + static String redactMethodClass(String line) { + return replaceAll( + METHOD_IN_CLASS, line, m -> m.group(1) + redactJvmClassName(m.group(2)) + "'"); + } + + /** + * Redacts all but the parent directory and filename from a library path. Handles both + * <offset 0x...> in /path/to/dir/lib.so and symbol+0 in + * /path/to/dir/lib.so to ... in /redacted/dir/lib.so + */ + static String redactLibraryPath(String line) { + return replaceAll(LIBRARY_PATH, line, m -> m.group(1) + redactPath(m.group(2))); + } + + /** + * Redacts the class name in oop dump object-reference field values: a + * 'com/company/Class' to a 'redacted/Redacted'. + */ + static String redactObjFieldRef(String line) { + return replaceAll(OBJ_FIELD_REF, line, m -> m.group(1) + redactJvmClassName(m.group(2)) + "'"); + } + + /** + * Redacts the class name in nmethod {@code Compiled method} output (JDK 11+): + * com.company.Foo::methodName to redacted.Redacted::methodName. Handles both + * dot-separated (PRODUCT build) and slash-separated (debug build) class names. + */ + static String redactNmethodClass(String line) { + return replaceAll( + NMETHOD_CLASS, + line, + m -> { + String cls = m.group(1); + String redacted = + cls.indexOf('/') >= 0 ? redactJvmClassName(cls) : redactDottedClassName(cls); + return redacted + "::"; + }); + } + + /** + * Redacts any {@code "value"\{0x...\}} OOP reference to {@code "REDACTED"\{0x...\}}. This is the + * safe default for lines that are not part of a {@code java.lang.Class} oop dump, where the + * String value may be arbitrary application data. For class-name-aware redaction (inside a {@code + * java.lang.Class} oop) use {@link #redactRegisterToMemoryMapping} which detects the oop type + * automatically. + */ + static String redactDottedClassOopRef(String line) { + return redactStringOopRef(line, false); + } + + /** + * Redacts the class name in {@code is an oop: ClassName}: is an oop: com.company.Class + * to is an oop: redacted.Redacted + */ + static String redactOopClassName(String line) { + return replaceAll(IS_AN_OOP, line, m -> m.group(1) + redactDottedClassName(m.group(2))); + } + + /** + * Redacts hex-dump bytes in points into unknown readable memory: lines, keeping the + * optional leading address. Handles two formats: + * + *
    + *
  • memory: 0x<addr> | ff ff ff ff to memory: 0x<addr> | + * REDACTED + *
  • memory: ff ff ff ff to memory: REDACTED + *
+ */ + static String redactReadableMemoryHexDump(String line) { + return replaceAll(READABLE_MEMORY_HEX_DUMP, line, m -> m.group(1) + REDACTED_STRING); + } + + /** + * Redacts a slash-separated JVM class name, unless it belongs to a known package. Unknown classes + * are fully redacted: com/company/SomeType to redacted/Redacted; + * java/lang/String unchanged. + */ + static String redactJvmClassName(String className) { + if (isKnownJvmPackage(className)) { + return className; + } + return redactClassName('/', className); + } + + /** + * Redacts a dot-separated class name, unless it belongs to a known package. Unknown classes are + * fully redacted: com.company.SomeType to redacted.Redacted; + * java.lang.String unchanged. + */ + static String redactDottedClassName(String className) { + if (isKnownJvmPackage(className.replace('.', '/'))) { + return className; + } + return redactClassName('.', className); + } + + private static String redactClassName(char sep, String className) { + int lastSep = className.lastIndexOf(sep); + if (lastSep < 0) return className; // no package — nothing to redact + return REDACTED + sep + REDACTED_CLASS; + } + + /** + * Redacts all but the parent directory and filename from a library path, collapsing all + * intermediate segments to a single {@code redacted}. /path/to/dir/lib.so to + * /redacted/dir/lib.so + */ + static String redactPath(String path) { + int last = path.lastIndexOf('/'); + if (last <= 0) return path; // /file or empty — nothing to redact + int secondLast = path.lastIndexOf('/', last - 1); + if (secondLast <= 0) return path; // /dir/file — nothing to redact + // Collapse everything before the second-last slash to a single /redacted + return "/" + REDACTED + path.substring(secondLast); + } + + private static boolean isKnownJvmPackage(String slashClassName) { + for (String prefix : KNOWN_PACKAGES_PREFIXES) { + if (slashClassName.startsWith(prefix)) { + return true; + } + } + // Match *.datadog* — packages whose second segment starts with "datadog" + // e.g. com/datadog/..., org/datadog/..., com/datadoghq/... + int slash = slashClassName.indexOf('/'); + return slash > 0 && slashClassName.startsWith("datadog", slash + 1); + } + + private static String replaceAll( + Pattern pattern, String input, Function replacement) { + Matcher m = pattern.matcher(input); + if (!m.find()) { + return input; + } + StringBuilder sb = new StringBuilder(); + int lastEnd = 0; + do { + sb.append(input, lastEnd, m.start()); + sb.append(replacement.apply(m)); + lastEnd = m.end(); + } while (m.find()); + return sb.append(input, lastEnd, input.length()).toString(); + } +} diff --git a/dd-java-agent/agent-crashtracking/src/test/java/datadog/crashtracking/CrashUploaderTest.java b/dd-java-agent/agent-crashtracking/src/test/java/datadog/crashtracking/CrashUploaderTest.java index 6fde316126..81a521a6f0 100644 --- a/dd-java-agent/agent-crashtracking/src/test/java/datadog/crashtracking/CrashUploaderTest.java +++ b/dd-java-agent/agent-crashtracking/src/test/java/datadog/crashtracking/CrashUploaderTest.java @@ -4,6 +4,7 @@ import static datadog.crashtracking.CrashUploader.HEADER_DD_TELEMETRY_API_VERSION; import static datadog.crashtracking.CrashUploader.TELEMETRY_API_VERSION; import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -416,6 +417,54 @@ public void testErrorTrackingSerializesRuntimeArgs() throws Exception { assertTrue(found); } + @Test + public void testErrorTrackingSerializesRegisterToMemoryMapping() throws Exception { + ConfigManager.StoredConfig crashConfig = + new ConfigManager.StoredConfig.Builder(config) + .reportUUID(SAMPLE_UUID) + .processTags("a:b") + .runtimeId("1234") + .tags(ConfigManager.getMergedTagsForSerialization(Config.get())) + .extendedInfoEnabled(true) + .build(); + + uploader = new CrashUploader(config, crashConfig); + server.enqueue(new MockResponse().setResponseCode(200)); + uploader.remoteUpload(readFileAsString("sample-crash.txt"), false, true); + + final RecordedRequest recordedRequest = server.takeRequest(5, TimeUnit.SECONDS); + final ObjectMapper mapper = new ObjectMapper(); + final JsonNode event = mapper.readTree(recordedRequest.getBody().readUtf8()); + + final JsonNode mapping = event.at("/experimental/register_to_memory_mapping"); + assertThat(mapping.isObject()).isTrue(); + assertThat(mapping.get("RSP").asText()) + .isEqualTo("0x00007f35e6253190 is pointing into the stack for thread: 0x00007f36cd96cc80"); + assertThat(mapping.get("RDI").asText()).isEqualTo("0x0 is NULL"); + } + + @Test + public void testErrorTrackingOmitsRegisterToMemoryMappingByDefault() throws Exception { + // registerMappingEnabled defaults to false — the mapping must not appear in the payload + ConfigManager.StoredConfig crashConfig = + new ConfigManager.StoredConfig.Builder(config) + .reportUUID(SAMPLE_UUID) + .processTags("a:b") + .runtimeId("1234") + .tags(ConfigManager.getMergedTagsForSerialization(Config.get())) + .build(); + + uploader = new CrashUploader(config, crashConfig); + server.enqueue(new MockResponse().setResponseCode(200)); + uploader.remoteUpload(readFileAsString("sample-crash.txt"), false, true); + + final RecordedRequest recordedRequest = server.takeRequest(5, TimeUnit.SECONDS); + final ObjectMapper mapper = new ObjectMapper(); + final JsonNode event = mapper.readTree(recordedRequest.getBody().readUtf8()); + + assertThat(event.at("/experimental/register_to_memory_mapping").isMissingNode()).isTrue(); + } + private void assertCommonHeader(JsonNode event) { assertEquals(TELEMETRY_API_VERSION, event.get("api_version").asText()); assertEquals("logs", event.get("request_type").asText()); diff --git a/dd-java-agent/agent-crashtracking/src/test/java/datadog/crashtracking/parsers/HotspotCrashLogParserTest.java b/dd-java-agent/agent-crashtracking/src/test/java/datadog/crashtracking/parsers/HotspotCrashLogParserTest.java index cbc020d84f..f978513f86 100644 --- a/dd-java-agent/agent-crashtracking/src/test/java/datadog/crashtracking/parsers/HotspotCrashLogParserTest.java +++ b/dd-java-agent/agent-crashtracking/src/test/java/datadog/crashtracking/parsers/HotspotCrashLogParserTest.java @@ -1,5 +1,7 @@ package datadog.crashtracking.parsers; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.InstanceOfAssertFactories.STRING; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -11,6 +13,7 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; +import java.util.Map; import java.util.Objects; import java.util.UUID; import java.util.stream.Collectors; @@ -66,6 +69,12 @@ public void testRegisterParsingMacosAarch64() throws Exception { assertEquals("0x000000010f8ac794", crashLog.experimental.ucontext.get("pc")); assertEquals("0x0000000060001000", crashLog.experimental.ucontext.get("cpsr")); + // "Top of Stack: (sp=0x...)" contains "=" — verify the parser stops at it and doesn't + // absorb its hex-dump content into the last register mapping entry (sp). + assertThat(crashLog.experimental.registerToMemoryMapping) + .extractingByKey("sp", STRING) + .doesNotContain("Top of Stack:"); + assertNotNull(crashLog.experimental.runtimeArgs); assertTrue(crashLog.experimental.runtimeArgs.contains("--enable-native-access=ALL-UNNAMED")); assertTrue(crashLog.experimental.runtimeArgs.contains("--add-modules=ALL-DEFAULT")); @@ -74,6 +83,53 @@ public void testRegisterParsingMacosAarch64() throws Exception { .anyMatch(arg -> arg.contains("SourceLauncher") || arg.endsWith("CrashTest.java"))); } + /** + * Verifies the register-to-memory mapping section for the macOS aarch64 sample: representative + * values, library path redaction, and that "Top of Stack:" / "Instructions:" subsections are not + * absorbed into register values. + */ + @Test + public void testRegisterToMemoryMappingMacosAarch64() throws Exception { + CrashLog crashLog = + new HotspotCrashLogParser() + .parse( + UUID.randomUUID().toString(), readFileAsString("sample-crash-macos-aarch64.txt")); + + Map mapping = crashLog.experimental.registerToMemoryMapping; + + // Representative single-line entries + assertThat(mapping) + .containsEntry("x0", "0x0000000000000c55 is an unknown value") + .containsEntry("x2", "0x0 is null") + .containsEntry("x28", "0x0000000100a153f0 is a thread"); + + // Library path (symbol+offset format) — intermediate segments collapsed to a single /redacted + assertThat(mapping) + .extractingByKey("x16", STRING) + .isEqualTo( + "0x0000000182d709d0: pthread_jit_write_protect_np+0 in /redacted/system/libsystem_pthread.dylib at 0x0000000182d69000"); + assertThat(mapping) + .extractingByKey("x21", STRING) + .isEqualTo( + "0x0000000106c1ccc0: _ZN19TemplateInterpreter13_active_tableE+0 in /redacted/server/libjvm.dylib at 0x0000000105efc000"); + + // macOS aarch64 uses address+pipe format — address kept, bytes redacted + assertThat(mapping) + .extractingByKey("x17", STRING) + .isEqualTo( + "0x0000000100a17cb0 points into unknown readable memory: 0x00000000ffffffff | REDACTED"); + + // "Top of Stack: (sp=0x...)" and "Instructions: (pc=0x...)" must not leak into register values + assertThat(mapping).doesNotContainKey("Top of Stack"); + assertThat(mapping) + .allSatisfy((k, v) -> assertThat(v).doesNotContain("Top of Stack:", "Instructions:")); + + // sp is the last register before "Top of Stack:" — its value must be clean + assertThat(mapping) + .extractingByKey("sp", STRING) + .isEqualTo("0x000000016feee0f0 is pointing into the stack for thread: 0x0000000100a153f0"); + } + /** Linux aarch64 uses uppercase register names: R0-R30 */ @Test public void testRegisterParsingLinuxAarch64() throws Exception { @@ -87,11 +143,54 @@ public void testRegisterParsingLinuxAarch64() throws Exception { assertEquals("0x0000000000000000", crashLog.experimental.ucontext.get("R0")); assertEquals("0x0000000000000001", crashLog.experimental.ucontext.get("R1")); assertEquals("0x0000ffff9efa168c", crashLog.experimental.ucontext.get("R30")); - // "Register to memory mapping:" section must NOT be included assertEquals(31, crashLog.experimental.ucontext.size(), "R0-R30 = 31 registers"); + } - assertNotNull(crashLog.experimental.runtimeArgs); - assertTrue(crashLog.experimental.runtimeArgs.contains("--add-modules=ALL-DEFAULT")); + @Test + public void testRegisterToMemoryMapping() throws Exception { + CrashLog crashLog = + new HotspotCrashLogParser() + .parse(UUID.randomUUID().toString(), readFileAsString("sample-crash.txt")); + + assertThat(crashLog.experimental).isNotNull(); + assertThat(crashLog.experimental.registerToMemoryMapping) + .isNotNull() + .containsEntry( + "RAX", + "0x00007f36ccfbf170 points into unknown readable memory: 0x00007f3600000758 | REDACTED") + .containsEntry( + "RSP", "0x00007f35e6253190 is pointing into the stack for thread: 0x00007f36cd96cc80") + .containsEntry("RDI", "0x0 is NULL") + .containsEntry( + "R11", + "{method} {0x00007f3744198b70} 'resize' '()[Ljava/util/HashMap$Node;' in 'java/util/HashMap'") + // unknown packages are fully redacted to redacted/Redacted + .containsEntry( + "RSI", + "{method} {0x00007f3639c2ff00} 'saveJob' '(Lredacted/Redacted;ILjava/lang/String;)V' in 'redacted/Redacted'"); + } + + @Test + public void testRegisterToMultilineMemoryMapping() throws Exception { + CrashLog crashLog = + new HotspotCrashLogParser() + .parse( + UUID.randomUUID().toString(), readFileAsString("sample-crash-linux-aarch64.txt")); + + assertThat(crashLog.experimental).isNotNull(); + assertThat(crashLog.experimental.registerToMemoryMapping).isNotNull().containsKey("R10"); + assertThat(crashLog.experimental.registerToMemoryMapping) + .extractingByKey("R10", STRING) + .startsWith("0x00000007ffe85850 is an oop: java.lang.Class ") + .contains("\n{0x00000007ffe85850} - klass: 'java/lang/Class'") + .contains("\n - ---- fields (total size 25 words):") + .contains( + "\n - private transient 'name' 'Ljava/lang/String;' @44 \"jdk.internal.misc.Unsafe\""); + + // Linux aarch64 uses bytes-only format (no address prefix before hex dump) + assertThat(crashLog.experimental.registerToMemoryMapping) + .extractingByKey("R9", STRING) + .isEqualTo("0x0000ffff9f686ca4 points into unknown readable memory: REDACTED"); } @Test diff --git a/dd-java-agent/agent-crashtracking/src/test/java/datadog/crashtracking/parsers/RedactUtilsTest.java b/dd-java-agent/agent-crashtracking/src/test/java/datadog/crashtracking/parsers/RedactUtilsTest.java new file mode 100644 index 0000000000..2bcc61aa16 --- /dev/null +++ b/dd-java-agent/agent-crashtracking/src/test/java/datadog/crashtracking/parsers/RedactUtilsTest.java @@ -0,0 +1,389 @@ +package datadog.crashtracking.parsers; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import org.tabletest.junit.TableTest; + +public class RedactUtilsTest { + + @TableTest({ + "scenario | input | expected ", + "unknown package | com/company/SomeType | redacted/Redacted ", + "three-level package | com/company/pkg/SomeType | redacted/Redacted ", + "java prefix | java/lang/String | java/lang/String ", + "jdk prefix | jdk/internal/misc/Unsafe | jdk/internal/misc/Unsafe ", + "sun prefix | sun/reflect/Reflection | sun/reflect/Reflection ", + "javax prefix | javax/net/ssl/SSLSocket | javax/net/ssl/SSLSocket ", + "jakarta prefix | jakarta/servlet/http/HttpServlet | jakarta/servlet/http/HttpServlet", + "com/sun prefix | com/sun/proxy/ProxyBuilder | com/sun/proxy/ProxyBuilder ", + "com/oracle prefix | com/oracle/jrockit/SomeClass | com/oracle/jrockit/SomeClass ", + "datadog prefix | datadog/trace/api/Tracer | datadog/trace/api/Tracer ", + "com/datadog prefix | com/datadog/agent/SomeClass | com/datadog/agent/SomeClass ", + "com/datadoghq prefix | com/datadoghq/profiler/Profiler | com/datadoghq/profiler/Profiler ", + "org/datadog prefix | org/datadog/jmxfetch/App | org/datadog/jmxfetch/App ", + "com/dd prefix | com/dd/logs/LogService | com/dd/logs/LogService ", + "no package | SomeType | SomeType ", + "inner class | com/company/Outer$Inner | redacted/Redacted " + }) + void testRedactJvmClassName(String input, String expected) { + assertThat(RedactUtils.redactJvmClassName(input)).isEqualTo(expected); + } + + @TableTest({ + "scenario | input | expected ", + "unknown package | com.company.SomeType | redacted.Redacted ", + "three-level package | com.company.pkg.SomeType | redacted.Redacted ", + "java prefix | java.lang.String | java.lang.String ", + "jdk prefix | jdk.internal.misc.Unsafe | jdk.internal.misc.Unsafe ", + "sun prefix | sun.reflect.Reflection | sun.reflect.Reflection ", + "jakarta prefix | jakarta.servlet.http.HttpServlet | jakarta.servlet.http.HttpServlet", + "com.oracle prefix | com.oracle.jrockit.SomeClass | com.oracle.jrockit.SomeClass ", + "datadog prefix | datadog.trace.api.Tracer | datadog.trace.api.Tracer ", + "com.datadog prefix | com.datadog.agent.SomeClass | com.datadog.agent.SomeClass ", + "com.datadoghq prefix | com.datadoghq.profiler.Profiler | com.datadoghq.profiler.Profiler ", + "org.datadog prefix | org.datadog.jmxfetch.App | org.datadog.jmxfetch.App ", + "com.dd prefix | com.dd.logs.LogService | com.dd.logs.LogService ", + "no package | SomeType | SomeType ", + "inner class | com.company.Outer$Inner | redacted.Redacted " + }) + void testRedactDottedClassName(String input, String expected) { + assertThat(RedactUtils.redactDottedClassName(input)).isEqualTo(expected); + } + + @TableTest({ + "scenario | input | expected ", + "four segments | /path/to/dir/lib.so | /redacted/dir/lib.so ", + "two segments | /dir/lib.so | /dir/lib.so ", + "three segments | /one/dir/lib.so | /redacted/dir/lib.so ", + "five segments | /usr/lib/jvm/corretto-21/libjvm.so | /redacted/corretto-21/libjvm.so", + "six segments | /usr/lib/jvm/corretto-21/server/libjvm.so | /redacted/server/libjvm.so " + }) + void testRedactPath(String input, String expected) { + assertThat(RedactUtils.redactPath(input)).isEqualTo(expected); + } + + @Test + void testRedactStringContent_redactsValue() { + assertThat(RedactUtils.redactStringTypeValue(" - string: \"SourceFile\"")) + .isEqualTo(" - string: \"REDACTED\""); + } + + @Test + void testRedactStringContent_redactsSensitiveValue() { + assertThat( + RedactUtils.redactStringTypeValue( + " - string: \"jdbc:postgresql://host/db?password=s3cr3t\"")) + .isEqualTo(" - string: \"REDACTED\""); + } + + @Test + void testRedactStringContent_leavesUnrelatedLinesUnchanged() { + assertThat(RedactUtils.redactStringTypeValue(" - klass: 'java/lang/Class'")) + .isEqualTo(" - klass: 'java/lang/Class'"); + } + + @Test + void testRedactTypeDescriptors_redactsUnknownPackage() { + assertThat(RedactUtils.redactTypeDescriptors("'(Lcom/company/Type;ILjava/lang/String;)V'")) + .isEqualTo("'(Lredacted/Redacted;ILjava/lang/String;)V'"); + } + + @Test + void testRedactTypeDescriptors_keepsKnownPackages() { + assertThat(RedactUtils.redactTypeDescriptors("'(Ljava/util/List;Ljdk/internal/misc/Unsafe;)V'")) + .isEqualTo("'(Ljava/util/List;Ljdk/internal/misc/Unsafe;)V'"); + } + + @Test + void testRedactKlassReference_redactsUnknownPackage() { + assertThat(RedactUtils.redactKlassReference("{0x...} - klass: 'com/company/MyClass'")) + .isEqualTo("{0x...} - klass: 'redacted/Redacted'"); + } + + @Test + void testRedactKlassReference_keepsKnownPackage() { + assertThat(RedactUtils.redactKlassReference("{0x...} - klass: 'java/lang/Class'")) + .isEqualTo("{0x...} - klass: 'java/lang/Class'"); + } + + @Test + void testRedactMethodClass_redactsUnknownPackage() { + assertThat( + RedactUtils.redactMethodClass( + "{method} {0x...} 'doWork' '(I)V' in 'com/company/Worker'")) + .isEqualTo("{method} {0x...} 'doWork' '(I)V' in 'redacted/Redacted'"); + } + + @Test + void testRedactMethodClass_keepsKnownPackage() { + assertThat( + RedactUtils.redactMethodClass( + "{method} {0x...} 'getLong' '(J)J' in 'jdk/internal/misc/Unsafe'")) + .isEqualTo("{method} {0x...} 'getLong' '(J)J' in 'jdk/internal/misc/Unsafe'"); + } + + @Test + void testRedactLibraryPath_offsetFormat() { + assertThat( + RedactUtils.redactLibraryPath( + "0x0000ffff9efa1650: in /opt/company/lib/server/app.so at 0x0000ffff9e1a0000")) + .isEqualTo( + "0x0000ffff9efa1650: in /redacted/server/app.so at 0x0000ffff9e1a0000"); + } + + @Test + void testRedactLibraryPath_symbolOffsetFormat() { + // macOS/Linux: dladdr resolved a C++ mangled symbol — "symbol+offset in /path at 0x..." + assertThat( + RedactUtils.redactLibraryPath( + "0x0000000106c1ccc0: _ZN19TemplateInterpreter13_active_tableE+0 in /Users/USER/.local/share/mise/installs/java/25.0.2/lib/server/libjvm.dylib at 0x0000000105efc000")) + .isEqualTo( + "0x0000000106c1ccc0: _ZN19TemplateInterpreter13_active_tableE+0 in /redacted/server/libjvm.dylib at 0x0000000105efc000"); + } + + @Test + void testRedactLibraryPath_cSymbolFormat() { + // macOS: C symbol "symbol+0 in /usr/lib/system/lib.dylib at 0x..." + assertThat( + RedactUtils.redactLibraryPath( + "0x0000000182d709d0: pthread_jit_write_protect_np+0 in /usr/lib/system/libsystem_pthread.dylib at 0x0000000182d69000")) + .isEqualTo( + "0x0000000182d709d0: pthread_jit_write_protect_np+0 in /redacted/system/libsystem_pthread.dylib at 0x0000000182d69000"); + } + + @Test + void testRedactLibraryPath_doesNotMatchInterpreterCodelet() { + // "code_begin+1776 in an Interpreter codelet" — "an" doesn't start with "/" so no match + assertThat( + RedactUtils.redactLibraryPath( + "0x0000000116d0c970 is at code_begin+1776 in an Interpreter codelet")) + .isEqualTo("0x0000000116d0c970 is at code_begin+1776 in an Interpreter codelet"); + } + + @Test + void testRedactLibraryPath_leavesUnrelatedLinesUnchanged() { + assertThat(RedactUtils.redactLibraryPath("0x00007f37a16e2590 is an unknown value")) + .isEqualTo("0x00007f37a16e2590 is an unknown value"); + } + + @Test + void testRedactDottedClassOopRef_redactsAnyStringOopRef() { + // Without oop-type context, all "value"{0x...} OOP refs are treated as arbitrary application + // data and fully redacted — even if the value looks like a class name + assertThat( + RedactUtils.redactDottedClassOopRef( + " - private transient 'name' 'Ljava/lang/String;' @44 \"com.company.SomeType\"{0x00000007142f7200} (0xe285ee40)")) + .isEqualTo( + " - private transient 'name' 'Ljava/lang/String;' @44 \"REDACTED\"{0x00000007142f7200} (0xe285ee40)"); + // Dotted names with known packages are also fully redacted — any string can be a secret + assertThat( + RedactUtils.redactDottedClassOopRef( + " - private transient 'name' 'Ljava/lang/String;' @44 \"jdk.internal.misc.Unsafe\"{0x00000007142f7200} (0xe285ee40)")) + .isEqualTo( + " - private transient 'name' 'Ljava/lang/String;' @44 \"REDACTED\"{0x00000007142f7200} (0xe285ee40)"); + // Plain single-word strings (no dots) are also redacted + assertThat( + RedactUtils.redactDottedClassOopRef( + " - final 'value' 'Ljava/lang/String;' @40 \"SourceFile\"{0x00000007ffe7a6a0} (0xfffcf4d4)")) + .isEqualTo( + " - final 'value' 'Ljava/lang/String;' @40 \"REDACTED\"{0x00000007ffe7a6a0} (0xfffcf4d4)"); + } + + @Test + void testRedactOopClassName_redactsUnknownPackage() { + assertThat( + RedactUtils.redactOopClassName("0x00000007ffe85850 is an oop: com.company.UserData ")) + .isEqualTo("0x00000007ffe85850 is an oop: redacted.Redacted "); + } + + @Test + void testRedactOopClassName_keepsKnownPackage() { + assertThat(RedactUtils.redactOopClassName("0x00000007ffe85850 is an oop: java.lang.Class ")) + .isEqualTo("0x00000007ffe85850 is an oop: java.lang.Class "); + } + + @Test + void testRedactRegisterToMemoryMapping_methodDescriptor() { + String value = + "{method} {0x00007f3639c2ff00} 'saveJob' '(Lcom/company/Job;ILjava/lang/String;)V' in 'com/company/JobService'"; + assertThat(RedactUtils.redactRegisterToMemoryMapping(value)) + .isEqualTo( + "{method} {0x00007f3639c2ff00} 'saveJob' '(Lredacted/Redacted;ILjava/lang/String;)V' in 'redacted/Redacted'"); + } + + @Test + void testRedactRegisterToMemoryMapping_multilineOopDump() { + // Non-java.lang.Class oop: ALL "value"{0x...} OOP refs are fully redacted to "REDACTED" + // regardless of their shape — any string value may be a secret. + String value = + "0x00000007142f8848 is an oop: com.company.SymbolEntry \n" + + "{0x00000007142f8848} - klass: 'com/company/SymbolEntry'\n" + + " - ---- fields (total size 9 words):\n" + + " - final 'tag' 'Ljava/lang/String;' @12 \"SourceFile\"{0x00000007ffe7a6a0} (0xfffcf4d4)\n" + + " - final 'value' 'Ljava/lang/String;' @16 \"com.company.Config\"{0x00000007aabbccdd} (0x12345678)\n" + + " - final 'hint' 'Ljava/lang/String;' @20 \"java.vendor.url.bug\"{0x00000007aabbccee} (0x12345679)\n" + + " - final 'owner' 'Ljava/lang/String;' @24 null (0x00000000)\n" + + " - string: \"some sensitive value\""; + assertThat(RedactUtils.redactRegisterToMemoryMapping(value)) + .isEqualTo( + "0x00000007142f8848 is an oop: redacted.Redacted \n" + + "{0x00000007142f8848} - klass: 'redacted/Redacted'\n" + + " - ---- fields (total size 9 words):\n" + + " - final 'tag' 'Ljava/lang/String;' @12 \"REDACTED\"{0x00000007ffe7a6a0} (0xfffcf4d4)\n" + + " - final 'value' 'Ljava/lang/String;' @16 \"REDACTED\"{0x00000007aabbccdd} (0x12345678)\n" + + " - final 'hint' 'Ljava/lang/String;' @20 \"REDACTED\"{0x00000007aabbccee} (0x12345679)\n" + + " - final 'owner' 'Ljava/lang/String;' @24 null (0x00000000)\n" + + " - string: \"REDACTED\""); + } + + @Test + void testRedactRegisterToMemoryMapping_javaLangClassOopRedactsUnknownClasses() { + // java.lang.Class oop: String OOP refs in field values are treated as class names. + // Unknown-package classes are redacted to redacted.Redacted; known packages are preserved. + String value = + "0x00000007ffe85850 is an oop: java.lang.Class \n" + + "{0x00000007ffe85850} - klass: 'java/lang/Class'\n" + + " - ---- fields (total size 25 words):\n" + + " - private transient 'name' 'Ljava/lang/String;' @44 \"com.company.Config\"{0x00000007aabbccdd} (0x12345678)\n" + + " - private transient 'name' 'Ljava/lang/String;' @44 \"jdk.internal.misc.Unsafe\"{0x00000007142f7200} (0xe285ee40)"; + assertThat(RedactUtils.redactRegisterToMemoryMapping(value)) + .isEqualTo( + "0x00000007ffe85850 is an oop: java.lang.Class \n" + + "{0x00000007ffe85850} - klass: 'java/lang/Class'\n" + + " - ---- fields (total size 25 words):\n" + + " - private transient 'name' 'Ljava/lang/String;' @44 \"redacted.Redacted\"{0x00000007aabbccdd} (0x12345678)\n" + + " - private transient 'name' 'Ljava/lang/String;' @44 \"jdk.internal.misc.Unsafe\"{0x00000007142f7200} (0xe285ee40)"); + } + + @Test + void testRedactRegisterToMemoryMapping_libraryPath() { + assertThat( + RedactUtils.redactRegisterToMemoryMapping( + "0x0000ffff9efa1650: in /usr/lib/jvm/corretto-21/server/libjvm.so at 0x0000ffff9e1a0000")) + .isEqualTo( + "0x0000ffff9efa1650: in /redacted/server/libjvm.so at 0x0000ffff9e1a0000"); + } + + @Test + void testRedactRegisterToMemoryMapping_safeValuesUnchanged() { + assertThat( + RedactUtils.redactRegisterToMemoryMapping( + "0x00007f35e6253190 is pointing into the stack for thread: 0x00007f36cd96cc80")) + .isEqualTo("0x00007f35e6253190 is pointing into the stack for thread: 0x00007f36cd96cc80"); + assertThat(RedactUtils.redactRegisterToMemoryMapping("0x0 is NULL")).isEqualTo("0x0 is NULL"); + assertThat(RedactUtils.redactRegisterToMemoryMapping("0x000000008fd66048 is an unknown value")) + .isEqualTo("0x000000008fd66048 is an unknown value"); + } + + @Test + void testRedactReadableMemoryHexDump_withAddressAndPipe() { + // Linux/macOS amd64: address + " | " + bytes — keep address, redact bytes + assertThat( + RedactUtils.redactReadableMemoryHexDump( + "0x0000000100a17cb0 points into unknown readable memory: 0x00000000ffffffff | ff ff ff ff 00 00 00 00")) + .isEqualTo( + "0x0000000100a17cb0 points into unknown readable memory: 0x00000000ffffffff | REDACTED"); + } + + @Test + void testRedactReadableMemoryHexDump_withoutAddress() { + // Linux aarch64: bytes only — redact everything after the colon + assertThat( + RedactUtils.redactReadableMemoryHexDump( + "0x0000ffff9f686ca4 points into unknown readable memory: 06 00 00 00")) + .isEqualTo("0x0000ffff9f686ca4 points into unknown readable memory: REDACTED"); + } + + @Test + void testRedactReadableMemoryHexDump_leavesUnrelatedLinesUnchanged() { + assertThat(RedactUtils.redactReadableMemoryHexDump("0x00007f37a16e2590 is an unknown value")) + .isEqualTo("0x00007f37a16e2590 is an unknown value"); + } + + @Test + void testRedactObjFieldRef_redactsUnknownPackage() { + assertThat( + RedactUtils.redactObjFieldRef( + " - 'owner' 'Lcom/company/Owner;' @12 a 'com/company/Owner'{0x00007f3700001234} (0x12345678)")) + .isEqualTo( + " - 'owner' 'Lcom/company/Owner;' @12 a 'redacted/Redacted'{0x00007f3700001234} (0x12345678)"); + } + + @Test + void testRedactObjFieldRef_keepsKnownPackage() { + assertThat( + RedactUtils.redactObjFieldRef( + " - 'loader' 'Ljava/lang/ClassLoader;' @12 a 'java/lang/ClassLoader'{0x00007f3700001234} (0x12345678)")) + .isEqualTo( + " - 'loader' 'Ljava/lang/ClassLoader;' @12 a 'java/lang/ClassLoader'{0x00007f3700001234} (0x12345678)"); + } + + @Test + void testRedactObjFieldRef_leavesUnrelatedLinesUnchanged() { + assertThat(RedactUtils.redactObjFieldRef("0x00007f37a16e2590 is an unknown value")) + .isEqualTo("0x00007f37a16e2590 is an unknown value"); + } + + @Test + void testRedactNmethodClass_dottedUnknownPackage() { + assertThat( + RedactUtils.redactNmethodClass( + "Compiled method (c2) 3068 4 com.company.Foo::methodName (456 bytes)")) + .isEqualTo("Compiled method (c2) 3068 4 redacted.Redacted::methodName (456 bytes)"); + } + + @Test + void testRedactNmethodClass_dottedKnownPackage() { + assertThat( + RedactUtils.redactNmethodClass( + "Compiled method (c2) 3068 4 java.util.HashMap::resize (456 bytes)")) + .isEqualTo("Compiled method (c2) 3068 4 java.util.HashMap::resize (456 bytes)"); + } + + @Test + void testRedactNmethodClass_slashUnknownPackage() { + assertThat(RedactUtils.redactNmethodClass("com/company/Foo::methodName")) + .isEqualTo("redacted/Redacted::methodName"); + } + + @Test + void testRedactNmethodClass_slashKnownPackage() { + assertThat(RedactUtils.redactNmethodClass("java/util/HashMap::resize")) + .isEqualTo("java/util/HashMap::resize"); + } + + @Test + void testRedactNmethodClass_leavesUnrelatedLinesUnchanged() { + assertThat(RedactUtils.redactNmethodClass("0x00007f37a16e2590 is an unknown value")) + .isEqualTo("0x00007f37a16e2590 is an unknown value"); + } + + @Test + void testRedactRegisterToMemoryMapping_objFieldRef() { + // Non-java.lang.Class oop with object-reference field: a 'ClassName' is redacted + String value = + "0x00000007142f8848 is an oop: com.company.Holder \n" + + "{0x00000007142f8848} - klass: 'com/company/Holder'\n" + + " - ---- fields (total size 3 words):\n" + + " - 'ref' 'Ljava/lang/Object;' @12 a 'com/company/Inner'{0x00007f1200003456} (0xabcdef01)"; + assertThat(RedactUtils.redactRegisterToMemoryMapping(value)) + .isEqualTo( + "0x00000007142f8848 is an oop: redacted.Redacted \n" + + "{0x00000007142f8848} - klass: 'redacted/Redacted'\n" + + " - ---- fields (total size 3 words):\n" + + " - 'ref' 'Ljava/lang/Object;' @12 a 'redacted/Redacted'{0x00007f1200003456} (0xabcdef01)"); + } + + @Test + void testRedactRegisterToMemoryMapping_nmethodCompiledMethod() { + // nmethod entry (JDK 11+): "Compiled method" line class name is redacted + String value = + "0x00007f36cd2b1600 is at entry_point+13512 in (nmethod*) 0x00007f36cd2b1510\n" + + "Compiled method (c2) 3068 4 com.company.Foo::processRequest (456 bytes)"; + assertThat(RedactUtils.redactRegisterToMemoryMapping(value)) + .isEqualTo( + "0x00007f36cd2b1600 is at entry_point+13512 in (nmethod*) 0x00007f36cd2b1510\n" + + "Compiled method (c2) 3068 4 redacted.Redacted::processRequest (456 bytes)"); + } +} diff --git a/dd-trace-api/src/main/java/datadog/trace/api/config/CrashTrackingConfig.java b/dd-trace-api/src/main/java/datadog/trace/api/config/CrashTrackingConfig.java index c31c26039d..7e2b3fe6ee 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/config/CrashTrackingConfig.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/config/CrashTrackingConfig.java @@ -38,6 +38,4 @@ public final class CrashTrackingConfig { public static final String CRASH_TRACKING_ENABLE_AUTOCONFIG = "crashtracking.debug.autoconfig.enable"; public static final boolean CRASH_TRACKING_ENABLE_AUTOCONFIG_DEFAULT = false; - - private CrashTrackingConfig() {} }