Skip to content

Commit 36f4468

Browse files
committed
fix(profiler): guard line-number-table copy against stale jmethodID
fillJavaMethodInfo() memcpy'd the JVMTI-returned line-number table without probing it first, unlike the sibling class/method name+sig strings hardened in #537. Crash reported in production as SIGSEGV in Lookup::resolveMethod on stock HotSpot JDK 11.
1 parent 4011bf7 commit 36f4468

3 files changed

Lines changed: 202 additions & 4 deletions

File tree

ddprof-lib/src/main/cpp/counters.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
X(UNWINDING_TIME_JVMTI, "unwinding_ticks_jvmti") \
7474
X(CALLTRACE_STORAGE_DROPPED, "calltrace_storage_dropped_traces") \
7575
X(LINE_NUMBER_TABLES, "line_number_tables") \
76+
X(LINE_NUMBER_TABLE_UNREADABLE, "line_number_table_unreadable") \
7677
X(REMOTE_SYMBOLICATION_FRAMES, "remote_symbolication_frames") \
7778
X(REMOTE_SYMBOLICATION_LIBS_WITH_BUILD_ID, "remote_symbolication_libs_with_build_id") \
7879
X(REMOTE_SYMBOLICATION_BUILD_ID_CACHE_HITS, "remote_symbolication_build_id_cache_hits") \

ddprof-lib/src/main/cpp/flightRecorder.cpp

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -348,11 +348,25 @@ void Lookup::fillJavaMethodInfo(MethodInfo *mi, jmethodID method,
348348
void *owned_table = nullptr;
349349
if (line_number_table_size > 0) {
350350
size_t bytes = (size_t)line_number_table_size * sizeof(jvmtiLineNumberEntry);
351-
owned_table = malloc(bytes);
352-
if (owned_table != nullptr) {
353-
memcpy(owned_table, line_number_table, bytes);
351+
// GetLineNumberTable() is called on the same possibly-stale jmethodID
352+
// that GetMethodDeclaringClass/GetClassSignature/GetMethodName above
353+
// were probed for -- the TOCTOU race documented above (class
354+
// unloaded between sample capture and dump) applies here just as
355+
// much as to those calls, and crash telemetry already showed those
356+
// sibling calls returning JVMTI_ERROR_NONE with unmapped string
357+
// pointers despite the spec saying the returned array should be a
358+
// fresh, caller-owned allocation. Nothing about this call guarantees
359+
// it is exempt from the same failure mode, so probe before copying
360+
// rather than assume the pointer is safe to dereference.
361+
if (SafeAccess::isReadableRange(line_number_table, bytes)) {
362+
owned_table = malloc(bytes);
363+
if (owned_table != nullptr) {
364+
memcpy(owned_table, line_number_table, bytes);
365+
} else {
366+
TEST_LOG("Failed to allocate %zu bytes for line number table copy", bytes);
367+
}
354368
} else {
355-
TEST_LOG("Failed to allocate %zu bytes for line number table copy", bytes);
369+
Counters::increment(LINE_NUMBER_TABLE_UNREADABLE);
356370
}
357371
}
358372
jvmtiError dealloc_err = jvmti->Deallocate((unsigned char *)line_number_table);
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
/*
2+
* Copyright 2026, Datadog, Inc.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
// Reproduces a crash reported in production:
7+
//
8+
// __memcpy_evex_unaligned_erms
9+
// Lookup::resolveMethod(_asgct_callframe&)
10+
// ...
11+
// Recording::writeStackTraces(Buffer*, Lookup*)
12+
// ...
13+
// Profiler::dump / FlightRecorder::dump
14+
//
15+
// Lookup::fillJavaMethodInfo() (inlined into resolveMethod() under -O2/-O3)
16+
// calls jvmtiEnv::GetLineNumberTable(method, ...) and then copied the
17+
// result with an unguarded:
18+
//
19+
// memcpy(owned_table, line_number_table, bytes);
20+
//
21+
// (flightRecorder.cpp now guards this copy with SafeAccess::isReadableRange()
22+
// -- see below -- so the line numbers of the unguarded version are no longer
23+
// present in the file; this file's tests reproduce that removed pattern.)
24+
//
25+
// Unlike the class_name/method_name/method_sig strings returned by the
26+
// preceding JVMTI calls (which ARE probed with SafeAccess::isReadableRange
27+
// before use, see PR #537 / commit ef13aa29f), the line_number_table pointer
28+
// is used directly.
29+
//
30+
// Per the JVMTI spec, GetLineNumberTable() hands back a freshly-allocated,
31+
// caller-owned array (hence the Deallocate() call after the copy) that is
32+
// decoupled from the Method's lifetime -- a spec-compliant implementation
33+
// cannot invalidate it out from under the caller. The actual risk, per the
34+
// comments already in fillJavaMethodInfo, is the TOCTOU race documented
35+
// right there: "GetMethodDeclaringClass may return a jclass wrapping a
36+
// stale/garbage oop when the class was unloaded between sample capture and
37+
// dump" -- a race against class unloading, not tied to any one JVM vendor.
38+
// That same comment block notes crash telemetry showed GetClassSignature/
39+
// GetMethodName returning JVMTI_ERROR_NONE with unmapped string pointers
40+
// despite the spec saying they should be valid (a separate OpenJ9-specific
41+
// jmethodID-corruption mode is also handled a few lines above, but is not
42+
// the only source of this). The production crash motivating this file was
43+
// on a stock HotSpot JDK 11, confirming the race is not OpenJ9-specific.
44+
// GetLineNumberTable() is called on the exact same jmethodID as
45+
// GetMethodDeclaringClass/GetClassSignature/GetMethodName, with nothing
46+
// about it that would exempt it from that same observed failure mode.
47+
//
48+
// These tests isolate the copy step from JVMTI/JNI (which fillJavaMethodInfo
49+
// requires and which is impractical to fake in a plain gtest) by exercising
50+
// the exact copy pattern against a pointer whose backing memory is gone --
51+
// standing in for "GetLineNumberTable() returned a bad pointer" regardless
52+
// of the precise reason. The result at the memcpy call site is identical
53+
// either way, so this is sufficient to prove both the crash and the fix.
54+
55+
#include <gtest/gtest.h>
56+
#include <cstring>
57+
#include <signal.h>
58+
#include <sys/mman.h>
59+
#include <unistd.h>
60+
61+
#include "flightRecorder.h"
62+
#include "os.h"
63+
#include "safeAccess.h"
64+
65+
namespace {
66+
67+
// SafeAccess::isReadableRange()/isReadable() rely on a SIGSEGV handler
68+
// registered via OS::replaceSigsegvHandler() to catch the fault at a known
69+
// trampoline address and turn it into a safe return value (see
70+
// safefetch_ut.cpp for the same pattern). Without it, a fault inside the
71+
// safefetch trampoline is an ordinary unhandled SIGSEGV.
72+
void (*orig_segvHandler)(int signo, siginfo_t *siginfo, void *ucontext);
73+
74+
void lineNumberTableSegvHandler(int signo, siginfo_t *siginfo, void *context) {
75+
if (!SafeAccess::handle_safefetch(signo, context)) {
76+
if (orig_segvHandler != nullptr) {
77+
orig_segvHandler(signo, siginfo, context);
78+
}
79+
}
80+
}
81+
82+
class LineNumberTableCopyTest : public ::testing::Test {
83+
protected:
84+
void SetUp() override {
85+
orig_segvHandler = OS::replaceSigsegvHandler(lineNumberTableSegvHandler);
86+
}
87+
88+
void TearDown() override { OS::replaceSigsegvHandler(orig_segvHandler); }
89+
};
90+
91+
// Allocates a page-sized region seeded with `count` jvmtiLineNumberEntry
92+
// records, standing in for a buffer jvmtiEnv::GetLineNumberTable() would
93+
// have returned.
94+
jvmtiLineNumberEntry *makeFakeLineNumberTable(void *page, int count) {
95+
jvmtiLineNumberEntry *table = (jvmtiLineNumberEntry *)page;
96+
for (int i = 0; i < count; i++) {
97+
table[i].start_location = i * 4;
98+
table[i].line_number = i + 1;
99+
}
100+
return table;
101+
}
102+
103+
} // namespace
104+
105+
// Reproducer: the code shape that used to be in flightRecorder.cpp's
106+
// fillJavaMethodInfo() before the fix, with no readability guard before the
107+
// memcpy. Demonstrates that once the source page is gone, the copy step used
108+
// by resolveMethod() crashes the process rather than failing gracefully.
109+
TEST(LineNumberTableCopyRawTest, UnguardedCopyCrashesWhenSourceUnmapped) {
110+
EXPECT_DEATH(
111+
{
112+
long page_size = sysconf(_SC_PAGESIZE);
113+
void *page = mmap(NULL, page_size, PROT_READ | PROT_WRITE,
114+
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
115+
if (page == MAP_FAILED) {
116+
_exit(1); // treat as death via non-zero exit if mmap itself fails
117+
}
118+
jint line_number_table_size = 4;
119+
jvmtiLineNumberEntry *line_number_table =
120+
makeFakeLineNumberTable(page, line_number_table_size);
121+
122+
// Stand-in for GetLineNumberTable() handing back a bad pointer for a
123+
// corrupted/stale jmethodID: the backing memory is simply gone by
124+
// the time the copy runs.
125+
munmap(page, page_size);
126+
127+
// This mirrors the pre-fix fillJavaMethodInfo() shape verbatim: no
128+
// readability check on line_number_table before the copy.
129+
size_t bytes =
130+
(size_t)line_number_table_size * sizeof(jvmtiLineNumberEntry);
131+
void *owned_table = malloc(bytes);
132+
memcpy(owned_table, line_number_table, bytes); // <-- crashes here
133+
free(owned_table);
134+
},
135+
"");
136+
}
137+
138+
// Documents the fix: guarding the same copy with
139+
// SafeAccess::isReadableRange() (the same primitive already used to probe
140+
// class_name/method_name/method_sig in fillJavaMethodInfo) turns the crash
141+
// into a clean, detectable failure with no memory touched past the guard.
142+
TEST_F(LineNumberTableCopyTest, GuardedCopySkipsSafelyWhenSourceUnmapped) {
143+
long page_size = sysconf(_SC_PAGESIZE);
144+
void *page = mmap(NULL, page_size, PROT_READ | PROT_WRITE,
145+
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
146+
ASSERT_NE(page, MAP_FAILED);
147+
148+
jint line_number_table_size = 4;
149+
jvmtiLineNumberEntry *line_number_table =
150+
makeFakeLineNumberTable(page, line_number_table_size);
151+
152+
ASSERT_EQ(0, munmap(page, page_size));
153+
154+
size_t bytes = (size_t)line_number_table_size * sizeof(jvmtiLineNumberEntry);
155+
void *owned_table = nullptr;
156+
if (SafeAccess::isReadableRange(line_number_table, bytes)) {
157+
owned_table = malloc(bytes);
158+
memcpy(owned_table, line_number_table, bytes);
159+
}
160+
161+
EXPECT_EQ(nullptr, owned_table);
162+
free(owned_table);
163+
}
164+
165+
// Sanity check: the guard must not reject a genuinely valid table, or every
166+
// real dump would silently lose line-number info.
167+
TEST_F(LineNumberTableCopyTest, GuardedCopyStillWorksForValidSource) {
168+
jint line_number_table_size = 8;
169+
jvmtiLineNumberEntry stack_table[8];
170+
jvmtiLineNumberEntry *line_number_table =
171+
makeFakeLineNumberTable(stack_table, line_number_table_size);
172+
173+
size_t bytes = (size_t)line_number_table_size * sizeof(jvmtiLineNumberEntry);
174+
void *owned_table = nullptr;
175+
if (SafeAccess::isReadableRange(line_number_table, bytes)) {
176+
owned_table = malloc(bytes);
177+
memcpy(owned_table, line_number_table, bytes);
178+
}
179+
180+
ASSERT_NE(nullptr, owned_table);
181+
EXPECT_EQ(0, memcmp(owned_table, line_number_table, bytes));
182+
free(owned_table);
183+
}

0 commit comments

Comments
 (0)