Skip to content

Commit f3b0447

Browse files
committed
chore(crashtracking): More string redaction
1 parent c591230 commit f3b0447

3 files changed

Lines changed: 95 additions & 36 deletions

File tree

dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/parsers/RedactUtils.java

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -72,26 +72,57 @@ private RedactUtils() {}
7272
public static String redactRegisterToMemoryMapping(String value) {
7373
if (value == null || value.isEmpty()) return value;
7474
String[] lines = value.split("\n", -1);
75+
// java.lang.Class oop dumps: String fields hold class names, not arbitrary data.
76+
// All other oop types: String fields are application data and must be fully redacted.
77+
boolean isClassOop = isJavaLangClassOop(lines[0]);
7578
StringBuilder sb = new StringBuilder();
7679
for (int i = 0; i < lines.length; i++) {
7780
if (i > 0) sb.append('\n');
78-
sb.append(redactLine(lines[i]));
81+
sb.append(redactLine(lines[i], isClassOop));
7982
}
8083
return sb.toString();
8184
}
8285

83-
private static String redactLine(String line) {
86+
/**
87+
* Returns true if the first line of a register-to-memory mapping value indicates a {@code
88+
* java.lang.Class} oop (not a subclass or other type).
89+
*/
90+
private static boolean isJavaLangClassOop(String firstLine) {
91+
int idx = firstLine.indexOf("is an oop: java.lang.Class");
92+
if (idx < 0) return false;
93+
// Ensure the class name ends here — not a prefix of e.g. java.lang.ClassLoader
94+
int end = idx + "is an oop: java.lang.Class".length();
95+
return end >= firstLine.length() || firstLine.charAt(end) == ' ';
96+
}
97+
98+
private static String redactLine(String line, boolean isClassOop) {
8499
line = redactStringTypeValue(line);
85100
line = redactTypeDescriptors(line);
86101
line = redactKlassReference(line);
87102
line = redactMethodClass(line);
88103
line = redactLibraryPath(line);
89-
line = redactDottedClassOopRef(line);
104+
line = redactStringOopRef(line, isClassOop);
90105
line = redactOopClassName(line);
91106
line = redactReadableMemoryHexDump(line);
92107
return line;
93108
}
94109

110+
/**
111+
* Redacts {@code "value"\{0x...\}} OOP references in oop dump field lines. When {@code
112+
* isClassOop} is true (inside a {@code java.lang.Class} oop dump) the value is treated as a class
113+
* name and its package is redacted. Otherwise — any other oop type — the value is always fully
114+
* redacted to {@code "REDACTED"} since it may be arbitrary application data.
115+
*/
116+
private static String redactStringOopRef(String line, boolean isClassOop) {
117+
return replaceAll(
118+
DOTTED_CLASS_OOP_REF,
119+
line,
120+
m ->
121+
isClassOop
122+
? "\"" + redactDottedClassName(m.group(1)) + "\"" + m.group(2)
123+
: "\"" + REDACTED_STRING + "\"" + m.group(2));
124+
}
125+
95126
/**
96127
* Redacts string content in String oop dump lines: <code> - string: "Some string"</code> to
97128
* <code> - string: "REDACTED"</code>
@@ -127,23 +158,23 @@ static String redactMethodClass(String line) {
127158
}
128159

129160
/**
130-
* Redacts all but the parent directory and filename from a library path. Handles both
131-
* <code>&lt;offset 0x...&gt; in /path/to/dir/lib.so</code> and <code>symbol+0 in
161+
* Redacts all but the parent directory and filename from a library path. Handles both <code>
162+
* &lt;offset 0x...&gt; in /path/to/dir/lib.so</code> and <code>symbol+0 in
132163
* /path/to/dir/lib.so</code> to <code>... in /redacted/redacted/dir/lib.so</code>
133164
*/
134165
static String redactLibraryPath(String line) {
135166
return replaceAll(LIBRARY_PATH, line, m -> m.group(1) + redactPath(m.group(2)));
136167
}
137168

138169
/**
139-
* Redacts dotted class names that appear as inline field values followed by an OOP reference:
140-
* <code>"com.company.SomeType"{0x...}</code> to <code>"redacted.redacted.SomeType"{0x...}</code>
170+
* Redacts any {@code "value"\{0x...\}} OOP reference to {@code "REDACTED"\{0x...\}}. This is the
171+
* safe default for lines that are not part of a {@code java.lang.Class} oop dump, where the
172+
* String value may be arbitrary application data. For class-name-aware redaction (inside a {@code
173+
* java.lang.Class} oop) use {@link #redactRegisterToMemoryMapping} which detects the oop type
174+
* automatically.
141175
*/
142176
static String redactDottedClassOopRef(String line) {
143-
return replaceAll(
144-
DOTTED_CLASS_OOP_REF,
145-
line,
146-
m -> "\"" + redactDottedClassName(m.group(1)) + "\"" + m.group(2));
177+
return redactStringOopRef(line, false);
147178
}
148179

149180
/**

dd-java-agent/agent-crashtracking/src/test/java/datadog/crashtracking/parsers/HotspotCrashLogParserTest.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,9 @@ public void testRegisterParsingMacosAarch64() throws Exception {
8484
}
8585

8686
/**
87-
* Verifies the register-to-memory mapping section for the macOS aarch64 sample:
88-
* representative values, library path redaction, and that "Top of Stack:" / "Instructions:"
89-
* subsections are not absorbed into register values.
87+
* Verifies the register-to-memory mapping section for the macOS aarch64 sample: representative
88+
* values, library path redaction, and that "Top of Stack:" / "Instructions:" subsections are not
89+
* absorbed into register values.
9090
*/
9191
@Test
9292
public void testRegisterToMemoryMappingMacosAarch64() throws Exception {
@@ -109,7 +109,8 @@ public void testRegisterToMemoryMappingMacosAarch64() throws Exception {
109109
.extractingByKey("x16", STRING)
110110
.isEqualTo(
111111
"0x0000000182d709d0: pthread_jit_write_protect_np+0 in /redacted/redacted/system/libsystem_pthread.dylib at 0x0000000182d69000");
112-
// /Users/USER/.local/share/mise/installs/java/25.0.2/lib/server/libjvm.dylib → 9 redacted + "server/libjvm.dylib"
112+
// /Users/USER/.local/share/mise/installs/java/25.0.2/lib/server/libjvm.dylib → 9 redacted +
113+
// "server/libjvm.dylib"
113114
assertThat(mapping)
114115
.extractingByKey("x21", STRING)
115116
.isEqualTo(
@@ -124,8 +125,7 @@ public void testRegisterToMemoryMappingMacosAarch64() throws Exception {
124125
// "Top of Stack: (sp=0x...)" and "Instructions: (pc=0x...)" must not leak into register values
125126
assertThat(mapping).doesNotContainKey("Top of Stack");
126127
assertThat(mapping)
127-
.allSatisfy(
128-
(k, v) -> assertThat(v).doesNotContain("Top of Stack:", "Instructions:"));
128+
.allSatisfy((k, v) -> assertThat(v).doesNotContain("Top of Stack:", "Instructions:"));
129129

130130
// sp is the last register before "Top of Stack:" — its value must be clean
131131
assertThat(mapping)

dd-java-agent/agent-crashtracking/src/test/java/datadog/crashtracking/parsers/RedactUtilsTest.java

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -168,21 +168,26 @@ void testRedactLibraryPath_leavesUnrelatedLinesUnchanged() {
168168
}
169169

170170
@Test
171-
void testRedactDottedClassOopRef_redactsUnknownPackage() {
171+
void testRedactDottedClassOopRef_redactsAnyStringOopRef() {
172+
// Without oop-type context, all "value"{0x...} OOP refs are treated as arbitrary application
173+
// data and fully redacted — even if the value looks like a class name
172174
assertThat(
173175
RedactUtils.redactDottedClassOopRef(
174176
" - private transient 'name' 'Ljava/lang/String;' @44 \"com.company.SomeType\"{0x00000007142f7200} (0xe285ee40)"))
175177
.isEqualTo(
176-
" - private transient 'name' 'Ljava/lang/String;' @44 \"redacted.redacted.SomeType\"{0x00000007142f7200} (0xe285ee40)");
177-
}
178-
179-
@Test
180-
void testRedactDottedClassOopRef_keepsKnownPackage() {
178+
" - private transient 'name' 'Ljava/lang/String;' @44 \"REDACTED\"{0x00000007142f7200} (0xe285ee40)");
179+
// Dotted names with known packages are also fully redacted — any string can be a secret
181180
assertThat(
182181
RedactUtils.redactDottedClassOopRef(
183182
" - private transient 'name' 'Ljava/lang/String;' @44 \"jdk.internal.misc.Unsafe\"{0x00000007142f7200} (0xe285ee40)"))
184183
.isEqualTo(
185-
" - private transient 'name' 'Ljava/lang/String;' @44 \"jdk.internal.misc.Unsafe\"{0x00000007142f7200} (0xe285ee40)");
184+
" - private transient 'name' 'Ljava/lang/String;' @44 \"REDACTED\"{0x00000007142f7200} (0xe285ee40)");
185+
// Plain single-word strings (no dots) are also redacted
186+
assertThat(
187+
RedactUtils.redactDottedClassOopRef(
188+
" - final 'value' 'Ljava/lang/String;' @40 \"SourceFile\"{0x00000007ffe7a6a0} (0xfffcf4d4)"))
189+
.isEqualTo(
190+
" - final 'value' 'Ljava/lang/String;' @40 \"REDACTED\"{0x00000007ffe7a6a0} (0xfffcf4d4)");
186191
}
187192

188193
@Test
@@ -209,25 +214,48 @@ void testRedactRegisterToMemoryMapping_methodDescriptor() {
209214

210215
@Test
211216
void testRedactRegisterToMemoryMapping_multilineOopDump() {
212-
// Mirrors the java.lang.Class oop dump format: the 'name' field holds a dotted class name
213-
// as an inline string value followed by an OOP ref, and 'loader' holds a typed object ref.
217+
// Non-java.lang.Class oop: ALL "value"{0x...} OOP refs are fully redacted to "REDACTED"
218+
// regardless of their shape — any string value may be a secret.
214219
String value =
215-
"0x00000007ffe85850 is an oop: com.company.Config \n"
216-
+ "{0x00000007ffe85850} - klass: 'com/company/Config'\n"
217-
+ " - ---- fields (total size 3 words):\n"
218-
+ " - private transient 'name' 'Ljava/lang/String;' @12 \"com.company.Config\"{0x00000007aabbccdd} (0x12345678)\n"
219-
+ " - private 'owner' 'Lcom/company/User;' @16 null (0x00000000)\n"
220+
"0x00000007142f8848 is an oop: com.company.SymbolEntry \n"
221+
+ "{0x00000007142f8848} - klass: 'com/company/SymbolEntry'\n"
222+
+ " - ---- fields (total size 9 words):\n"
223+
+ " - final 'tag' 'Ljava/lang/String;' @12 \"SourceFile\"{0x00000007ffe7a6a0} (0xfffcf4d4)\n"
224+
+ " - final 'value' 'Ljava/lang/String;' @16 \"com.company.Config\"{0x00000007aabbccdd} (0x12345678)\n"
225+
+ " - final 'hint' 'Ljava/lang/String;' @20 \"java.vendor.url.bug\"{0x00000007aabbccee} (0x12345679)\n"
226+
+ " - final 'owner' 'Ljava/lang/String;' @24 null (0x00000000)\n"
220227
+ " - string: \"some sensitive value\"";
221228
assertThat(RedactUtils.redactRegisterToMemoryMapping(value))
222229
.isEqualTo(
223-
"0x00000007ffe85850 is an oop: redacted.redacted.Config \n"
224-
+ "{0x00000007ffe85850} - klass: 'redacted/redacted/Config'\n"
225-
+ " - ---- fields (total size 3 words):\n"
226-
+ " - private transient 'name' 'Ljava/lang/String;' @12 \"redacted.redacted.Config\"{0x00000007aabbccdd} (0x12345678)\n"
227-
+ " - private 'owner' 'Lredacted/redacted/User;' @16 null (0x00000000)\n"
230+
"0x00000007142f8848 is an oop: redacted.redacted.SymbolEntry \n"
231+
+ "{0x00000007142f8848} - klass: 'redacted/redacted/SymbolEntry'\n"
232+
+ " - ---- fields (total size 9 words):\n"
233+
+ " - final 'tag' 'Ljava/lang/String;' @12 \"REDACTED\"{0x00000007ffe7a6a0} (0xfffcf4d4)\n"
234+
+ " - final 'value' 'Ljava/lang/String;' @16 \"REDACTED\"{0x00000007aabbccdd} (0x12345678)\n"
235+
+ " - final 'hint' 'Ljava/lang/String;' @20 \"REDACTED\"{0x00000007aabbccee} (0x12345679)\n"
236+
+ " - final 'owner' 'Ljava/lang/String;' @24 null (0x00000000)\n"
228237
+ " - string: \"REDACTED\"");
229238
}
230239

240+
@Test
241+
void testRedactRegisterToMemoryMapping_javaLangClassOopPreservesClassName() {
242+
// java.lang.Class oop: String OOP refs in field values are treated as class names and
243+
// get package redaction (not full redaction), since that is what java.lang.Class stores.
244+
String value =
245+
"0x00000007ffe85850 is an oop: java.lang.Class \n"
246+
+ "{0x00000007ffe85850} - klass: 'java/lang/Class'\n"
247+
+ " - ---- fields (total size 25 words):\n"
248+
+ " - private transient 'name' 'Ljava/lang/String;' @44 \"com.company.Config\"{0x00000007aabbccdd} (0x12345678)\n"
249+
+ " - private transient 'name' 'Ljava/lang/String;' @44 \"jdk.internal.misc.Unsafe\"{0x00000007142f7200} (0xe285ee40)";
250+
assertThat(RedactUtils.redactRegisterToMemoryMapping(value))
251+
.isEqualTo(
252+
"0x00000007ffe85850 is an oop: java.lang.Class \n"
253+
+ "{0x00000007ffe85850} - klass: 'java/lang/Class'\n"
254+
+ " - ---- fields (total size 25 words):\n"
255+
+ " - private transient 'name' 'Ljava/lang/String;' @44 \"redacted.redacted.Config\"{0x00000007aabbccdd} (0x12345678)\n"
256+
+ " - private transient 'name' 'Ljava/lang/String;' @44 \"jdk.internal.misc.Unsafe\"{0x00000007142f7200} (0xe285ee40)");
257+
}
258+
231259
@Test
232260
void testRedactRegisterToMemoryMapping_libraryPath() {
233261
assertThat(

0 commit comments

Comments
 (0)