Skip to content

Commit 1fa1dbd

Browse files
committed
feat(crashtracking): Redact register mapping
1 parent 96c44cc commit 1fa1dbd

File tree

5 files changed

+467
-13
lines changed

5 files changed

+467
-13
lines changed

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

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,7 @@ public HotspotCrashLogParser() {
104104
// find(), which would otherwise match the lowercase "sp"/"pc" tokens embedded in those lines.
105105
private static final Pattern REGISTER_LINE_START =
106106
Pattern.compile("^\\s*[A-Za-z][A-Za-z0-9]*\\s*=\\s*0x");
107-
private static final Pattern SUBSECTION_TITLE =
108-
Pattern.compile("^\\s*[A-Za-z][\\w ]*:.+$");
107+
private static final Pattern SUBSECTION_TITLE = Pattern.compile("^\\s*[A-Za-z][\\w ]*:.+$");
109108
private static final Pattern COMPILED_JAVA_ADDRESS_PARSER =
110109
Pattern.compile("@\\s+(0x[0-9a-fA-F]+)\\s+\\[(0x[0-9a-fA-F]+)\\+(0x[0-9a-fA-F]+)\\]");
111110

@@ -599,6 +598,7 @@ public CrashLog parse(String uuid, String crashLog) {
599598
Metadata metadata = new Metadata("dd-trace-java", VersionInfo.VERSION, "java", null);
600599
Integer parsedPid = safelyParseInt(pid);
601600
ProcInfo procInfo = parsedPid != null ? new ProcInfo(parsedPid) : null;
601+
registerToMemoryMapping.replaceAll((k, v) -> RedactUtils.redactRegisterToMemoryMapping(v));
602602
Experimental experimental =
603603
!registers.isEmpty()
604604
|| !registerToMemoryMapping.isEmpty()
@@ -650,9 +650,7 @@ private static State nextThreadSectionState(String line, boolean previousLineBla
650650
if (line.contains("P R O C E S S")) {
651651
return State.PROCESS;
652652
}
653-
if (previousLineBlank
654-
&& !line.contains("=")
655-
&& SUBSECTION_TITLE.matcher(line).matches()) {
653+
if (previousLineBlank && !line.contains("=") && SUBSECTION_TITLE.matcher(line).matches()) {
656654
return State.STACKTRACE;
657655
}
658656
return null;
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
package datadog.crashtracking.parsers;
2+
3+
import java.util.function.Function;
4+
import java.util.regex.Matcher;
5+
import java.util.regex.Pattern;
6+
7+
/**
8+
* Utilities for redacting potentially sensitive data from JVM crash log register-to-memory mapping
9+
* entries.
10+
*/
11+
public final class RedactUtils {
12+
13+
static final String REDACTED = "redacted";
14+
private static final String REDACTED_STRING = "REDACTED";
15+
16+
private static final String[] KNOWN_PACKAGES_PREFIXES = {
17+
// Java SE / JDK internals
18+
"java/", "jdk/", "sun/", "javax/",
19+
// Jakarta EE (successor to javax)
20+
"jakarta/",
21+
// Oracle/Sun vendor packages
22+
"com/sun/", "com/oracle/",
23+
};
24+
25+
// " - string: "value"" in String oop dumps
26+
private static final Pattern STRING_CONTENT = Pattern.compile("(\\s*- string: )\"[^\"]*\"");
27+
28+
// Type descriptors like Lcom/company/Type;
29+
private static final Pattern TYPE_DESCRIPTOR = Pattern.compile("L([A-Za-z$_][A-Za-z0-9$_/]*);");
30+
31+
// klass/interface references: - klass: 'com/company/Class'
32+
private static final Pattern KLASS_REF = Pattern.compile("((?:klass|interface): ')([^']+)(')");
33+
34+
// 'in 'class'' clause in {method} descriptor entries
35+
private static final Pattern METHOD_IN_CLASS = Pattern.compile("( in ')([^']+)(')");
36+
37+
// Library path after <offset 0x...> in /path/to/lib.so
38+
private static final Pattern LIBRARY_PATH =
39+
Pattern.compile("(<offset 0x[0-9a-fA-F]+> in )(/\\S+)");
40+
41+
// Dotted class name followed by an OOP reference: "com.company.Type"{0x...}
42+
// This specifically identifies the inline string value of a java.lang.Class 'name' field
43+
private static final Pattern DOTTED_CLASS_OOP_REF =
44+
Pattern.compile(
45+
"\"([A-Za-z][A-Za-z0-9$]*(?:\\.[A-Za-z][A-Za-z0-9$]*)*)\"(\\{0x[0-9a-fA-F]+\\})");
46+
47+
// is an oop: com.company.Class
48+
private static final Pattern IS_AN_OOP =
49+
Pattern.compile("(is an oop: )([A-Za-z][A-Za-z0-9$]*(?:\\.[A-Za-z][A-Za-z0-9$]*)*)");
50+
51+
private RedactUtils() {
52+
}
53+
54+
/**
55+
* Main entry point: redact sensitive data from a register-to-memory mapping value (possibly
56+
* multiline).
57+
*/
58+
public static String redactRegisterToMemoryMapping(String value) {
59+
if (value == null || value.isEmpty()) return value;
60+
String[] lines = value.split("\n", -1);
61+
StringBuilder sb = new StringBuilder();
62+
for (int i = 0; i < lines.length; i++) {
63+
if (i > 0) sb.append('\n');
64+
sb.append(redactLine(lines[i]));
65+
}
66+
return sb.toString();
67+
}
68+
69+
private static String redactLine(String line) {
70+
line = redactStringTypeValue(line);
71+
line = redactTypeDescriptors(line);
72+
line = redactKlassReference(line);
73+
line = redactMethodClass(line);
74+
line = redactLibraryPath(line);
75+
line = redactDottedClassOopRef(line);
76+
line = redactOopClassName(line);
77+
return line;
78+
}
79+
80+
/**
81+
* Redacts string content in String oop dump lines:
82+
*
83+
* <code> - string: "Some string"</code> to <code> - string: "REDACTED"</code>
84+
*/
85+
static String redactStringTypeValue(String line) {
86+
return STRING_CONTENT.matcher(line).replaceAll("$1\"" + REDACTED_STRING + "\"");
87+
}
88+
89+
/**
90+
* Redacts the package of type descriptors in a line:
91+
*
92+
* <code>Lcom/company/Type;</code> to <code>Lredacted/redacted/Type;</code>
93+
*/
94+
static String redactTypeDescriptors(String line) {
95+
return replaceAll(TYPE_DESCRIPTOR, line, m -> "L" + redactJvmClassName(m.group(1)) + ";");
96+
}
97+
98+
/**
99+
* Redacts klass/interface references in a line:
100+
*
101+
* <code>klass: 'com/company/Class'</code> to <code>klass: 'redacted/redacted/Class'</code>
102+
*/
103+
static String redactKlassReference(String line) {
104+
return replaceAll(
105+
KLASS_REF, line, m -> m.group(1) + redactJvmClassName(m.group(2)) + m.group(3));
106+
}
107+
108+
/**
109+
* Redacts the class in a method descriptor's {@code in 'class'} clause:
110+
*
111+
* <code>in 'com/company/Class'</code> to <code>in 'redacted/redacted/Class'</code>
112+
*/
113+
static String redactMethodClass(String line) {
114+
return replaceAll(
115+
METHOD_IN_CLASS, line, m -> m.group(1) + redactJvmClassName(m.group(2)) + m.group(3));
116+
}
117+
118+
/**
119+
* Redacts all but the parent directory and filename from a library path in the line:
120+
*
121+
* <code>&lt;offset 0x...&gt; in /path/to/dir/lib.so</code> to
122+
* <code>&lt;offset 0x...&gt; in /redacted/redacted/dir/lib.so</code>
123+
*/
124+
static String redactLibraryPath(String line) {
125+
return replaceAll(LIBRARY_PATH, line, m -> m.group(1) + redactPath(m.group(2)));
126+
}
127+
128+
/**
129+
* Redacts dotted class names that appear as inline field values followed by an OOP reference:
130+
*
131+
* <code>"com.company.SomeType"{0x...}</code> to <code>"redacted.redacted.SomeType"{0x...}</code>
132+
*/
133+
static String redactDottedClassOopRef(String line) {
134+
return replaceAll(
135+
DOTTED_CLASS_OOP_REF,
136+
line,
137+
m -> "\"" + redactDottedClassName(m.group(1)) + "\"" + m.group(2));
138+
}
139+
140+
/**
141+
* Redacts the class name in {@code is an oop: ClassName}:
142+
*
143+
* <code>is an oop: com.company.Class</code> to <code>is an oop: redacted.redacted.Class</code>
144+
*/
145+
static String redactOopClassName(String line) {
146+
return replaceAll(IS_AN_OOP, line, m -> m.group(1) + redactDottedClassName(m.group(2)));
147+
}
148+
149+
/**
150+
* Redacts the package of a slash-separated JVM class name, unless it belongs to a known package.
151+
*
152+
* <code>com/company/SomeType</code> to <code>redacted/redacted/SomeType</code>;
153+
* <code>java/lang/String</code> unchanged.
154+
*/
155+
static String redactJvmClassName(String className) {
156+
if (isKnownJvmPackage(className)) {
157+
return className;
158+
}
159+
return redactClassName('/', className);
160+
}
161+
162+
/**
163+
* Redacts the package of a dot-separated class name, unless it belongs to a known package.
164+
*
165+
* <code>com.company.SomeType</code> to <code>redacted.redacted.SomeType</code>;
166+
* <code>java.lang.String</code> unchanged.
167+
*/
168+
static String redactDottedClassName(String className) {
169+
if (isKnownJvmPackage(className.replace('.', '/'))) {
170+
return className;
171+
}
172+
return redactClassName('.', className);
173+
}
174+
175+
private static String redactClassName(char sep, String className) {
176+
int lastSep = className.lastIndexOf(sep);
177+
if (lastSep < 0) return className;
178+
StringBuilder sb = new StringBuilder();
179+
int pos = 0;
180+
while (pos <= lastSep) {
181+
int next = className.indexOf(sep, pos);
182+
if (sb.length() > 0) sb.append(sep);
183+
sb.append(REDACTED);
184+
pos = next + 1;
185+
}
186+
return sb.append(sep).append(className, lastSep + 1, className.length()).toString();
187+
}
188+
189+
/**
190+
* Redacts all path segments except the parent directory and filename.
191+
*
192+
* <code>/path/to/dir/lib.so</code> to <code>/redacted/redacted/dir/lib.so</code>
193+
*/
194+
static String redactPath(String path) {
195+
String[] parts = path.split("/", -1);
196+
// parts[0] is always "" (before the leading slash)
197+
if (parts.length <= 3) {
198+
return path; // /dir/file or shorter: nothing to redact
199+
}
200+
StringBuilder sb = new StringBuilder();
201+
for (int i = 1; i < parts.length - 2; i++) {
202+
sb.append('/').append(REDACTED);
203+
}
204+
return sb.append('/')
205+
.append(parts[parts.length - 2])
206+
.append('/')
207+
.append(parts[parts.length - 1])
208+
.toString();
209+
}
210+
211+
private static boolean isKnownJvmPackage(String slashClassName) {
212+
for (String prefix : KNOWN_PACKAGES_PREFIXES) {
213+
if (slashClassName.startsWith(prefix)) {
214+
return true;
215+
}
216+
}
217+
return slashClassName.contains("datadog") || slashClassName.startsWith("com/dd/");
218+
}
219+
220+
private static String replaceAll(
221+
Pattern pattern, String input, Function<Matcher, String> replacement) {
222+
Matcher m = pattern.matcher(input);
223+
if (!m.find()) {
224+
return input;
225+
}
226+
StringBuilder sb = new StringBuilder();
227+
int lastEnd = 0;
228+
do {
229+
sb.append(input, lastEnd, m.start());
230+
sb.append(replacement.apply(m));
231+
lastEnd = m.end();
232+
} while (m.find());
233+
return sb.append(input, lastEnd, input.length()).toString();
234+
}
235+
}

dd-java-agent/agent-crashtracking/src/test/java/datadog/crashtracking/CrashUploaderTest.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -394,8 +394,7 @@ public void testErrorTrackingSerializesRuntimeArgs() throws Exception {
394394
}
395395

396396
@Test
397-
public void testErrorTrackingSerializesRegisterToMemoryMapping()
398-
throws Exception {
397+
public void testErrorTrackingSerializesRegisterToMemoryMapping() throws Exception {
399398
ConfigManager.StoredConfig crashConfig =
400399
new ConfigManager.StoredConfig.Builder(config)
401400
.reportUUID(SAMPLE_UUID)
@@ -415,8 +414,7 @@ public void testErrorTrackingSerializesRegisterToMemoryMapping()
415414
final JsonNode mapping = event.at("/experimental/register_to_memory_mapping");
416415
assertThat(mapping.isObject()).isTrue();
417416
assertThat(mapping.get("RSP").asText())
418-
.isEqualTo(
419-
"0x00007f35e6253190 is pointing into the stack for thread: 0x00007f36cd96cc80");
417+
.isEqualTo("0x00007f35e6253190 is pointing into the stack for thread: 0x00007f36cd96cc80");
420418
assertThat(mapping.get("RDI").asText()).isEqualTo("0x0 is NULL");
421419
}
422420

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,11 @@ public void testRegisterToMemoryMapping() throws Exception {
109109
.containsEntry("RDI", "0x0 is NULL")
110110
.containsEntry(
111111
"R11",
112-
"{method} {0x00007f3744198b70} 'resize' '()[Ljava/util/HashMap$Node;' in 'java/util/HashMap'");
112+
"{method} {0x00007f3744198b70} 'resize' '()[Ljava/util/HashMap$Node;' in 'java/util/HashMap'")
113+
// unknown packages are redacted; known class names (last segment) are preserved
114+
.containsEntry(
115+
"RSI",
116+
"{method} {0x00007f3639c2ff00} 'saveJob' '(Lredacted/redacted/redacted/redacted/REDACT_THIS;ILjava/lang/String;)V' in 'redacted/redacted/redacted/redacted/REDACT_THIS'");
113117
}
114118

115119
@Test
@@ -120,9 +124,7 @@ public void testRegisterToMultilineMemoryMapping() throws Exception {
120124
UUID.randomUUID().toString(), readFileAsString("sample-crash-linux-aarch64.txt"));
121125

122126
assertThat(crashLog.experimental).isNotNull();
123-
assertThat(crashLog.experimental.registerToMemoryMapping)
124-
.isNotNull()
125-
.containsKey("R10");
127+
assertThat(crashLog.experimental.registerToMemoryMapping).isNotNull().containsKey("R10");
126128
assertThat(crashLog.experimental.registerToMemoryMapping)
127129
.extractingByKey("R10", STRING)
128130
.startsWith("0x00000007ffe85850 is an oop: java.lang.Class ")

0 commit comments

Comments
 (0)