Skip to content

Commit 8f17333

Browse files
committed
feat(crashtracking): Report registry to memory mapping
1 parent b7e21fa commit 8f17333

File tree

5 files changed

+155
-16
lines changed

5 files changed

+155
-16
lines changed

dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/CrashUploader.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,7 @@ private RequestBody makeErrorTrackingRequestBody(@Nonnull CrashLog payload, bool
577577
// experimental
578578
if (payload.experimental != null
579579
&& (payload.experimental.ucontext != null
580+
|| payload.experimental.registerToMemoryMapping != null
580581
|| payload.experimental.runtimeArgs != null)) {
581582
writer.name("experimental");
582583
writer.beginObject();
@@ -588,6 +589,15 @@ private RequestBody makeErrorTrackingRequestBody(@Nonnull CrashLog payload, bool
588589
}
589590
writer.endObject();
590591
}
592+
if (payload.experimental.registerToMemoryMapping != null) {
593+
writer.name("register_to_memory_mapping");
594+
writer.beginObject();
595+
for (Map.Entry<String, String> entry :
596+
payload.experimental.registerToMemoryMapping.entrySet()) {
597+
writer.name(entry.getKey()).value(entry.getValue());
598+
}
599+
writer.endObject();
600+
}
591601
if (payload.experimental.runtimeArgs != null) {
592602
writer.name("runtime_args");
593603
writer.beginArray();

dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/dto/Experimental.java

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,40 @@
88
public final class Experimental {
99
public final Map<String, String> ucontext;
1010

11+
@Json(name = "register_to_memory_mapping")
12+
public final Map<String, String> registerToMemoryMapping;
13+
1114
@Json(name = "runtime_args")
1215
public final List<String> runtimeArgs;
1316

1417
public Experimental(Map<String, String> ucontext) {
15-
this(ucontext, null);
18+
this(ucontext, null, null);
1619
}
1720

1821
public Experimental(Map<String, String> ucontext, List<String> runtimeArgs) {
22+
this(ucontext, null, runtimeArgs);
23+
}
24+
25+
public Experimental(
26+
Map<String, String> ucontext,
27+
Map<String, String> registerToMemoryMapping,
28+
List<String> runtimeArgs) {
1929
this.ucontext = ucontext;
30+
this.registerToMemoryMapping = registerToMemoryMapping;
2031
this.runtimeArgs = runtimeArgs;
2132
}
2233

2334
@Override
2435
public boolean equals(Object o) {
2536
if (!(o instanceof Experimental)) return false;
2637
Experimental that = (Experimental) o;
27-
return Objects.equals(ucontext, that.ucontext) && Objects.equals(runtimeArgs, that.runtimeArgs);
38+
return Objects.equals(ucontext, that.ucontext)
39+
&& Objects.equals(registerToMemoryMapping, that.registerToMemoryMapping)
40+
&& Objects.equals(runtimeArgs, that.runtimeArgs);
2841
}
2942

3043
@Override
3144
public int hashCode() {
32-
return Objects.hash(ucontext, runtimeArgs);
45+
return Objects.hash(ucontext, registerToMemoryMapping, runtimeArgs);
3346
}
3447
}

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

Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ enum State {
6262
SUMMARY,
6363
THREAD,
6464
STACKTRACE,
65+
REGISTER_TO_MEMORY_MAPPING,
6566
REGISTERS,
6667
PROCESS,
6768
VM_ARGUMENTS,
@@ -95,12 +96,16 @@ public HotspotCrashLogParser() {
9596
// per line.
9697
private static final Pattern REGISTER_ENTRY_PARSER =
9798
Pattern.compile("([A-Za-z][A-Za-z0-9]*)\\s*=\\s*(0x[0-9a-fA-F]+)");
99+
private static final Pattern REGISTER_TO_MEMORY_MAPPING_PARSER =
100+
Pattern.compile("^\\s*([A-Za-z][A-Za-z0-9]*)\\s*=");
98101
// Used for the REGISTERS-state exit condition only: the register name must start the line
99102
// (after optional whitespace). This prevents lines like "Top of Stack: (sp=0x...)" and
100103
// "Instructions: (pc=0x...)" from being mistaken for register entries by REGISTER_ENTRY_PARSER's
101104
// find(), which would otherwise match the lowercase "sp"/"pc" tokens embedded in those lines.
102105
private static final Pattern REGISTER_LINE_START =
103106
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 ]*:.+$");
104109
private static final Pattern COMPILED_JAVA_ADDRESS_PARSER =
105110
Pattern.compile("@\\s+(0x[0-9a-fA-F]+)\\s+\\[(0x[0-9a-fA-F]+)\\+(0x[0-9a-fA-F]+)\\]");
106111

@@ -350,10 +355,14 @@ public CrashLog parse(String uuid, String crashLog) {
350355
String datetimeRaw = null;
351356
boolean incomplete = false;
352357
String oomMessage = null;
353-
Map<String, String> registers = null;
358+
Map<String, String> registers = new LinkedHashMap<>();
359+
Map<String, String> registerToMemoryMapping = new LinkedHashMap<>();
360+
String currentRegisterToMemoryMapping = "";
354361
List<String> runtimeArgs = null;
355362
List<String> dynamicLibraryLines = null;
356363
String dynamicLibraryKey = null;
364+
boolean previousLineBlank = false;
365+
State nextThreadSectionState = null;
357366

358367
String[] lines = NEWLINE_SPLITTER.split(crashLog);
359368
outer:
@@ -410,7 +419,10 @@ public CrashLog parse(String uuid, String crashLog) {
410419
}
411420
break;
412421
case STACKTRACE:
413-
if (line.startsWith("siginfo:")) {
422+
nextThreadSectionState = nextThreadSectionState(line, previousLineBlank);
423+
if (nextThreadSectionState != null) {
424+
state = nextThreadSectionState;
425+
} else if (line.startsWith("siginfo:")) {
414426
// spotless:off
415427
// siginfo: si_signo: 11 (SIGSEGV), si_code: 1 (SEGV_MAPERR), si_addr: 0x70
416428
// 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) {
426438
Integer siUid = safelyParseInt(siginfoMatcher.group(7));
427439
sigInfo = new SigInfo(number, name, siCode, sigAction, address, siPid, siUid);
428440
}
429-
} else if (line.startsWith("Registers:")) {
430-
registers = new LinkedHashMap<>();
431-
state = State.REGISTERS;
432-
} else if (line.contains("P R O C E S S")) {
433-
state = State.PROCESS;
434441
} else {
435442
// Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code)
436443
final StackFrame frame = parseLine(line);
@@ -439,8 +446,28 @@ public CrashLog parse(String uuid, String crashLog) {
439446
}
440447
}
441448
break;
449+
case REGISTER_TO_MEMORY_MAPPING:
450+
nextThreadSectionState = nextThreadSectionState(line, previousLineBlank);
451+
if (nextThreadSectionState != null) {
452+
currentRegisterToMemoryMapping = "";
453+
state = nextThreadSectionState;
454+
} else if (!line.isEmpty()) {
455+
final Matcher m = REGISTER_TO_MEMORY_MAPPING_PARSER.matcher(line);
456+
if (m.lookingAt()) {
457+
currentRegisterToMemoryMapping = m.group(1);
458+
registerToMemoryMapping.put(
459+
currentRegisterToMemoryMapping, line.substring(line.indexOf('=') + 1));
460+
} else if (!currentRegisterToMemoryMapping.isEmpty()) {
461+
registerToMemoryMapping.computeIfPresent(
462+
currentRegisterToMemoryMapping, (key, value) -> value + "\n" + line);
463+
}
464+
}
465+
break;
442466
case REGISTERS:
443-
if (!line.isEmpty() && !REGISTER_LINE_START.matcher(line).find()) {
467+
nextThreadSectionState = nextThreadSectionState(line, previousLineBlank);
468+
if (nextThreadSectionState != null) {
469+
state = nextThreadSectionState;
470+
} else if (!line.isEmpty() && !REGISTER_LINE_START.matcher(line).find()) {
444471
// non-empty line that does not start with a register entry signals end of section
445472
state = State.STACKTRACE;
446473
} else {
@@ -508,6 +535,7 @@ public CrashLog parse(String uuid, String crashLog) {
508535
// unexpected parser state; bail out
509536
break outer;
510537
}
538+
previousLineBlank = line.isEmpty();
511539
}
512540

513541
// PROCESS and SYSTEM sections are late enough that all critical data is captured
@@ -572,9 +600,10 @@ public CrashLog parse(String uuid, String crashLog) {
572600
Integer parsedPid = safelyParseInt(pid);
573601
ProcInfo procInfo = parsedPid != null ? new ProcInfo(parsedPid) : null;
574602
Experimental experimental =
575-
(registers != null && !registers.isEmpty())
603+
!registers.isEmpty()
604+
|| !registerToMemoryMapping.isEmpty()
576605
|| (runtimeArgs != null && !runtimeArgs.isEmpty())
577-
? new Experimental(registers, runtimeArgs)
606+
? new Experimental(registers, registerToMemoryMapping, runtimeArgs)
578607
: null;
579608
DynamicLibs files =
580609
(dynamicLibraryLines != null && !dynamicLibraryLines.isEmpty())
@@ -608,6 +637,27 @@ static String dateTimeToISO(String datetime) {
608637
}
609638
}
610639

640+
private static State nextThreadSectionState(String line, boolean previousLineBlank) {
641+
if (line.startsWith("Register to memory mapping:")) {
642+
return State.REGISTER_TO_MEMORY_MAPPING;
643+
}
644+
if (line.startsWith("Registers:")) {
645+
return State.REGISTERS;
646+
}
647+
if (line.startsWith("siginfo:")) {
648+
return null;
649+
}
650+
if (line.contains("P R O C E S S")) {
651+
return State.PROCESS;
652+
}
653+
if (previousLineBlank
654+
&& !line.contains("=")
655+
&& SUBSECTION_TITLE.matcher(line).matches()) {
656+
return State.STACKTRACE;
657+
}
658+
return null;
659+
}
660+
611661
/**
612662
* Detects whether the Dynamic libraries section comes from Linux {@code /proc/self/maps} (address
613663
* range format {@code addr-addr perms ...}) or from the BSD/macOS dyld callback (format {@code

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import static datadog.crashtracking.CrashUploader.HEADER_DD_TELEMETRY_API_VERSION;
55
import static datadog.crashtracking.CrashUploader.TELEMETRY_API_VERSION;
66
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
7+
import static org.assertj.core.api.Assertions.assertThat;
78
import static org.junit.jupiter.api.Assertions.assertEquals;
89
import static org.junit.jupiter.api.Assertions.assertFalse;
910
import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -392,6 +393,33 @@ public void testErrorTrackingSerializesRuntimeArgs() throws Exception {
392393
assertTrue(found);
393394
}
394395

396+
@Test
397+
public void testErrorTrackingSerializesRegisterToMemoryMapping()
398+
throws Exception {
399+
ConfigManager.StoredConfig crashConfig =
400+
new ConfigManager.StoredConfig.Builder(config)
401+
.reportUUID(SAMPLE_UUID)
402+
.processTags("a:b")
403+
.runtimeId("1234")
404+
.tags(ConfigManager.getMergedTagsForSerialization(Config.get()))
405+
.build();
406+
407+
uploader = new CrashUploader(config, crashConfig);
408+
server.enqueue(new MockResponse().setResponseCode(200));
409+
uploader.remoteUpload(readFileAsString("sample-crash.txt"), false, true);
410+
411+
final RecordedRequest recordedRequest = server.takeRequest(5, TimeUnit.SECONDS);
412+
final ObjectMapper mapper = new ObjectMapper();
413+
final JsonNode event = mapper.readTree(recordedRequest.getBody().readUtf8());
414+
415+
final JsonNode mapping = event.at("/experimental/register_to_memory_mapping");
416+
assertThat(mapping.isObject()).isTrue();
417+
assertThat(mapping.get("RSP").asText())
418+
.isEqualTo(
419+
"0x00007f35e6253190 is pointing into the stack for thread: 0x00007f36cd96cc80");
420+
assertThat(mapping.get("RDI").asText()).isEqualTo("0x0 is NULL");
421+
}
422+
395423
private void assertCommonHeader(JsonNode event) {
396424
assertEquals(TELEMETRY_API_VERSION, event.get("api_version").asText());
397425
assertEquals("logs", event.get("request_type").asText());

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

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package datadog.crashtracking.parsers;
22

3+
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.assertj.core.api.InstanceOfAssertFactories.STRING;
35
import static org.junit.jupiter.api.Assertions.assertEquals;
46
import static org.junit.jupiter.api.Assertions.assertFalse;
57
import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -87,11 +89,47 @@ public void testRegisterParsingLinuxAarch64() throws Exception {
8789
assertEquals("0x0000000000000000", crashLog.experimental.ucontext.get("R0"));
8890
assertEquals("0x0000000000000001", crashLog.experimental.ucontext.get("R1"));
8991
assertEquals("0x0000ffff9efa168c", crashLog.experimental.ucontext.get("R30"));
90-
// "Register to memory mapping:" section must NOT be included
9192
assertEquals(31, crashLog.experimental.ucontext.size(), "R0-R30 = 31 registers");
93+
}
9294

93-
assertNotNull(crashLog.experimental.runtimeArgs);
94-
assertTrue(crashLog.experimental.runtimeArgs.contains("--add-modules=ALL-DEFAULT"));
95+
@Test
96+
public void testRegisterToMemoryMapping() throws Exception {
97+
CrashLog crashLog =
98+
new HotspotCrashLogParser()
99+
.parse(UUID.randomUUID().toString(), readFileAsString("sample-crash.txt"));
100+
101+
assertThat(crashLog.experimental).isNotNull();
102+
assertThat(crashLog.experimental.registerToMemoryMapping)
103+
.isNotNull()
104+
.containsEntry(
105+
"RAX",
106+
"0x00007f36ccfbf170 points into unknown readable memory: 0x00007f3600000758 | 58 07 00 00 36 7f 00 00")
107+
.containsEntry(
108+
"RSP", "0x00007f35e6253190 is pointing into the stack for thread: 0x00007f36cd96cc80")
109+
.containsEntry("RDI", "0x0 is NULL")
110+
.containsEntry(
111+
"R11",
112+
"{method} {0x00007f3744198b70} 'resize' '()[Ljava/util/HashMap$Node;' in 'java/util/HashMap'");
113+
}
114+
115+
@Test
116+
public void testRegisterToMultilineMemoryMapping() throws Exception {
117+
CrashLog crashLog =
118+
new HotspotCrashLogParser()
119+
.parse(
120+
UUID.randomUUID().toString(), readFileAsString("sample-crash-linux-aarch64.txt"));
121+
122+
assertThat(crashLog.experimental).isNotNull();
123+
assertThat(crashLog.experimental.registerToMemoryMapping)
124+
.isNotNull()
125+
.containsKey("R10");
126+
assertThat(crashLog.experimental.registerToMemoryMapping)
127+
.extractingByKey("R10", STRING)
128+
.startsWith("0x00000007ffe85850 is an oop: java.lang.Class ")
129+
.contains("\n{0x00000007ffe85850} - klass: 'java/lang/Class'")
130+
.contains("\n - ---- fields (total size 25 words):")
131+
.contains(
132+
"\n - private transient 'name' 'Ljava/lang/String;' @44 \"jdk.internal.misc.Unsafe\"");
95133
}
96134

97135
@Test

0 commit comments

Comments
 (0)