Skip to content

Commit b8ae4df

Browse files
committed
feat(crashtracking): Redact register mapping
1 parent 8f17333 commit b8ae4df

File tree

5 files changed

+460
-13
lines changed

5 files changed

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

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)