Skip to content

Commit bc3ec7d

Browse files
authored
Merge branch 'main' into jb/malloc_profiles
2 parents d27a19e + 0c8985a commit bc3ec7d

34 files changed

Lines changed: 796 additions & 76 deletions

AGENTS.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,19 @@ table: see [doc/build/JdkUpgrades.md](doc/build/JdkUpgrades.md).
437437
- All code needs to strive to be lean in terms of resources consumption and easy to follow -
438438
do not shy away from factoring out self containing code to shorter functions with explicit name
439439

440+
## CI / Automation Script Changes
441+
- Before rewriting or replacing a script invoked from `.gitlab-ci.yml`, `.gitlab/**/*.yml`, or any CI job,
442+
run `git log -p -- <script>` (or diff against the version being replaced) and enumerate every behavior
443+
branch it has — fallbacks, env-var-driven paths, error exits. Preserve all of them unless explicitly
444+
asked to drop one; do not silently narrow behavior while refactoring.
445+
- Grep the calling `.gitlab-ci.yml` / `.gitlab/**/*.yml` jobs for how the script is invoked (env vars set,
446+
exit codes expected) and confirm the rewrite still satisfies every caller, not just the common/local case.
447+
- When removing an item from a default list or config (an antagonist, a test tag, a feature flag), cite
448+
the exact file/line proving it is inert or unused in that context before removing it — never remove
449+
based on assumption alone.
450+
- After changing a CI-invoked script, run a syntax check (e.g. `bash -n <script>`) and trace through each
451+
CI job that calls it before considering the change complete.
452+
440453
### C/C++ Code Style
441454
- **Indentation**: Match the exact indentation style of the surrounding code in each file. Do not introduce inconsistent indentation — reviewers will flag it.
442455
- **Minimal complexity**: Do not split inline logic into separate helper functions unless the helpers are reused or the original is genuinely hard to follow. Unnecessary splits add indirection without value.

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

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

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

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,16 @@
4747
static const char *const SETTING_RING[] = {NULL, "kernel", "user", "any"};
4848
static const char *const SETTING_CSTACK[] = {NULL, "no", "fp", "dwarf", "lbr"};
4949

50+
// JVM spec SS4.7.3 caps a method's bytecode (code_length) at 65535 bytes (u2),
51+
// so a well-formed LineNumberTable can never have more entries than that.
52+
// Used to sanity-bound line_number_table_size before it drives the byte-count
53+
// passed to SafeAccess::safeCopy(): if GetLineNumberTable()
54+
// returns a corrupted pointer for a stale jmethodID (see the TOCTOU race
55+
// documented in fillJavaMethodInfo below), the paired out-param size is just
56+
// as likely to be corrupted, and an implausible size should be rejected
57+
// before it is trusted to compute a byte range.
58+
static const jint MAX_LINE_NUMBER_TABLE_ENTRIES = 65535;
59+
5060
// Compute a non-negative event duration from TSC timestamps. Unsigned u64
5161
// subtraction wraps to a near-2^64 value when end < start, which can happen if
5262
// the thread migrates cores between the two TSC reads and the per-core counters
@@ -360,18 +370,50 @@ void Lookup::fillJavaMethodInfo(MethodInfo *mi, jmethodID method,
360370
// the JVMTI-allocated memory immediately. This keeps _ptr valid even
361371
// after the underlying class is unloaded.
362372
void *owned_table = nullptr;
363-
if (line_number_table_size > 0) {
373+
if (line_number_table_size > 0 &&
374+
line_number_table_size <= MAX_LINE_NUMBER_TABLE_ENTRIES) {
364375
size_t bytes = (size_t)line_number_table_size * sizeof(jvmtiLineNumberEntry);
376+
// GetLineNumberTable() is called on the same possibly-stale jmethodID
377+
// that GetMethodDeclaringClass/GetClassSignature/GetMethodName above
378+
// were probed for -- the TOCTOU race documented above (class
379+
// unloaded between sample capture and dump) applies here just as
380+
// much as to those calls, and crash telemetry already showed those
381+
// sibling calls returning JVMTI_ERROR_NONE with unmapped string
382+
// pointers despite the spec saying the returned array should be a
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.
365392
owned_table = malloc(bytes);
366393
if (owned_table != nullptr) {
367-
memcpy(owned_table, line_number_table, bytes);
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);
399+
}
368400
} else {
369401
TEST_LOG("Failed to allocate %zu bytes for line number table copy", bytes);
370402
}
403+
} else if (line_number_table_size != 0) {
404+
// A corrupted size out-param alongside a corrupted pointer is exactly
405+
// as plausible as the corrupted-pointer case above (both come from
406+
// the same GetLineNumberTable() call on the same stale jmethodID);
407+
// an implausible entry count -- including a negative one, since this
408+
// is a signed jint and a corrupted value can fall on either side of
409+
// zero -- means the pointer can't be trusted for Deallocate() either,
410+
// so treat it the same as the unreadable case.
411+
line_number_table = nullptr;
412+
Counters::increment(LINE_NUMBER_TABLE_UNREADABLE);
371413
}
372-
jvmtiError dealloc_err = jvmti->Deallocate((unsigned char *)line_number_table);
373-
if (dealloc_err != JVMTI_ERROR_NONE) {
374-
TEST_LOG("Unexpected error while deallocating linenumber table: %d", dealloc_err);
414+
if (line_number_table != nullptr) {
415+
jvmtiError dealloc_err = jvmti->Deallocate((unsigned char *)line_number_table);
416+
assert(dealloc_err == JVMTI_ERROR_NONE && "Unexpected error while deallocating linenumber table");
375417
}
376418
if (owned_table != nullptr) {
377419
mi->_line_number_table = std::make_shared<SharedLineNumberTable>(

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,11 @@ class FrameType {
7979
return (FrameTypeId)((bci >> TYPE_SHIFT) & FRAME_TYPE_MASK);
8080
}
8181

82+
// Raw pointers only exist on HotSpot (see hotspotSupport.cpp fillFrameRaw).
83+
// On other VMs an unencoded, VM-supplied bci can coincidentally have bit 30
84+
// set; that must not be mistaken for our raw-pointer encoding.
8285
static inline bool isRawPointer(int bci) {
83-
return bci > 0 && (bci & RAW_POINTER_MASK) != 0;
86+
return VM::isHotspot() && bci > 0 && (bci & RAW_POINTER_MASK) != 0;
8487
}
8588
};
8689

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

Lines changed: 125 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2025 Datadog, Inc
2+
* Copyright 2025, 2026 Datadog, Inc
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,12 +16,44 @@
1616

1717

1818
#include "safeAccess.h"
19+
#include <cstdio>
20+
#include <cstdlib>
1921
#include <signal.h>
2022
#include <ucontext.h>
2123

2224
extern "C" int safefetch32_cont(int* adr, int errValue);
2325
extern "C" int64_t safefetch64_cont(int64_t* adr, int64_t errValue);
2426

27+
// safecopy_impl copies `len` bytes from `src` to `dst` one byte at a time and
28+
// returns 1 on success. If any load from `src` faults, the signal handler
29+
// redirects the pc anywhere in [safecopy_impl, safecopy_cont) to safecopy_cont,
30+
// which returns 0. Both are leaf routines with no stack frame, so the redirect
31+
// keeps the return address intact.
32+
extern "C" int safecopy_impl(void* dst, const void* src, size_t len);
33+
extern "C" int safecopy_cont(void* dst, const void* src, size_t len);
34+
35+
#ifdef DEBUG
36+
// handle_safefetch protects safeCopy by treating any fault whose pc lands in
37+
// [safecopy_impl, safecopy_cont) as a recoverable read fault. That range is
38+
// only valid if the assembler/linker keeps safecopy_cont strictly above
39+
// safecopy_impl (they are emitted adjacently in one asm block). A future
40+
// toolchain change could in principle reorder them, collapsing the range to
41+
// empty and silently disabling fault protection. Assert the invariant once at
42+
// load time so such a regression aborts loudly at startup instead of turning
43+
// safeCopy into an unprotected crash. Debug builds only: this is a
44+
// toolchain-regression tripwire, not a runtime safety check.
45+
__attribute__((constructor))
46+
static void verify_safecopy_range() {
47+
if ((uintptr_t)safecopy_cont <= (uintptr_t)safecopy_impl) {
48+
fprintf(stderr,
49+
"FATAL: safecopy_cont (%p) is not above safecopy_impl (%p); "
50+
"safeCopy fault protection would be lost\n",
51+
(void*)safecopy_cont, (void*)safecopy_impl);
52+
abort();
53+
}
54+
}
55+
#endif // DEBUG
56+
2557
#ifdef __APPLE__
2658
#if defined(__x86_64__)
2759
#define current_pc context_rip
@@ -67,6 +99,25 @@ extern "C" int64_t safefetch64_cont(int64_t* adr, int64_t errValue);
6799
_safefetch64_cont:
68100
movq %rsi, %rax
69101
ret
102+
.globl _safecopy_impl
103+
.private_extern _safecopy_impl
104+
_safecopy_impl:
105+
xorl %ecx, %ecx
106+
Lsafecopy_loop:
107+
cmpq %rdx, %rcx
108+
jae Lsafecopy_done
109+
movzbl (%rsi,%rcx,1), %eax
110+
movb %al, (%rdi,%rcx,1)
111+
incq %rcx
112+
jmp Lsafecopy_loop
113+
Lsafecopy_done:
114+
movl $1, %eax
115+
ret
116+
.globl _safecopy_cont
117+
.private_extern _safecopy_cont
118+
_safecopy_cont:
119+
xorl %eax, %eax
120+
ret
70121
)");
71122
#else
72123
asm(R"(
@@ -95,6 +146,27 @@ extern "C" int64_t safefetch64_cont(int64_t* adr, int64_t errValue);
95146
safefetch64_cont:
96147
movq %rsi, %rax
97148
ret
149+
.globl safecopy_impl
150+
.hidden safecopy_impl
151+
.type safecopy_impl, %function
152+
safecopy_impl:
153+
xorl %ecx, %ecx
154+
.Lsafecopy_loop:
155+
cmpq %rdx, %rcx
156+
jae .Lsafecopy_done
157+
movzbl (%rsi,%rcx,1), %eax
158+
movb %al, (%rdi,%rcx,1)
159+
incq %rcx
160+
jmp .Lsafecopy_loop
161+
.Lsafecopy_done:
162+
movl $1, %eax
163+
ret
164+
.globl safecopy_cont
165+
.hidden safecopy_cont
166+
.type safecopy_cont, %function
167+
safecopy_cont:
168+
xorl %eax, %eax
169+
ret
98170
)");
99171
#endif // __APPLE__
100172
#elif defined(__aarch64__)
@@ -120,6 +192,25 @@ extern "C" int64_t safefetch64_cont(int64_t* adr, int64_t errValue);
120192
_safefetch64_cont:
121193
mov x0, x1
122194
ret
195+
.globl _safecopy_impl
196+
.private_extern _safecopy_impl
197+
_safecopy_impl:
198+
mov x3, xzr
199+
Lsafecopy_loop:
200+
cmp x3, x2
201+
b.hs Lsafecopy_done
202+
ldrb w4, [x1, x3]
203+
strb w4, [x0, x3]
204+
add x3, x3, #1
205+
b Lsafecopy_loop
206+
Lsafecopy_done:
207+
mov w0, #1
208+
ret
209+
.globl _safecopy_cont
210+
.private_extern _safecopy_cont
211+
_safecopy_cont:
212+
mov w0, #0
213+
ret
123214
)");
124215
#else
125216
asm(R"(
@@ -148,61 +239,39 @@ extern "C" int64_t safefetch64_cont(int64_t* adr, int64_t errValue);
148239
safefetch64_cont:
149240
mov x0, x1
150241
ret
242+
.globl safecopy_impl
243+
.hidden safecopy_impl
244+
.type safecopy_impl, %function
245+
safecopy_impl:
246+
mov x3, xzr
247+
.Lsafecopy_loop:
248+
cmp x3, x2
249+
b.hs .Lsafecopy_done
250+
ldrb w4, [x1, x3]
251+
strb w4, [x0, x3]
252+
add x3, x3, #1
253+
b .Lsafecopy_loop
254+
.Lsafecopy_done:
255+
mov w0, #1
256+
ret
257+
.globl safecopy_cont
258+
.hidden safecopy_cont
259+
.type safecopy_cont, %function
260+
safecopy_cont:
261+
mov w0, #0
262+
ret
151263
)");
152264
#endif
153265
#endif
154266

155267
bool SafeAccess::safeCopy(void* dst, const void* src, size_t len) {
156-
// Two-sentinel pattern (same as isReadable): a real-data word may equal
157-
// one sentinel by chance, but not both — if both fetches return their
158-
// sentinel, the access truly faulted.
159-
//
160-
// All safefetch32 loads issued here use 4-byte-aligned addresses. Pages
161-
// are 4 KiB (or 16 KiB on Apple Silicon), both divisible by 4, so an
162-
// aligned 4-byte load never spans a page boundary. The only fault
163-
// possible is when the aligned address itself lies in an unmapped page;
164-
// we never spuriously fault on an over-read past `src + len`.
165-
static const int32_t SENT_A = (int32_t)0x55AA55AA;
166-
static const int32_t SENT_B = (int32_t)0xAA55AA55;
167-
uint8_t* d = (uint8_t*)dst;
168-
const uint8_t* s = (const uint8_t*)src;
169-
size_t i = 0;
170-
171-
// Front fixup: if `src` is not 4-byte aligned, fetch at the previous
172-
// aligned address (1..3 bytes before src). That address lies in the
173-
// same 4-byte word as src — and since pages are 4-byte aligned, in
174-
// the same page as src. The leading k bytes of the fetched word lie
175-
// before the caller's range and are discarded via the +k offset; they
176-
// never reach `dst`.
177-
size_t k = (uintptr_t)s & 3u;
178-
if (k != 0 && i < len) {
179-
int32_t* aligned = (int32_t*)(s - k);
180-
int32_t v1 = safefetch32_impl(aligned, SENT_A);
181-
int32_t v2 = safefetch32_impl(aligned, SENT_B);
182-
if (v1 == SENT_A && v2 == SENT_B) {
183-
return false;
184-
}
185-
size_t take = (4 - k < len) ? (4 - k) : len;
186-
memcpy(d, ((const uint8_t*)&v1) + k, take);
187-
i = take;
188-
}
189-
190-
// Middle + tail: (s + i) is now 4-byte aligned. The final iteration may
191-
// load up to 3 over-read bytes past `src + len`, but those bytes sit in
192-
// the same 4-byte-aligned word and therefore the same page as the bytes
193-
// we actually wanted — never a fault from the over-read alone.
194-
while (i < len) {
195-
int32_t* aligned = (int32_t*)(s + i);
196-
int32_t v1 = safefetch32_impl(aligned, SENT_A);
197-
int32_t v2 = safefetch32_impl(aligned, SENT_B);
198-
if (v1 == SENT_A && v2 == SENT_B) {
199-
return false;
200-
}
201-
size_t chunk = (len - i >= 4) ? 4 : (len - i);
202-
memcpy(d + i, &v1, chunk); // memcpy from local — no UAF risk
203-
i += chunk;
204-
}
205-
return true;
268+
// The copy runs entirely inside the safecopy_impl assembly stub, which
269+
// reads `src` one byte at a time. If a load faults, handle_safefetch
270+
// redirects execution to safecopy_cont, which returns 0. Because the copy
271+
// is byte-granular it only ever touches bytes in [src, src+len) — there is
272+
// no over-read past the requested range and no alignment reasoning needed.
273+
// A fault mid-copy may leave up to len-1 bytes already written to `dst`.
274+
return safecopy_impl(dst, src, len) != 0;
206275
}
207276

208277
bool SafeAccess::handle_safefetch(int sig, void* context) {
@@ -215,6 +284,11 @@ bool SafeAccess::handle_safefetch(int sig, void* context) {
215284
} else if (pc == (uintptr_t)safefetch64_impl) {
216285
uc->current_pc = (uintptr_t)safefetch64_cont;
217286
return true;
287+
} else if (pc >= (uintptr_t)safecopy_impl && pc < (uintptr_t)safecopy_cont) {
288+
// Unlike safefetch, the faulting load can be at any pc inside the copy
289+
// loop, so match the whole [safecopy_impl, safecopy_cont) range.
290+
uc->current_pc = (uintptr_t)safecopy_cont;
291+
return true;
218292
}
219293
}
220294
return false;

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/*
22
* Copyright 2021 Andrei Pangin
3+
* Copyright 2026 Datadog, Inc
34
*
45
* Licensed under the Apache License, Version 2.0 (the "License");
56
* you may not use this file except in compliance with the License.
@@ -64,11 +65,12 @@ class SafeAccess {
6465
return safefetch64_impl(ptr, errorValue);
6566
}
6667

67-
// Copies up to len bytes from src to dst using safefetch32_impl so that a
68-
// page-unmap or repurpose of src memory during the copy does not crash the
69-
// process. Returns true on full success, false if any read faulted. dst must
70-
// have at least len bytes capacity; reads from src may over-read up to 3
71-
// bytes past src+len (over-read is also safefetch-protected).
68+
// Copies len bytes from src to dst via the safecopy_impl assembly stub so
69+
// that a page-unmap or repurpose of src memory during the copy does not
70+
// crash the process. Returns true on full success, false if any read
71+
// faulted (in which case dst may hold a partial prefix). dst must have at
72+
// least len bytes capacity. The copy is byte-granular, so it never reads
73+
// past src+len.
7274
NOINLINE
7375
static bool safeCopy(void* dst, const void* src, size_t len);
7476

0 commit comments

Comments
 (0)