From 8f173335dcac277a70657abf7ce7bd9258d4fda3 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Wed, 1 Apr 2026 14:54:21 +0200 Subject: [PATCH 1/9] feat(crashtracking): Report registry to memory mapping --- .../datadog/crashtracking/CrashUploader.java | 10 +++ .../crashtracking/dto/Experimental.java | 19 ++++- .../parsers/HotspotCrashLogParser.java | 70 ++++++++++++++++--- .../crashtracking/CrashUploaderTest.java | 28 ++++++++ .../parsers/HotspotCrashLogParserTest.java | 44 +++++++++++- 5 files changed, 155 insertions(+), 16 deletions(-) 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 3695592869..cc6727db2c 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 @@ -577,6 +577,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(); @@ -588,6 +589,15 @@ private RequestBody makeErrorTrackingRequestBody(@Nonnull CrashLog payload, bool } writer.endObject(); } + if (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 (payload.experimental.runtimeArgs != null) { writer.name("runtime_args"); writer.beginArray(); 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..ad1a220bc6 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,16 @@ 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 +355,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 +419,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 +438,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 +446,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 +535,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 @@ -572,9 +600,10 @@ public CrashLog parse(String uuid, String crashLog) { Integer parsedPid = safelyParseInt(pid); ProcInfo procInfo = parsedPid != null ? new ProcInfo(parsedPid) : null; Experimental experimental = - (registers != null && !registers.isEmpty()) + !registers.isEmpty() + || !registerToMemoryMapping.isEmpty() || (runtimeArgs != null && !runtimeArgs.isEmpty()) - ? new Experimental(registers, runtimeArgs) + ? new Experimental(registers, registerToMemoryMapping, runtimeArgs) : null; DynamicLibs files = (dynamicLibraryLines != null && !dynamicLibraryLines.isEmpty()) @@ -608,6 +637,27 @@ 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 + && !line.contains("=") + && 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/test/java/datadog/crashtracking/CrashUploaderTest.java b/dd-java-agent/agent-crashtracking/src/test/java/datadog/crashtracking/CrashUploaderTest.java index 763cbd01a5..2c73ef0a4b 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; @@ -392,6 +393,33 @@ 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())) + .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"); + } + 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 b23937a41d..7b37287da6 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; @@ -87,11 +89,47 @@ 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 | 58 07 00 00 36 7f 00 00") + .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'"); + } + + @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\""); } @Test From b8ae4df0bdc0d8eb5660f9b76946fac7deace4bd Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Wed, 1 Apr 2026 16:54:55 +0200 Subject: [PATCH 2/9] feat(crashtracking): Redact register mapping --- .../parsers/HotspotCrashLogParser.java | 8 +- .../crashtracking/parsers/RedactUtils.java | 228 ++++++++++++++++++ .../crashtracking/CrashUploaderTest.java | 6 +- .../parsers/HotspotCrashLogParserTest.java | 10 +- .../parsers/RedactUtilsTest.java | 221 +++++++++++++++++ 5 files changed, 460 insertions(+), 13 deletions(-) create mode 100644 dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/parsers/RedactUtils.java create mode 100644 dd-java-agent/agent-crashtracking/src/test/java/datadog/crashtracking/parsers/RedactUtilsTest.java 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 ad1a220bc6..6bf8e14086 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 @@ -104,8 +104,7 @@ public HotspotCrashLogParser() { // 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 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]+)\\]"); @@ -599,6 +598,7 @@ 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; + registerToMemoryMapping.replaceAll((k, v) -> RedactUtils.redactRegisterToMemoryMapping(v)); Experimental experimental = !registers.isEmpty() || !registerToMemoryMapping.isEmpty() @@ -650,9 +650,7 @@ private static State nextThreadSectionState(String line, boolean previousLineBla if (line.contains("P R O C E S S")) { return State.PROCESS; } - if (previousLineBlank - && !line.contains("=") - && SUBSECTION_TITLE.matcher(line).matches()) { + if (previousLineBlank && !line.contains("=") && SUBSECTION_TITLE.matcher(line).matches()) { return State.STACKTRACE; } return null; 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..2ccf80044b --- /dev/null +++ b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/parsers/RedactUtils.java @@ -0,0 +1,228 @@ +package datadog.crashtracking.parsers; + +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"; + 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/", + }; + + // " - 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/interface references: - klass: 'com/company/Class' + private static final Pattern KLASS_REF = Pattern.compile("((?:klass|interface): ')([^']+)(')"); + + // 'in 'class'' clause in {method} descriptor entries + private static final Pattern METHOD_IN_CLASS = Pattern.compile("( in ')([^']+)(')"); + + // Library path after in /path/to/lib.so + private static final Pattern LIBRARY_PATH = + Pattern.compile("( in )(/\\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$]*)*)"); + + private RedactUtils() {} + + /** + * Main entry point: redact sensitive data from a register-to-memory mapping value (possibly + * multiline). + */ + public static String redactRegisterToMemoryMapping(String value) { + if (value == null || value.isEmpty()) return value; + String[] lines = value.split("\n", -1); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < lines.length; i++) { + if (i > 0) sb.append('\n'); + sb.append(redactLine(lines[i])); + } + return sb.toString(); + } + + private static String redactLine(String line) { + line = redactStringTypeValue(line); + line = redactTypeDescriptors(line); + line = redactKlassReference(line); + line = redactMethodClass(line); + line = redactLibraryPath(line); + line = redactDottedClassOopRef(line); + line = redactOopClassName(line); + return line; + } + + /** + * 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/Type; + */ + static String redactTypeDescriptors(String line) { + return replaceAll(TYPE_DESCRIPTOR, line, m -> "L" + redactJvmClassName(m.group(1)) + ";"); + } + + /** + * Redacts klass/interface references in a line: klass: 'com/company/Class' to + * klass: 'redacted/redacted/Class' + */ + static String redactKlassReference(String line) { + return replaceAll( + KLASS_REF, line, m -> m.group(1) + redactJvmClassName(m.group(2)) + m.group(3)); + } + + /** + * Redacts the class in a method descriptor's {@code in 'class'} clause: + * in 'com/company/Class' to in 'redacted/redacted/Class' + */ + static String redactMethodClass(String line) { + return replaceAll( + METHOD_IN_CLASS, line, m -> m.group(1) + redactJvmClassName(m.group(2)) + m.group(3)); + } + + /** + * Redacts all but the parent directory and filename from a library path in the line: + * <offset 0x...> in /path/to/dir/lib.so to + * <offset 0x...> in /redacted/redacted/dir/lib.so + */ + static String redactLibraryPath(String line) { + return replaceAll(LIBRARY_PATH, line, m -> m.group(1) + redactPath(m.group(2))); + } + + /** + * Redacts dotted class names that appear as inline field values followed by an OOP reference: + * "com.company.SomeType"{0x...} to "redacted.redacted.SomeType"{0x...} + */ + static String redactDottedClassOopRef(String line) { + return replaceAll( + DOTTED_CLASS_OOP_REF, + line, + m -> "\"" + redactDottedClassName(m.group(1)) + "\"" + m.group(2)); + } + + /** + * Redacts the class name in {@code is an oop: ClassName}: is an oop: com.company.Class + * to is an oop: redacted.redacted.Class + */ + static String redactOopClassName(String line) { + return replaceAll(IS_AN_OOP, line, m -> m.group(1) + redactDottedClassName(m.group(2))); + } + + /** + * Redacts the package of a slash-separated JVM class name, unless it belongs to a known package. + * com/company/SomeType to redacted/redacted/SomeType; + * java/lang/String unchanged. + */ + static String redactJvmClassName(String className) { + if (isKnownJvmPackage(className)) { + return className; + } + return redactClassName('/', className); + } + + /** + * Redacts the package of a dot-separated class name, unless it belongs to a known package. + * com.company.SomeType to redacted.redacted.SomeType; 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; + StringBuilder sb = new StringBuilder(); + int pos = 0; + while (pos <= lastSep) { + int next = className.indexOf(sep, pos); + if (sb.length() > 0) sb.append(sep); + sb.append(REDACTED); + pos = next + 1; + } + return sb.append(sep).append(className, lastSep + 1, className.length()).toString(); + } + + /** + * Redacts all path segments except the parent directory and filename. /path/to/dir/lib.so + * to /redacted/redacted/dir/lib.so + */ + static String redactPath(String path) { + String[] parts = path.split("/", -1); + // parts[0] is always "" (before the leading slash) + if (parts.length <= 3) { + return path; // /dir/file or shorter: nothing to redact + } + StringBuilder sb = new StringBuilder(); + for (int i = 1; i < parts.length - 2; i++) { + sb.append('/').append(REDACTED); + } + return sb.append('/') + .append(parts[parts.length - 2]) + .append('/') + .append(parts[parts.length - 1]) + .toString(); + } + + private static boolean isKnownJvmPackage(String slashClassName) { + for (String prefix : KNOWN_PACKAGES_PREFIXES) { + if (slashClassName.startsWith(prefix)) { + return true; + } + } + return slashClassName.contains("datadog") || slashClassName.startsWith("com/dd/"); + } + + 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 2c73ef0a4b..1e5a8da882 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 @@ -394,8 +394,7 @@ public void testErrorTrackingSerializesRuntimeArgs() throws Exception { } @Test - public void testErrorTrackingSerializesRegisterToMemoryMapping() - throws Exception { + public void testErrorTrackingSerializesRegisterToMemoryMapping() throws Exception { ConfigManager.StoredConfig crashConfig = new ConfigManager.StoredConfig.Builder(config) .reportUUID(SAMPLE_UUID) @@ -415,8 +414,7 @@ public void testErrorTrackingSerializesRegisterToMemoryMapping() 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"); + .isEqualTo("0x00007f35e6253190 is pointing into the stack for thread: 0x00007f36cd96cc80"); assertThat(mapping.get("RDI").asText()).isEqualTo("0x0 is NULL"); } 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 7b37287da6..5213fc648b 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 @@ -109,7 +109,11 @@ public void testRegisterToMemoryMapping() throws Exception { .containsEntry("RDI", "0x0 is NULL") .containsEntry( "R11", - "{method} {0x00007f3744198b70} 'resize' '()[Ljava/util/HashMap$Node;' in 'java/util/HashMap'"); + "{method} {0x00007f3744198b70} 'resize' '()[Ljava/util/HashMap$Node;' in 'java/util/HashMap'") + // unknown packages are redacted; known class names (last segment) are preserved + .containsEntry( + "RSI", + "{method} {0x00007f3639c2ff00} 'saveJob' '(Lredacted/redacted/redacted/redacted/REDACT_THIS;ILjava/lang/String;)V' in 'redacted/redacted/redacted/redacted/REDACT_THIS'"); } @Test @@ -120,9 +124,7 @@ public void testRegisterToMultilineMemoryMapping() throws Exception { UUID.randomUUID().toString(), readFileAsString("sample-crash-linux-aarch64.txt")); assertThat(crashLog.experimental).isNotNull(); - assertThat(crashLog.experimental.registerToMemoryMapping) - .isNotNull() - .containsKey("R10"); + assertThat(crashLog.experimental.registerToMemoryMapping).isNotNull().containsKey("R10"); assertThat(crashLog.experimental.registerToMemoryMapping) .extractingByKey("R10", STRING) .startsWith("0x00000007ffe85850 is an oop: java.lang.Class ") 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..1bdf600c5b --- /dev/null +++ b/dd-java-agent/agent-crashtracking/src/test/java/datadog/crashtracking/parsers/RedactUtilsTest.java @@ -0,0 +1,221 @@ +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/SomeType ", + "three-level package | com/company/pkg/SomeType | redacted/redacted/redacted/SomeType", + "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/Outer$Inner " + }) + void testRedactJvmClassName(String input, String expected) { + assertThat(RedactUtils.redactJvmClassName(input)).isEqualTo(expected); + } + + @TableTest({ + "scenario | input | expected ", + "unknown package | com.company.SomeType | redacted.redacted.SomeType ", + "three-level package | com.company.pkg.SomeType | redacted.redacted.redacted.SomeType", + "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.Outer$Inner " + }) + void testRedactDottedClassName(String input, String expected) { + assertThat(RedactUtils.redactDottedClassName(input)).isEqualTo(expected); + } + + @TableTest({ + "scenario | input | expected ", + "four segments | /path/to/dir/lib.so | /redacted/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/redacted/redacted/corretto-21/libjvm.so ", + "six segments | /usr/lib/jvm/corretto-21/server/libjvm.so | /redacted/redacted/redacted/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/Type;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/MyClass'"); + } + + @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/Worker'"); + } + + @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_redactsIntermediateSegments() { + assertThat( + RedactUtils.redactLibraryPath( + "0x0000ffff9efa1650: in /opt/company/lib/server/app.so at 0x0000ffff9e1a0000")) + .isEqualTo( + "0x0000ffff9efa1650: in /redacted/redacted/redacted/server/app.so at 0x0000ffff9e1a0000"); + } + + @Test + void testRedactLibraryPath_leavesUnrelatedLinesUnchanged() { + assertThat(RedactUtils.redactLibraryPath("0x00007f37a16e2590 is an unknown value")) + .isEqualTo("0x00007f37a16e2590 is an unknown value"); + } + + @Test + void testRedactDottedClassOopRef_redactsUnknownPackage() { + assertThat( + RedactUtils.redactDottedClassOopRef( + " - private transient 'name' 'Ljava/lang/String;' @44 \"com.company.SomeType\"{0x00000007142f7200} (0xe285ee40)")) + .isEqualTo( + " - private transient 'name' 'Ljava/lang/String;' @44 \"redacted.redacted.SomeType\"{0x00000007142f7200} (0xe285ee40)"); + } + + @Test + void testRedactDottedClassOopRef_keepsKnownPackage() { + assertThat( + RedactUtils.redactDottedClassOopRef( + " - private transient 'name' 'Ljava/lang/String;' @44 \"jdk.internal.misc.Unsafe\"{0x00000007142f7200} (0xe285ee40)")) + .isEqualTo( + " - private transient 'name' 'Ljava/lang/String;' @44 \"jdk.internal.misc.Unsafe\"{0x00000007142f7200} (0xe285ee40)"); + } + + @Test + void testRedactOopClassName_redactsUnknownPackage() { + assertThat( + RedactUtils.redactOopClassName("0x00000007ffe85850 is an oop: com.company.UserData ")) + .isEqualTo("0x00000007ffe85850 is an oop: redacted.redacted.UserData "); + } + + @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/Job;ILjava/lang/String;)V' in 'redacted/redacted/JobService'"); + } + + @Test + void testRedactRegisterToMemoryMapping_multilineOopDump() { + // Mirrors the java.lang.Class oop dump format: the 'name' field holds a dotted class name + // as an inline string value followed by an OOP ref, and 'loader' holds a typed object ref. + String value = + "0x00000007ffe85850 is an oop: com.company.Config \n" + + "{0x00000007ffe85850} - klass: 'com/company/Config'\n" + + " - ---- fields (total size 3 words):\n" + + " - private transient 'name' 'Ljava/lang/String;' @12 \"com.company.Config\"{0x00000007aabbccdd} (0x12345678)\n" + + " - private 'owner' 'Lcom/company/User;' @16 null (0x00000000)\n" + + " - string: \"some sensitive value\""; + assertThat(RedactUtils.redactRegisterToMemoryMapping(value)) + .isEqualTo( + "0x00000007ffe85850 is an oop: redacted.redacted.Config \n" + + "{0x00000007ffe85850} - klass: 'redacted/redacted/Config'\n" + + " - ---- fields (total size 3 words):\n" + + " - private transient 'name' 'Ljava/lang/String;' @12 \"redacted.redacted.Config\"{0x00000007aabbccdd} (0x12345678)\n" + + " - private 'owner' 'Lredacted/redacted/User;' @16 null (0x00000000)\n" + + " - string: \"REDACTED\""); + } + + @Test + void testRedactRegisterToMemoryMapping_libraryPath() { + assertThat( + RedactUtils.redactRegisterToMemoryMapping( + "0x0000ffff9efa1650: in /usr/lib/jvm/corretto-21/server/libjvm.so at 0x0000ffff9e1a0000")) + .isEqualTo( + "0x0000ffff9efa1650: in /redacted/redacted/redacted/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"); + } +} From ab51afce1e9f7902503b7b92b336b224da1c303c Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Wed, 1 Apr 2026 17:51:24 +0200 Subject: [PATCH 3/9] fix(crashtracking): Redaction on macOS aarch64 register-to-memory mapping --- .../parsers/HotspotCrashLogParser.java | 2 +- .../crashtracking/parsers/RedactUtils.java | 12 +++-- .../parsers/HotspotCrashLogParserTest.java | 51 +++++++++++++++++++ .../parsers/RedactUtilsTest.java | 31 ++++++++++- 4 files changed, 89 insertions(+), 7 deletions(-) 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 6bf8e14086..601ba5d7a9 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 @@ -650,7 +650,7 @@ private static State nextThreadSectionState(String line, boolean previousLineBla if (line.contains("P R O C E S S")) { return State.PROCESS; } - if (previousLineBlank && !line.contains("=") && SUBSECTION_TITLE.matcher(line).matches()) { + if (previousLineBlank && SUBSECTION_TITLE.matcher(line).matches()) { return State.STACKTRACE; } return null; 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 index 2ccf80044b..705f6e22b6 100644 --- 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 @@ -38,9 +38,11 @@ public final class RedactUtils { // 'in 'class'' clause in {method} descriptor entries private static final Pattern METHOD_IN_CLASS = Pattern.compile("( in ')([^']+)(')"); - // Library path after in /path/to/lib.so + // 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("( in )(/\\S+)"); + 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 @@ -115,9 +117,9 @@ static String redactMethodClass(String line) { } /** - * Redacts all but the parent directory and filename from a library path in the line: - * <offset 0x...> in /path/to/dir/lib.so to - * <offset 0x...> in /redacted/redacted/dir/lib.so + * 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/redacted/dir/lib.so */ static String redactLibraryPath(String line) { return replaceAll(LIBRARY_PATH, line, m -> m.group(1) + redactPath(m.group(2))); 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 5213fc648b..67d0efe86a 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 @@ -13,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; @@ -68,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")); @@ -76,6 +83,50 @@ 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) — path must be redacted, keeping only last 2 segments + // /usr/lib/system/libsystem_pthread.dylib → 2 redacted ("usr","lib") + "system/lib..." + assertThat(mapping) + .extractingByKey("x16", STRING) + .isEqualTo( + "0x0000000182d709d0: pthread_jit_write_protect_np+0 in /redacted/redacted/system/libsystem_pthread.dylib at 0x0000000182d69000"); + // /Users/USER/.local/share/mise/installs/java/25.0.2/lib/server/libjvm.dylib → 9 redacted + "server/libjvm.dylib" + assertThat(mapping) + .extractingByKey("x21", STRING) + .isEqualTo( + "0x0000000106c1ccc0: _ZN19TemplateInterpreter13_active_tableE+0 in /redacted/redacted/redacted/redacted/redacted/redacted/redacted/redacted/redacted/server/libjvm.dylib at 0x0000000105efc000"); + + // "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 { 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 index 1bdf600c5b..a72b1db73b 100644 --- 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 @@ -124,7 +124,7 @@ void testRedactMethodClass_keepsKnownPackage() { } @Test - void testRedactLibraryPath_redactsIntermediateSegments() { + void testRedactLibraryPath_offsetFormat() { assertThat( RedactUtils.redactLibraryPath( "0x0000ffff9efa1650: in /opt/company/lib/server/app.so at 0x0000ffff9e1a0000")) @@ -132,6 +132,35 @@ void testRedactLibraryPath_redactsIntermediateSegments() { "0x0000ffff9efa1650: in /redacted/redacted/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/redacted/redacted/redacted/redacted/redacted/redacted/redacted/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/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")) From c59123036fbdad3826cce40ac7b2be92fd7c4c12 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Wed, 1 Apr 2026 18:25:49 +0200 Subject: [PATCH 4/9] chore(crashtracking): Redact unknown memory --- .../crashtracking/parsers/RedactUtils.java | 24 ++++++++++++++++++ .../parsers/HotspotCrashLogParserTest.java | 13 +++++++++- .../parsers/RedactUtilsTest.java | 25 +++++++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) 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 index 705f6e22b6..95f5151cc1 100644 --- 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 @@ -54,6 +54,15 @@ public final class RedactUtils { 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() {} /** @@ -79,6 +88,7 @@ private static String redactLine(String line) { line = redactLibraryPath(line); line = redactDottedClassOopRef(line); line = redactOopClassName(line); + line = redactReadableMemoryHexDump(line); return line; } @@ -144,6 +154,20 @@ 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 the package of a slash-separated JVM class name, unless it belongs to a known package. * com/company/SomeType to redacted/redacted/SomeType; 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 67d0efe86a..b0c416c9de 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 @@ -115,6 +115,12 @@ public void testRegisterToMemoryMappingMacosAarch64() throws Exception { .isEqualTo( "0x0000000106c1ccc0: _ZN19TemplateInterpreter13_active_tableE+0 in /redacted/redacted/redacted/redacted/redacted/redacted/redacted/redacted/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) @@ -154,7 +160,7 @@ public void testRegisterToMemoryMapping() throws Exception { .isNotNull() .containsEntry( "RAX", - "0x00007f36ccfbf170 points into unknown readable memory: 0x00007f3600000758 | 58 07 00 00 36 7f 00 00") + "0x00007f36ccfbf170 points into unknown readable memory: 0x00007f3600000758 | REDACTED") .containsEntry( "RSP", "0x00007f35e6253190 is pointing into the stack for thread: 0x00007f36cd96cc80") .containsEntry("RDI", "0x0 is NULL") @@ -183,6 +189,11 @@ public void testRegisterToMultilineMemoryMapping() throws Exception { .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 index a72b1db73b..2ae0502a04 100644 --- 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 @@ -247,4 +247,29 @@ void testRedactRegisterToMemoryMapping_safeValuesUnchanged() { 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"); + } } From f3b04477b753cd994e8a6c68b754bca004b45797 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Wed, 1 Apr 2026 18:52:53 +0200 Subject: [PATCH 5/9] chore(crashtracking): More string redaction --- .../crashtracking/parsers/RedactUtils.java | 53 +++++++++++---- .../parsers/HotspotCrashLogParserTest.java | 12 ++-- .../parsers/RedactUtilsTest.java | 66 +++++++++++++------ 3 files changed, 95 insertions(+), 36 deletions(-) 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 index 95f5151cc1..90b7daea7c 100644 --- 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 @@ -72,26 +72,57 @@ private RedactUtils() {} 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])); + sb.append(redactLine(lines[i], isClassOop)); } return sb.toString(); } - private static String redactLine(String line) { + /** + * 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 = redactLibraryPath(line); - line = redactDottedClassOopRef(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 its package is redacted. 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" @@ -127,8 +158,8 @@ static String redactMethodClass(String line) { } /** - * 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 + * 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/redacted/dir/lib.so */ static String redactLibraryPath(String line) { @@ -136,14 +167,14 @@ static String redactLibraryPath(String line) { } /** - * Redacts dotted class names that appear as inline field values followed by an OOP reference: - * "com.company.SomeType"{0x...} to "redacted.redacted.SomeType"{0x...} + * 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 replaceAll( - DOTTED_CLASS_OOP_REF, - line, - m -> "\"" + redactDottedClassName(m.group(1)) + "\"" + m.group(2)); + return redactStringOopRef(line, false); } /** 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 b0c416c9de..5b144e5f99 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 @@ -84,9 +84,9 @@ public void testRegisterParsingMacosAarch64() throws Exception { } /** - * 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. + * 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 { @@ -109,7 +109,8 @@ public void testRegisterToMemoryMappingMacosAarch64() throws Exception { .extractingByKey("x16", STRING) .isEqualTo( "0x0000000182d709d0: pthread_jit_write_protect_np+0 in /redacted/redacted/system/libsystem_pthread.dylib at 0x0000000182d69000"); - // /Users/USER/.local/share/mise/installs/java/25.0.2/lib/server/libjvm.dylib → 9 redacted + "server/libjvm.dylib" + // /Users/USER/.local/share/mise/installs/java/25.0.2/lib/server/libjvm.dylib → 9 redacted + + // "server/libjvm.dylib" assertThat(mapping) .extractingByKey("x21", STRING) .isEqualTo( @@ -124,8 +125,7 @@ public void testRegisterToMemoryMappingMacosAarch64() throws Exception { // "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:")); + .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) 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 index 2ae0502a04..56a00896c1 100644 --- 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 @@ -168,21 +168,26 @@ void testRedactLibraryPath_leavesUnrelatedLinesUnchanged() { } @Test - void testRedactDottedClassOopRef_redactsUnknownPackage() { + 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.redacted.SomeType\"{0x00000007142f7200} (0xe285ee40)"); - } - - @Test - void testRedactDottedClassOopRef_keepsKnownPackage() { + " - 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 \"jdk.internal.misc.Unsafe\"{0x00000007142f7200} (0xe285ee40)"); + " - 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 @@ -209,25 +214,48 @@ void testRedactRegisterToMemoryMapping_methodDescriptor() { @Test void testRedactRegisterToMemoryMapping_multilineOopDump() { - // Mirrors the java.lang.Class oop dump format: the 'name' field holds a dotted class name - // as an inline string value followed by an OOP ref, and 'loader' holds a typed object ref. + // 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 = - "0x00000007ffe85850 is an oop: com.company.Config \n" - + "{0x00000007ffe85850} - klass: 'com/company/Config'\n" - + " - ---- fields (total size 3 words):\n" - + " - private transient 'name' 'Ljava/lang/String;' @12 \"com.company.Config\"{0x00000007aabbccdd} (0x12345678)\n" - + " - private 'owner' 'Lcom/company/User;' @16 null (0x00000000)\n" + "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( - "0x00000007ffe85850 is an oop: redacted.redacted.Config \n" - + "{0x00000007ffe85850} - klass: 'redacted/redacted/Config'\n" - + " - ---- fields (total size 3 words):\n" - + " - private transient 'name' 'Ljava/lang/String;' @12 \"redacted.redacted.Config\"{0x00000007aabbccdd} (0x12345678)\n" - + " - private 'owner' 'Lredacted/redacted/User;' @16 null (0x00000000)\n" + "0x00000007142f8848 is an oop: redacted.redacted.SymbolEntry \n" + + "{0x00000007142f8848} - klass: 'redacted/redacted/SymbolEntry'\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_javaLangClassOopPreservesClassName() { + // java.lang.Class oop: String OOP refs in field values are treated as class names and + // get package redaction (not full redaction), since that is what java.lang.Class stores. + 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.Config\"{0x00000007aabbccdd} (0x12345678)\n" + + " - private transient 'name' 'Ljava/lang/String;' @44 \"jdk.internal.misc.Unsafe\"{0x00000007142f7200} (0xe285ee40)"); + } + @Test void testRedactRegisterToMemoryMapping_libraryPath() { assertThat( From e3ba878e37a94d18ebf6576e23918e2afd0dfdb2 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Wed, 1 Apr 2026 21:20:05 +0200 Subject: [PATCH 6/9] chore(crashtracking): Allow String::split with a single char --- .../main/java/datadog/crashtracking/parsers/RedactUtils.java | 3 +++ 1 file changed, 3 insertions(+) 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 index 90b7daea7c..dc2e7f7455 100644 --- 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 @@ -1,5 +1,6 @@ package datadog.crashtracking.parsers; +import de.thetaphi.forbiddenapis.SuppressForbidden; import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -69,6 +70,7 @@ 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); @@ -241,6 +243,7 @@ private static String redactClassName(char sep, String className) { * Redacts all path segments except the parent directory and filename. /path/to/dir/lib.so * to /redacted/redacted/dir/lib.so */ + @SuppressForbidden // split on single-character uses a fast path without regex static String redactPath(String path) { String[] parts = path.split("/", -1); // parts[0] is always "" (before the leading slash) From 61998ec4a430b34331c2051b5733aa7cf17b919d Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Thu, 2 Apr 2026 10:59:54 +0200 Subject: [PATCH 7/9] chore(crashtracking): Collapse adjacent redacted path or package segments --- .../crashtracking/parsers/RedactUtils.java | 62 +++++----- .../parsers/HotspotCrashLogParserTest.java | 13 +-- .../parsers/RedactUtilsTest.java | 106 +++++++++--------- 3 files changed, 85 insertions(+), 96 deletions(-) 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 index dc2e7f7455..36da2d2d5c 100644 --- 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 @@ -12,6 +12,7 @@ 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 = { @@ -25,6 +26,9 @@ public final class RedactUtils { // Oracle/Sun vendor packages "com/sun/", "com/oracle/", + // Datadog top-level and internal shorthand + "datadog/", + "com/dd/", }; // " - string: "value"" in String oop dumps @@ -112,8 +116,9 @@ private static String redactLine(String line, boolean isClassOop) { /** * 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 its package is redacted. Otherwise — any other oop type — the value is always fully - * redacted to {@code "REDACTED"} since it may be arbitrary application data. + * 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( @@ -202,8 +207,8 @@ static String redactReadableMemoryHexDump(String line) { } /** - * Redacts the package of a slash-separated JVM class name, unless it belongs to a known package. - * com/company/SomeType to redacted/redacted/SomeType; + * 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) { @@ -214,9 +219,9 @@ static String redactJvmClassName(String className) { } /** - * Redacts the package of a dot-separated class name, unless it belongs to a known package. - * com.company.SomeType to redacted.redacted.SomeType; java.lang.String - * unchanged. + * 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('.', '/'))) { @@ -227,38 +232,22 @@ static String redactDottedClassName(String className) { private static String redactClassName(char sep, String className) { int lastSep = className.lastIndexOf(sep); - if (lastSep < 0) return className; - StringBuilder sb = new StringBuilder(); - int pos = 0; - while (pos <= lastSep) { - int next = className.indexOf(sep, pos); - if (sb.length() > 0) sb.append(sep); - sb.append(REDACTED); - pos = next + 1; - } - return sb.append(sep).append(className, lastSep + 1, className.length()).toString(); + if (lastSep < 0) return className; // no package — nothing to redact + return REDACTED + sep + REDACTED_CLASS; } /** - * Redacts all path segments except the parent directory and filename. /path/to/dir/lib.so - * to /redacted/redacted/dir/lib.so + * 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 */ - @SuppressForbidden // split on single-character uses a fast path without regex static String redactPath(String path) { - String[] parts = path.split("/", -1); - // parts[0] is always "" (before the leading slash) - if (parts.length <= 3) { - return path; // /dir/file or shorter: nothing to redact - } - StringBuilder sb = new StringBuilder(); - for (int i = 1; i < parts.length - 2; i++) { - sb.append('/').append(REDACTED); - } - return sb.append('/') - .append(parts[parts.length - 2]) - .append('/') - .append(parts[parts.length - 1]) - .toString(); + 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) { @@ -267,7 +256,10 @@ private static boolean isKnownJvmPackage(String slashClassName) { return true; } } - return slashClassName.contains("datadog") || slashClassName.startsWith("com/dd/"); + // 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( 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 5b144e5f99..be643c8e47 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 @@ -103,18 +103,15 @@ public void testRegisterToMemoryMappingMacosAarch64() throws Exception { .containsEntry("x2", "0x0 is null") .containsEntry("x28", "0x0000000100a153f0 is a thread"); - // Library path (symbol+offset format) — path must be redacted, keeping only last 2 segments - // /usr/lib/system/libsystem_pthread.dylib → 2 redacted ("usr","lib") + "system/lib..." + // 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/redacted/system/libsystem_pthread.dylib at 0x0000000182d69000"); - // /Users/USER/.local/share/mise/installs/java/25.0.2/lib/server/libjvm.dylib → 9 redacted + - // "server/libjvm.dylib" + "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/redacted/redacted/redacted/redacted/redacted/redacted/redacted/redacted/server/libjvm.dylib at 0x0000000105efc000"); + "0x0000000106c1ccc0: _ZN19TemplateInterpreter13_active_tableE+0 in /redacted/server/libjvm.dylib at 0x0000000105efc000"); // macOS aarch64 uses address+pipe format — address kept, bytes redacted assertThat(mapping) @@ -167,10 +164,10 @@ public void testRegisterToMemoryMapping() throws Exception { .containsEntry( "R11", "{method} {0x00007f3744198b70} 'resize' '()[Ljava/util/HashMap$Node;' in 'java/util/HashMap'") - // unknown packages are redacted; known class names (last segment) are preserved + // unknown packages are fully redacted to redacted/Redacted .containsEntry( "RSI", - "{method} {0x00007f3639c2ff00} 'saveJob' '(Lredacted/redacted/redacted/redacted/REDACT_THIS;ILjava/lang/String;)V' in 'redacted/redacted/redacted/redacted/REDACT_THIS'"); + "{method} {0x00007f3639c2ff00} 'saveJob' '(Lredacted/Redacted;ILjava/lang/String;)V' in 'redacted/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 index 56a00896c1..bfdfbace48 100644 --- 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 @@ -8,56 +8,56 @@ public class RedactUtilsTest { @TableTest({ - "scenario | input | expected ", - "unknown package | com/company/SomeType | redacted/redacted/SomeType ", - "three-level package | com/company/pkg/SomeType | redacted/redacted/redacted/SomeType", - "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/Outer$Inner " + "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.SomeType ", - "three-level package | com.company.pkg.SomeType | redacted.redacted.redacted.SomeType", - "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.Outer$Inner " + "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/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/redacted/redacted/corretto-21/libjvm.so ", - "six segments | /usr/lib/jvm/corretto-21/server/libjvm.so | /redacted/redacted/redacted/redacted/server/libjvm.so" + "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); @@ -86,7 +86,7 @@ void testRedactStringContent_leavesUnrelatedLinesUnchanged() { @Test void testRedactTypeDescriptors_redactsUnknownPackage() { assertThat(RedactUtils.redactTypeDescriptors("'(Lcom/company/Type;ILjava/lang/String;)V'")) - .isEqualTo("'(Lredacted/redacted/Type;ILjava/lang/String;)V'"); + .isEqualTo("'(Lredacted/Redacted;ILjava/lang/String;)V'"); } @Test @@ -98,7 +98,7 @@ void testRedactTypeDescriptors_keepsKnownPackages() { @Test void testRedactKlassReference_redactsUnknownPackage() { assertThat(RedactUtils.redactKlassReference("{0x...} - klass: 'com/company/MyClass'")) - .isEqualTo("{0x...} - klass: 'redacted/redacted/MyClass'"); + .isEqualTo("{0x...} - klass: 'redacted/Redacted'"); } @Test @@ -112,7 +112,7 @@ void testRedactMethodClass_redactsUnknownPackage() { assertThat( RedactUtils.redactMethodClass( "{method} {0x...} 'doWork' '(I)V' in 'com/company/Worker'")) - .isEqualTo("{method} {0x...} 'doWork' '(I)V' in 'redacted/redacted/Worker'"); + .isEqualTo("{method} {0x...} 'doWork' '(I)V' in 'redacted/Redacted'"); } @Test @@ -129,7 +129,7 @@ void testRedactLibraryPath_offsetFormat() { RedactUtils.redactLibraryPath( "0x0000ffff9efa1650: in /opt/company/lib/server/app.so at 0x0000ffff9e1a0000")) .isEqualTo( - "0x0000ffff9efa1650: in /redacted/redacted/redacted/server/app.so at 0x0000ffff9e1a0000"); + "0x0000ffff9efa1650: in /redacted/server/app.so at 0x0000ffff9e1a0000"); } @Test @@ -139,7 +139,7 @@ void testRedactLibraryPath_symbolOffsetFormat() { 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/redacted/redacted/redacted/redacted/redacted/redacted/redacted/redacted/server/libjvm.dylib at 0x0000000105efc000"); + "0x0000000106c1ccc0: _ZN19TemplateInterpreter13_active_tableE+0 in /redacted/server/libjvm.dylib at 0x0000000105efc000"); } @Test @@ -149,7 +149,7 @@ void testRedactLibraryPath_cSymbolFormat() { 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/redacted/system/libsystem_pthread.dylib at 0x0000000182d69000"); + "0x0000000182d709d0: pthread_jit_write_protect_np+0 in /redacted/system/libsystem_pthread.dylib at 0x0000000182d69000"); } @Test @@ -194,7 +194,7 @@ void testRedactDottedClassOopRef_redactsAnyStringOopRef() { void testRedactOopClassName_redactsUnknownPackage() { assertThat( RedactUtils.redactOopClassName("0x00000007ffe85850 is an oop: com.company.UserData ")) - .isEqualTo("0x00000007ffe85850 is an oop: redacted.redacted.UserData "); + .isEqualTo("0x00000007ffe85850 is an oop: redacted.Redacted "); } @Test @@ -209,7 +209,7 @@ void testRedactRegisterToMemoryMapping_methodDescriptor() { "{method} {0x00007f3639c2ff00} 'saveJob' '(Lcom/company/Job;ILjava/lang/String;)V' in 'com/company/JobService'"; assertThat(RedactUtils.redactRegisterToMemoryMapping(value)) .isEqualTo( - "{method} {0x00007f3639c2ff00} 'saveJob' '(Lredacted/redacted/Job;ILjava/lang/String;)V' in 'redacted/redacted/JobService'"); + "{method} {0x00007f3639c2ff00} 'saveJob' '(Lredacted/Redacted;ILjava/lang/String;)V' in 'redacted/Redacted'"); } @Test @@ -227,8 +227,8 @@ void testRedactRegisterToMemoryMapping_multilineOopDump() { + " - string: \"some sensitive value\""; assertThat(RedactUtils.redactRegisterToMemoryMapping(value)) .isEqualTo( - "0x00000007142f8848 is an oop: redacted.redacted.SymbolEntry \n" - + "{0x00000007142f8848} - klass: 'redacted/redacted/SymbolEntry'\n" + "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" @@ -238,9 +238,9 @@ void testRedactRegisterToMemoryMapping_multilineOopDump() { } @Test - void testRedactRegisterToMemoryMapping_javaLangClassOopPreservesClassName() { - // java.lang.Class oop: String OOP refs in field values are treated as class names and - // get package redaction (not full redaction), since that is what java.lang.Class stores. + 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" @@ -252,7 +252,7 @@ void testRedactRegisterToMemoryMapping_javaLangClassOopPreservesClassName() { "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.Config\"{0x00000007aabbccdd} (0x12345678)\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)"); } @@ -262,7 +262,7 @@ void testRedactRegisterToMemoryMapping_libraryPath() { RedactUtils.redactRegisterToMemoryMapping( "0x0000ffff9efa1650: in /usr/lib/jvm/corretto-21/server/libjvm.so at 0x0000ffff9e1a0000")) .isEqualTo( - "0x0000ffff9efa1650: in /redacted/redacted/redacted/redacted/server/libjvm.so at 0x0000ffff9e1a0000"); + "0x0000ffff9efa1650: in /redacted/server/libjvm.so at 0x0000ffff9e1a0000"); } @Test From 3ddb175f8c2dca3c00d5fa44be9e2dc87ea1d8de Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Thu, 2 Apr 2026 12:03:58 +0200 Subject: [PATCH 8/9] chore(crashtracking): Redact other things --- .../crashtracking/parsers/RedactUtils.java | 60 ++++++++++--- .../parsers/RedactUtilsTest.java | 87 +++++++++++++++++++ 2 files changed, 136 insertions(+), 11 deletions(-) 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 index 36da2d2d5c..d0111e71b7 100644 --- 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 @@ -37,11 +37,21 @@ public final class RedactUtils { // Type descriptors like Lcom/company/Type; private static final Pattern TYPE_DESCRIPTOR = Pattern.compile("L([A-Za-z$_][A-Za-z0-9$_/]*);"); - // klass/interface references: - klass: 'com/company/Class' - private static final Pattern KLASS_REF = Pattern.compile("((?:klass|interface): ')([^']+)(')"); + // 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 ')([^']+)(')"); + 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) @@ -106,6 +116,8 @@ private static String redactLine(String line, boolean isClassOop) { 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); @@ -140,39 +152,65 @@ static String redactStringTypeValue(String line) { /** * Redacts the package of type descriptors in a line: Lcom/company/Type; to - * Lredacted/redacted/Type; + * Lredacted/Redacted; */ static String redactTypeDescriptors(String line) { return replaceAll(TYPE_DESCRIPTOR, line, m -> "L" + redactJvmClassName(m.group(1)) + ";"); } /** - * Redacts klass/interface references in a line: klass: 'com/company/Class' to - * klass: 'redacted/redacted/Class' + * 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)) + m.group(3)); + 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/Class' + * 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)) + m.group(3)); + 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/redacted/dir/lib.so + * /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 @@ -186,7 +224,7 @@ static String redactDottedClassOopRef(String line) { /** * Redacts the class name in {@code is an oop: ClassName}: is an oop: com.company.Class - * to is an oop: redacted.redacted.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))); 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 index bfdfbace48..6b308d9cfd 100644 --- 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 @@ -300,4 +300,91 @@ 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)"); + } } From c71deb43af19bbfa453ed74532ee028fe194d6b7 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Thu, 2 Apr 2026 15:41:54 +0200 Subject: [PATCH 9/9] feat(crashtracking): Add crashtracking.experimental.register-mapping.enabled --- .../datadog/crashtracking/ConfigManager.java | 26 +++++- .../datadog/crashtracking/CrashLogParser.java | 10 ++- .../datadog/crashtracking/CrashUploader.java | 3 +- .../crashtracking/CrashUploaderSettings.java | 18 +++++ .../parsers/HotspotCrashLogParser.java | 17 +++- .../crashtracking/parsers/RedactUtils.java | 9 +-- .../crashtracking/ConfigManagerTest.java | 3 + .../crashtracking/CrashUploaderTest.java | 23 ++++++ .../parsers/HotspotCrashLogParserTest.java | 26 ++++++ .../parsers/RedactUtilsTest.java | 79 +++++++++---------- .../trace/api/config/CrashTrackingConfig.java | 4 + .../main/java/datadog/trace/api/Config.java | 13 +++ metadata/supported-configurations.json | 8 ++ 13 files changed, 186 insertions(+), 53 deletions(-) create mode 100644 dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/CrashUploaderSettings.java diff --git a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/ConfigManager.java b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/ConfigManager.java index 073acd40ee..9abc47dc21 100644 --- a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/ConfigManager.java +++ b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/ConfigManager.java @@ -35,6 +35,7 @@ public static class StoredConfig { final String reportUUID; final boolean agentless; final boolean sendToErrorTracking; + final boolean registerMappingEnabled; StoredConfig( String reportUUID, @@ -45,7 +46,8 @@ public static class StoredConfig { String processTags, String runtimeId, boolean agentless, - boolean sendToErrorTracking) { + boolean sendToErrorTracking, + boolean registerMappingEnabled) { this.service = service; this.env = env; this.version = version; @@ -55,6 +57,11 @@ public static class StoredConfig { this.reportUUID = reportUUID; this.agentless = agentless; this.sendToErrorTracking = sendToErrorTracking; + this.registerMappingEnabled = registerMappingEnabled; + } + + public CrashUploaderSettings toCrashUploaderSettings() { + return new CrashUploaderSettings(registerMappingEnabled); } public static class Builder { @@ -67,6 +74,7 @@ public static class Builder { String reportUUID; boolean agentless; boolean sendToErrorTracking; + boolean registerMappingEnabled; public Builder(Config config) { // get sane defaults @@ -77,6 +85,7 @@ public Builder(Config config) { this.reportUUID = RandomUtils.randomUUID().toString(); this.agentless = config.isCrashTrackingAgentless(); this.sendToErrorTracking = config.isCrashTrackingErrorsIntakeEnabled(); + this.registerMappingEnabled = config.isCrashTrackingExperimentalRegisterMappingEnabled(); } public Builder service(String service) { @@ -119,6 +128,11 @@ public Builder agentless(boolean agentless) { return this; } + public Builder registerMappingEnabled(boolean registerMappingEnabled) { + this.registerMappingEnabled = registerMappingEnabled; + return this; + } + // @VisibleForTesting Builder reportUUID(String reportUUID) { this.reportUUID = reportUUID; @@ -135,7 +149,8 @@ public StoredConfig build() { processTags, runtimeId, agentless, - sendToErrorTracking); + sendToErrorTracking, + registerMappingEnabled); } } } @@ -194,6 +209,10 @@ static void writeConfigToFile(Config config, Path cfgPath, String... additionalE writeEntry(bw, "java_home", SystemProperties.get("java.home")); writeEntry(bw, "agentless", Boolean.toString(config.isCrashTrackingAgentless())); writeEntry(bw, "upload_to_et", Boolean.toString(config.isCrashTrackingErrorsIntakeEnabled())); + writeEntry( + bw, + "register_mapping", + Boolean.toString(config.isCrashTrackingExperimentalRegisterMappingEnabled())); Runtime.getRuntime() .addShutdownHook( @@ -257,6 +276,9 @@ public static StoredConfig readConfig(Config config, Path scriptPath) { case "upload_to_et": cfgBuilder.sendToErrorTracking(Boolean.parseBoolean(value)); break; + case "register_mapping": + cfgBuilder.registerMappingEnabled(Boolean.parseBoolean(value)); + break; default: // ignore break; 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..679761ac6d 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 @@ -33,10 +33,18 @@ public static CrashLog fromJ9Javacore(String uuid, String javacoreContent) { * */ public static CrashLog parse(String uuid, String content) { + return parse(uuid, content, new CrashUploaderSettings(true)); + } + + /** + * Auto-detect crash log format and parse accordingly, using the provided settings to control + * which sections are included in the result. + */ + public static CrashLog parse(String uuid, String content, CrashUploaderSettings settings) { if (isJ9Javacore(content)) { return fromJ9Javacore(uuid, content); } - return fromHotspotCrashLog(uuid, content); + return new HotspotCrashLogParser(settings).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 cc6727db2c..bb351c13c8 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 @@ -257,7 +257,8 @@ void remoteUpload( final String uuid = storedConfig.reportUUID; try { // Auto-detect crash log format (HotSpot hs_err or J9 javacore) - CrashLog crashLog = CrashLogParser.parse(uuid, fileContent); + CrashLog crashLog = + CrashLogParser.parse(uuid, fileContent, storedConfig.toCrashUploaderSettings()); if (sendToTelemetry) { uploadToTelemetry(crashLog); } diff --git a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/CrashUploaderSettings.java b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/CrashUploaderSettings.java new file mode 100644 index 0000000000..20353691a8 --- /dev/null +++ b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/CrashUploaderSettings.java @@ -0,0 +1,18 @@ +package datadog.crashtracking; + +/** + * Crash-tracking-specific settings controlling which sections are included in parsed crash reports. + */ +public final class CrashUploaderSettings { + + private final boolean registerMappingEnabled; + + public CrashUploaderSettings(boolean registerMappingEnabled) { + this.registerMappingEnabled = registerMappingEnabled; + } + + /** Whether the register-to-memory mapping section should be included in parsed crash reports. */ + public boolean isRegisterMappingEnabled() { + return registerMappingEnabled; + } +} 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 601ba5d7a9..7bd67af461 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 @@ -3,6 +3,7 @@ import static java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME; import datadog.common.version.VersionInfo; +import datadog.crashtracking.CrashUploaderSettings; import datadog.crashtracking.buildid.BuildIdCollector; import datadog.crashtracking.buildid.BuildInfo; import datadog.crashtracking.dto.CrashLog; @@ -72,9 +73,15 @@ enum State { } private State state = State.NEW; + private final CrashUploaderSettings settings; public HotspotCrashLogParser() { + this(new CrashUploaderSettings(true)); + } + + public HotspotCrashLogParser(CrashUploaderSettings settings) { this.buildIdCollector = new BuildIdCollector(); + this.settings = settings; } private static final Pattern PLUS_SPLITTER = Pattern.compile("\\+"); @@ -598,12 +605,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; - registerToMemoryMapping.replaceAll((k, v) -> RedactUtils.redactRegisterToMemoryMapping(v)); + Map resolvedMapping = null; + if (settings.isRegisterMappingEnabled() && !registerToMemoryMapping.isEmpty()) { + registerToMemoryMapping.replaceAll((k, v) -> RedactUtils.redactRegisterToMemoryMapping(v)); + resolvedMapping = registerToMemoryMapping; + } Experimental experimental = !registers.isEmpty() - || !registerToMemoryMapping.isEmpty() + || resolvedMapping != null || (runtimeArgs != null && !runtimeArgs.isEmpty()) - ? new Experimental(registers, registerToMemoryMapping, runtimeArgs) + ? new Experimental(registers, resolvedMapping, runtimeArgs) : null; DynamicLibs files = (dynamicLibraryLines != null && !dynamicLibraryLines.isEmpty()) 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 index d0111e71b7..3e18fd2bef 100644 --- 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 @@ -44,8 +44,7 @@ public final class RedactUtils { 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$_/]*)'"); + 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) @@ -163,8 +162,7 @@ static String redactTypeDescriptors(String line) { * klass: 'redacted/Redacted' */ static String redactKlassReference(String line) { - return replaceAll( - KLASS_REF, line, m -> m.group(1) + redactJvmClassName(m.group(2)) + "'"); + return replaceAll(KLASS_REF, line, m -> m.group(1) + redactJvmClassName(m.group(2)) + "'"); } /** @@ -190,8 +188,7 @@ static String redactLibraryPath(String line) { * '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)) + "'"); + return replaceAll(OBJ_FIELD_REF, line, m -> m.group(1) + redactJvmClassName(m.group(2)) + "'"); } /** diff --git a/dd-java-agent/agent-crashtracking/src/test/java/datadog/crashtracking/ConfigManagerTest.java b/dd-java-agent/agent-crashtracking/src/test/java/datadog/crashtracking/ConfigManagerTest.java index 35d5aae3e2..f7b735e4bd 100644 --- a/dd-java-agent/agent-crashtracking/src/test/java/datadog/crashtracking/ConfigManagerTest.java +++ b/dd-java-agent/agent-crashtracking/src/test/java/datadog/crashtracking/ConfigManagerTest.java @@ -24,6 +24,7 @@ public void testConfigWriteAndRead() throws IOException { .thenReturn(new WellKnownTags("1234", "", "env", "service", "version", "")); when(config.isCrashTrackingAgentless()).thenReturn(false); when(config.isCrashTrackingErrorsIntakeEnabled()).thenReturn(true); + when(config.isCrashTrackingExperimentalRegisterMappingEnabled()).thenReturn(true); when(config.getMergedCrashTrackingTags()).thenReturn(Collections.singletonMap("key", "value")); File tmpFile = File.createTempFile("ConfigManagerTest", null); tmpFile.deleteOnExit(); @@ -41,6 +42,7 @@ public void testConfigWriteAndRead() throws IOException { deserialized.processTags); assertFalse(deserialized.agentless); assertTrue(deserialized.sendToErrorTracking); + assertTrue(deserialized.registerMappingEnabled); } @Test @@ -56,5 +58,6 @@ public void testStoredConfigDefaults() { assertEquals("env", storedConfig.env); assertFalse(storedConfig.agentless); assertFalse(storedConfig.sendToErrorTracking); + assertFalse(storedConfig.registerMappingEnabled); } } 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 1e5a8da882..1eb76c7446 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 @@ -401,6 +401,7 @@ public void testErrorTrackingSerializesRegisterToMemoryMapping() throws Exceptio .processTags("a:b") .runtimeId("1234") .tags(ConfigManager.getMergedTagsForSerialization(Config.get())) + .registerMappingEnabled(true) .build(); uploader = new CrashUploader(config, crashConfig); @@ -418,6 +419,28 @@ public void testErrorTrackingSerializesRegisterToMemoryMapping() throws Exceptio 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 be643c8e47..9aca3033f6 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 @@ -7,6 +7,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import datadog.crashtracking.CrashUploaderSettings; import datadog.crashtracking.dto.CrashLog; import java.io.BufferedReader; import java.io.IOException; @@ -304,6 +305,31 @@ public void testParseCurrentThreadName(String line, String expected) { HotspotCrashLogParser.parseCurrentThreadName(line)); } + @Test + public void testRegisterToMemoryMappingExcludedWhenDisabled() throws Exception { + CrashLog crashLog = + new HotspotCrashLogParser(new CrashUploaderSettings(false)) + .parse(UUID.randomUUID().toString(), readFileAsString("sample-crash.txt")); + + assertNotNull(crashLog.experimental, "registers and runtimeArgs should still be populated"); + assertThat(crashLog.experimental.registerToMemoryMapping) + .as("registerToMemoryMapping should be absent when disabled") + .isNull(); + } + + @Test + public void testRegisterToMemoryMappingIncludedWhenEnabled() throws Exception { + CrashLog crashLog = + new HotspotCrashLogParser(new CrashUploaderSettings(true)) + .parse(UUID.randomUUID().toString(), readFileAsString("sample-crash.txt")); + + assertNotNull(crashLog.experimental); + assertNotNull(crashLog.experimental.registerToMemoryMapping); + assertFalse( + crashLog.experimental.registerToMemoryMapping.isEmpty(), + "registerToMemoryMapping should be populated when enabled"); + } + private String readFileAsString(String resource) throws IOException { try (InputStream stream = getClass().getClassLoader().getResourceAsStream(resource)) { return new BufferedReader( 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 index 6b308d9cfd..2bcc61aa16 100644 --- 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 @@ -8,56 +8,56 @@ 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 " + "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 " + "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 " + "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); @@ -338,8 +338,7 @@ 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)"); + .isEqualTo("Compiled method (c2) 3068 4 java.util.HashMap::resize (456 bytes)"); } @Test 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 7739d9ffdc..eb899c9e02 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 @@ -35,5 +35,9 @@ public final class CrashTrackingConfig { "crashtracking.debug.autoconfig.enable"; public static final boolean CRASH_TRACKING_ENABLE_AUTOCONFIG_DEFAULT = false; + public static final String CRASH_TRACKING_EXPERIMENTAL_REGISTER_MAPPING_ENABLED = + "crashtracking.experimental.register-mapping.enabled"; + public static final boolean CRASH_TRACKING_EXPERIMENTAL_REGISTER_MAPPING_ENABLED_DEFAULT = false; + private CrashTrackingConfig() {} } diff --git a/internal-api/src/main/java/datadog/trace/api/Config.java b/internal-api/src/main/java/datadog/trace/api/Config.java index fcaac7a9b5..664b8309e1 100644 --- a/internal-api/src/main/java/datadog/trace/api/Config.java +++ b/internal-api/src/main/java/datadog/trace/api/Config.java @@ -298,6 +298,8 @@ import static datadog.trace.api.config.CrashTrackingConfig.CRASH_TRACKING_AGENTLESS_DEFAULT; import static datadog.trace.api.config.CrashTrackingConfig.CRASH_TRACKING_ERRORS_INTAKE_ENABLED; import static datadog.trace.api.config.CrashTrackingConfig.CRASH_TRACKING_ERRORS_INTAKE_ENABLED_DEFAULT; +import static datadog.trace.api.config.CrashTrackingConfig.CRASH_TRACKING_EXPERIMENTAL_REGISTER_MAPPING_ENABLED; +import static datadog.trace.api.config.CrashTrackingConfig.CRASH_TRACKING_EXPERIMENTAL_REGISTER_MAPPING_ENABLED_DEFAULT; import static datadog.trace.api.config.CrashTrackingConfig.CRASH_TRACKING_TAGS; import static datadog.trace.api.config.CwsConfig.CWS_ENABLED; import static datadog.trace.api.config.CwsConfig.CWS_TLS_REFRESH; @@ -1001,6 +1003,7 @@ public static String getHostName() { private final boolean crashTrackingAgentless; private final Map crashTrackingTags; private final boolean crashTrackingErrorsIntakeEnabled; + private final boolean crashTrackingExperimentalRegisterMappingEnabled; private final boolean clientIpEnabled; @@ -2216,6 +2219,10 @@ PROFILING_DATADOG_PROFILER_ENABLED, isDatadogProfilerSafeInCurrentEnvironment()) crashTrackingErrorsIntakeEnabled = configProvider.getBoolean( CRASH_TRACKING_ERRORS_INTAKE_ENABLED, CRASH_TRACKING_ERRORS_INTAKE_ENABLED_DEFAULT); + crashTrackingExperimentalRegisterMappingEnabled = + configProvider.getBoolean( + CRASH_TRACKING_EXPERIMENTAL_REGISTER_MAPPING_ENABLED, + CRASH_TRACKING_EXPERIMENTAL_REGISTER_MAPPING_ENABLED_DEFAULT); float telemetryInterval = configProvider.getFloat(TELEMETRY_HEARTBEAT_INTERVAL, DEFAULT_TELEMETRY_HEARTBEAT_INTERVAL); @@ -3809,6 +3816,10 @@ public boolean isCrashTrackingErrorsIntakeEnabled() { return crashTrackingErrorsIntakeEnabled; } + public boolean isCrashTrackingExperimentalRegisterMappingEnabled() { + return crashTrackingExperimentalRegisterMappingEnabled; + } + public boolean isTelemetryEnabled() { return instrumenterConfig.isTelemetryEnabled(); } @@ -6114,6 +6125,8 @@ public String toString() { + crashTrackingAgentless + ", crashTrackingErrorsIntakeEnabled=" + crashTrackingErrorsIntakeEnabled + + ", crashTrackingExperimentalRegisterMappingEnabled=" + + crashTrackingExperimentalRegisterMappingEnabled + ", remoteConfigEnabled=" + remoteConfigEnabled + ", remoteConfigUrl=" diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index 5b7f7fde88..c18dbddbae 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -953,6 +953,14 @@ "aliases": [] } ], + "DD_CRASHTRACKING_EXPERIMENTAL_REGISTER_MAPPING_ENABLED": [ + { + "version": "A", + "type": "boolean", + "default": "false", + "aliases": [] + } + ], "DD_CRASHTRACKING_PROXY_HOST": [ { "version": "A",