Skip to content

Commit f4ee43a

Browse files
committed
fix(profiler): close TOCTOU race in line-number-table copy with safeCopy()
isReadableRange() + memcpy still races if the probed memory changes mapping between the check and the copy. Use SafeAccess::safeCopy() instead, which fault-protects each read as it happens.
1 parent 6c0bae9 commit f4ee43a

2 files changed

Lines changed: 40 additions & 28 deletions

File tree

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

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ static const char *const SETTING_CSTACK[] = {NULL, "no", "fp", "dwarf", "lbr"};
5050
// JVM spec SS4.7.3 caps a method's bytecode (code_length) at 65535 bytes (u2),
5151
// so a well-formed LineNumberTable can never have more entries than that.
5252
// Used to sanity-bound line_number_table_size before it drives the byte-count
53-
// passed to SafeAccess::isReadableRange()/memcpy(): if GetLineNumberTable()
53+
// passed to SafeAccess::safeCopy(): if GetLineNumberTable()
5454
// returns a corrupted pointer for a stale jmethodID (see the TOCTOU race
5555
// documented in fillJavaMethodInfo below), the paired out-param size is just
5656
// as likely to be corrupted, and an implausible size should be rejected
@@ -380,19 +380,25 @@ void Lookup::fillJavaMethodInfo(MethodInfo *mi, jmethodID method,
380380
// much as to those calls, and crash telemetry already showed those
381381
// sibling calls returning JVMTI_ERROR_NONE with unmapped string
382382
// pointers despite the spec saying the returned array should be a
383-
// fresh, caller-owned allocation. Nothing about this call guarantees
384-
// it is exempt from the same failure mode, so probe before copying
385-
// rather than assume the pointer is safe to dereference.
386-
if (SafeAccess::isReadableRange(line_number_table, bytes)) {
387-
owned_table = malloc(bytes);
388-
if (owned_table != nullptr) {
389-
memcpy(owned_table, line_number_table, bytes);
390-
} else {
391-
TEST_LOG("Failed to allocate %zu bytes for line number table copy", bytes);
383+
// fresh, caller-owned allocation. A genuinely-valid returned table is
384+
// fully decoupled from jmethodID lifetime per the JVMTI spec and
385+
// can't be invalidated later by class unload; the actual risk is a
386+
// corrupted pointer that happens to alias other live memory at
387+
// check-time and stops being mapped moments later. A separate
388+
// isReadableRange() probe followed by a plain memcpy() would still
389+
// race that window, so copy via safeCopy() instead: it fault-protects
390+
// each read as it happens rather than trusting a point-in-time check
391+
// before an unprotected copy.
392+
owned_table = malloc(bytes);
393+
if (owned_table != nullptr) {
394+
if (!SafeAccess::safeCopy(owned_table, line_number_table, bytes)) {
395+
free(owned_table);
396+
owned_table = nullptr;
397+
line_number_table = nullptr; // make sure the invalid address is not used for jvmti->Deallocate
398+
Counters::increment(LINE_NUMBER_TABLE_UNREADABLE);
392399
}
393400
} else {
394-
line_number_table = nullptr; // make sure the invalid address is not used for jvmti->Deallocate
395-
Counters::increment(LINE_NUMBER_TABLE_UNREADABLE);
401+
TEST_LOG("Failed to allocate %zu bytes for line number table copy", bytes);
396402
}
397403
} else if (line_number_table_size != 0) {
398404
// A corrupted size out-param alongside a corrupted pointer is exactly

ddprof-lib/src/test/cpp/lineNumberTableCopy_ut.cpp

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,14 @@
1818
//
1919
// memcpy(owned_table, line_number_table, bytes);
2020
//
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.)
21+
// (flightRecorder.cpp now guards this copy with SafeAccess::safeCopy() -- see
22+
// below -- so the line numbers of the unguarded version are no longer present
23+
// in the file; this file's tests reproduce that removed pattern. An earlier
24+
// version of the fix used a separate SafeAccess::isReadableRange() probe
25+
// followed by a plain memcpy(), but that still left a check-then-use race:
26+
// nothing stops the probed memory from becoming invalid in the gap between
27+
// the probe and the copy. safeCopy() closes that gap by fault-protecting
28+
// each read as it happens instead of trusting a point-in-time check.)
2429
//
2530
// Unlike the class_name/method_name/method_sig strings returned by the
2631
// preceding JVMTI calls (which ARE probed with SafeAccess::isReadableRange
@@ -152,10 +157,10 @@ TEST(LineNumberTableCopyRawTest, UnguardedCopyCrashesWhenSourceUnmapped) {
152157
"");
153158
}
154159

155-
// Documents the fix: guarding the same copy with
156-
// SafeAccess::isReadableRange() (the same primitive already used to probe
157-
// class_name/method_name/method_sig in fillJavaMethodInfo) turns the crash
158-
// into a clean, detectable failure with no memory touched past the guard.
160+
// Documents the fix: copying via SafeAccess::safeCopy() (which fault-protects
161+
// each read as it happens, rather than a point-in-time isReadableRange()
162+
// probe followed by a separate, unprotected memcpy()) turns the crash into a
163+
// clean, detectable failure with no memory touched past the fault.
159164
TEST_F(LineNumberTableCopyTest, GuardedCopySkipsSafelyWhenSourceUnmapped) {
160165
long page_size = sysconf(_SC_PAGESIZE);
161166
void *page = mmap(NULL, page_size, PROT_READ | PROT_WRITE,
@@ -169,14 +174,14 @@ TEST_F(LineNumberTableCopyTest, GuardedCopySkipsSafelyWhenSourceUnmapped) {
169174
ASSERT_EQ(0, munmap(page, page_size));
170175

171176
size_t bytes = (size_t)line_number_table_size * sizeof(jvmtiLineNumberEntry);
172-
void *owned_table = nullptr;
173-
if (SafeAccess::isReadableRange(line_number_table, bytes)) {
174-
owned_table = malloc(bytes);
175-
memcpy(owned_table, line_number_table, bytes);
177+
void *owned_table = malloc(bytes);
178+
ASSERT_NE(owned_table, nullptr);
179+
if (!SafeAccess::safeCopy(owned_table, line_number_table, bytes)) {
180+
free(owned_table);
181+
owned_table = nullptr;
176182
}
177183

178184
EXPECT_EQ(nullptr, owned_table);
179-
free(owned_table);
180185
}
181186

182187
// Sanity check: the guard must not reject a genuinely valid table, or every
@@ -188,10 +193,11 @@ TEST_F(LineNumberTableCopyTest, GuardedCopyStillWorksForValidSource) {
188193
makeFakeLineNumberTable(stack_table, line_number_table_size);
189194

190195
size_t bytes = (size_t)line_number_table_size * sizeof(jvmtiLineNumberEntry);
191-
void *owned_table = nullptr;
192-
if (SafeAccess::isReadableRange(line_number_table, bytes)) {
193-
owned_table = malloc(bytes);
194-
memcpy(owned_table, line_number_table, bytes);
196+
void *owned_table = malloc(bytes);
197+
ASSERT_NE(owned_table, nullptr);
198+
if (!SafeAccess::safeCopy(owned_table, line_number_table, bytes)) {
199+
free(owned_table);
200+
owned_table = nullptr;
195201
}
196202

197203
ASSERT_NE(nullptr, owned_table);

0 commit comments

Comments
 (0)