From 52e919ee9e13be95a9ab813cc25c00412a84d18a Mon Sep 17 00:00:00 2001 From: Scott Gerring Date: Fri, 10 Jul 2026 13:59:01 +0200 Subject: [PATCH 1/4] chore(heap-profiling): if a user doesnt opt into live heap, ensure the USDT does not land in the process --- libdd-profiling-heap-sampler/include/datadog/heap/probes.h | 6 ++++++ libdd-profiling-heap-sampler/src/allocation_freed.c | 2 ++ libdd-profiling-heap-sampler/src/probes.c | 2 ++ 3 files changed, 10 insertions(+) diff --git a/libdd-profiling-heap-sampler/include/datadog/heap/probes.h b/libdd-profiling-heap-sampler/include/datadog/heap/probes.h index a67066ecc0..e762a46742 100644 --- a/libdd-profiling-heap-sampler/include/datadog/heap/probes.h +++ b/libdd-profiling-heap-sampler/include/datadog/heap/probes.h @@ -31,10 +31,16 @@ */ void dd_probe_alloc(void *user, uint64_t size, uint64_t weight); +#if DD_HEAP_LIVE_TRACKING /* * Emits the `ddheap:free` USDT. * ptr - user-visible pointer being freed + * + * Only available when compiled with live-heap tracking. The absence of + * the `ddheap:free` note in .note.stapsdt signals to external profilers + * that this binary does not support live-heap correlation. */ void dd_probe_free(void *ptr); +#endif #endif diff --git a/libdd-profiling-heap-sampler/src/allocation_freed.c b/libdd-profiling-heap-sampler/src/allocation_freed.c index b8f6b89f0f..a4407a84c4 100644 --- a/libdd-profiling-heap-sampler/src/allocation_freed.c +++ b/libdd-profiling-heap-sampler/src/allocation_freed.c @@ -22,7 +22,9 @@ dd_alloc_freed_t dd_allocation_freed_slow(void *ptr, void *raw, size_t size, size_t alignment) { /* Fire with the user-visible pointer, matching what was reported at alloc * time, so the profiler can correlate the two events by address. */ +#if DD_HEAP_LIVE_TRACKING dd_probe_free(ptr); +#endif dd_alloc_freed_t out = { /* Return the raw pointer so the caller passes the real allocation base diff --git a/libdd-profiling-heap-sampler/src/probes.c b/libdd-profiling-heap-sampler/src/probes.c index b4bc5612a6..59b2ae64be 100644 --- a/libdd-profiling-heap-sampler/src/probes.c +++ b/libdd-profiling-heap-sampler/src/probes.c @@ -28,8 +28,10 @@ void dd_probe_alloc(void *user, uint64_t size, uint64_t weight) { errno = saved_errno; } +#if DD_HEAP_LIVE_TRACKING void dd_probe_free(void *ptr) { int saved_errno = errno; USDT(ddheap, free, ptr); errno = saved_errno; } +#endif From 4680d1d53373c94eb8a643487d22ca10c44fa341 Mon Sep 17 00:00:00 2001 From: Scott Gerring Date: Fri, 10 Jul 2026 16:44:08 +0200 Subject: [PATCH 2/4] address clanker review comments --- .../include/datadog/heap/probes.h | 9 ++++----- libdd-profiling-heap-sampler/src/allocation_freed.c | 9 +++++---- libdd-profiling-heap-sampler/src/probes.c | 7 +++++-- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/libdd-profiling-heap-sampler/include/datadog/heap/probes.h b/libdd-profiling-heap-sampler/include/datadog/heap/probes.h index e762a46742..28913194bc 100644 --- a/libdd-profiling-heap-sampler/include/datadog/heap/probes.h +++ b/libdd-profiling-heap-sampler/include/datadog/heap/probes.h @@ -31,16 +31,15 @@ */ void dd_probe_alloc(void *user, uint64_t size, uint64_t weight); -#if DD_HEAP_LIVE_TRACKING /* * Emits the `ddheap:free` USDT. * ptr - user-visible pointer being freed * - * Only available when compiled with live-heap tracking. The absence of - * the `ddheap:free` note in .note.stapsdt signals to external profilers - * that this binary does not support live-heap correlation. + * The symbol always exists, but the USDT is only emitted when compiled + * with live-heap tracking. The absence of the `ddheap:free` note in + * .note.stapsdt signals to external profilers that this binary does not + * support live-heap correlation. */ void dd_probe_free(void *ptr); -#endif #endif diff --git a/libdd-profiling-heap-sampler/src/allocation_freed.c b/libdd-profiling-heap-sampler/src/allocation_freed.c index a4407a84c4..33355da31b 100644 --- a/libdd-profiling-heap-sampler/src/allocation_freed.c +++ b/libdd-profiling-heap-sampler/src/allocation_freed.c @@ -12,7 +12,8 @@ * dd_sample_flag_check_and_clear confirmed that ptr carries the sample flag, * meaning this allocation was previously sampled. * - * Fires the ddheap:free USDT with the user-visible pointer, then returns + * Fires the ddheap:free USDT (when live-heap tracking is enabled) with the + * user-visible pointer, then returns * the raw pointer and adjusted size that the caller must forward to the * real deallocator. On x86-64 the size grows by DD_HEADER_BYTES to cover * the header that was reserved at allocation time; on arm64 the size is @@ -21,10 +22,10 @@ dd_alloc_freed_t dd_allocation_freed_slow(void *ptr, void *raw, size_t size, size_t alignment) { /* Fire with the user-visible pointer, matching what was reported at alloc - * time, so the profiler can correlate the two events by address. */ -#if DD_HEAP_LIVE_TRACKING + * time, so the profiler can correlate the two events by address. Safe to + * call unconditionally: dd_probe_free is a no-op USDT-wise when live-heap + * tracking is off. */ dd_probe_free(ptr); -#endif dd_alloc_freed_t out = { /* Return the raw pointer so the caller passes the real allocation base diff --git a/libdd-profiling-heap-sampler/src/probes.c b/libdd-profiling-heap-sampler/src/probes.c index 59b2ae64be..b82930fe4b 100644 --- a/libdd-profiling-heap-sampler/src/probes.c +++ b/libdd-profiling-heap-sampler/src/probes.c @@ -18,6 +18,7 @@ */ #include +#include #include @@ -28,10 +29,12 @@ void dd_probe_alloc(void *user, uint64_t size, uint64_t weight) { errno = saved_errno; } -#if DD_HEAP_LIVE_TRACKING void dd_probe_free(void *ptr) { +#if DD_HEAP_LIVE_TRACKING int saved_errno = errno; USDT(ddheap, free, ptr); errno = saved_errno; -} +#else + (void)ptr; #endif +} From 4556dade57039b690f135332026278ace9f1f3f4 Mon Sep 17 00:00:00 2001 From: Scott Gerring Date: Mon, 13 Jul 2026 15:47:20 +0200 Subject: [PATCH 3/4] add free path test --- libdd-profiling-heap-sampler/src/lib.rs | 32 +++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/libdd-profiling-heap-sampler/src/lib.rs b/libdd-profiling-heap-sampler/src/lib.rs index fd21af1447..7015e3bd56 100644 --- a/libdd-profiling-heap-sampler/src/lib.rs +++ b/libdd-profiling-heap-sampler/src/lib.rs @@ -216,6 +216,38 @@ mod tests { } } + // With live-heap tracking compiled out, dd_allocation_freed is a + // straight passthrough: it never inspects the sample-flag header and + // never fires ddheap:free. Verify this even when the underlying + // memory happens to contain the magic pattern that *would* trigger + // the slow path if live-heap were enabled. + #[cfg(not(feature = "live-heap"))] + #[test] + fn freed_is_passthrough_when_live_heap_disabled() { + const MAGIC: u64 = 0xfab1eddec0dedca7; + const HEADER_BYTES: usize = 16; + + // Set up a buffer with the magic header so that, if the flag-check + // were compiled in, it would consider this allocation sampled. + let mut buf = vec![0u8; 128]; + let base = buf.as_mut_ptr() as usize; + let user_addr = base + HEADER_BYTES; + + // Stamp magic at ptr - HEADER_BYTES + buf[0..8].copy_from_slice(&MAGIC.to_ne_bytes()); + // Stamp a plausible offset + buf[8..16].copy_from_slice(&(HEADER_BYTES as u64).to_ne_bytes()); + + let ptr = user_addr as *mut c_void; + unsafe { + let freed = dd_allocation_freed(ptr, 64, 8); + // Without live-heap the pointer is returned unchanged (no + // flag-check, no raw-pointer recovery). + assert_eq!(freed.ptr, ptr); + assert_eq!(freed.size, 64); + } + } + #[test] fn zero_sampling_interval_disables_sampling() { let mut tl = dd_tl_state_t { From b92fc02888772172cf1f548c84600850c41b9714 Mon Sep 17 00:00:00 2001 From: Scott Gerring Date: Wed, 15 Jul 2026 12:28:13 +0200 Subject: [PATCH 4/4] test for free USDT being emitted need to be gated on the free USDT being emitted --- libdd-profiling-heap-gotter-ffi/Cargo.toml | 2 ++ .../tests/usdt_notes.rs | 24 ++++++++++++-- .../src/usdt_check.rs | 33 ++++++++++++++----- 3 files changed, 48 insertions(+), 11 deletions(-) diff --git a/libdd-profiling-heap-gotter-ffi/Cargo.toml b/libdd-profiling-heap-gotter-ffi/Cargo.toml index 062a64c20b..3b594d97e5 100644 --- a/libdd-profiling-heap-gotter-ffi/Cargo.toml +++ b/libdd-profiling-heap-gotter-ffi/Cargo.toml @@ -18,6 +18,8 @@ default = ["cbindgen"] cbindgen = ["build_common/cbindgen", "libdd-common-ffi/cbindgen"] # See libdd-profiling-heap-gotter's test-support feature. Not for production use. test-support = ["libdd-profiling-heap-gotter/test-support"] +# Forward live-heap (retained-heap) tracking down to the sampler +live-heap = ["libdd-profiling-heap-gotter/live-heap"] [build-dependencies] build_common = { path = "../build-common" } diff --git a/libdd-profiling-heap-gotter-ffi/tests/usdt_notes.rs b/libdd-profiling-heap-gotter-ffi/tests/usdt_notes.rs index 33213c2eb4..219a0d9d9d 100644 --- a/libdd-profiling-heap-gotter-ffi/tests/usdt_notes.rs +++ b/libdd-profiling-heap-gotter-ffi/tests/usdt_notes.rs @@ -7,6 +7,13 @@ //! attached consumer fire twice, so this guards the "one note per probe" //! invariant documented in the sampler's `probes.c`. //! +//! Split into two tests because the two probes have different lifetimes: +//! `ddheap:alloc` is expected in every build, while `ddheap:free` only +//! exists when compiled with live-heap tracking (the `live-heap` feature) — +//! its absence otherwise is intentional, so it's covered by its own +//! feature-gated test rather than folded into a single "all probes present" +//! assertion. +//! //! Inspecting the linked cdylib (rather than running the check in-process) //! validates the real shipped artifact. Mirrors //! libdd-otel-thread-ctx-ffi/tests/elf_properties.rs. @@ -25,9 +32,22 @@ fn cdylib_path() -> PathBuf { .join("liblibdd_profiling_heap_gotter_ffi.so") } +/// `ddheap:alloc` is unconditional: every build samples allocations, so its +/// note must always be present exactly once. +#[test] +#[cfg_attr(miri, ignore)] +fn ddheap_alloc_probe_has_one_note() { + let path = cdylib_path(); + libdd_profiling_heap_sampler::usdt_check::check_usdt_probes_in(&path, &["alloc"]).unwrap(); +} + +/// `ddheap:free` only exists when built with live-heap tracking. Gated on the +/// `live-heap` feature (forwarded to the sampler crate) so this test only +/// runs, and only asserts the note is present, in builds that opt in. +#[cfg(feature = "live-heap")] #[test] #[cfg_attr(miri, ignore)] -fn ddheap_probes_have_one_note_each() { +fn ddheap_free_probe_has_one_note() { let path = cdylib_path(); - libdd_profiling_heap_sampler::usdt_check::check_usdt_probes_in(&path).unwrap(); + libdd_profiling_heap_sampler::usdt_check::check_usdt_probes_in(&path, &["free"]).unwrap(); } diff --git a/libdd-profiling-heap-sampler/src/usdt_check.rs b/libdd-profiling-heap-sampler/src/usdt_check.rs index fbf35ad9da..d7da14109c 100644 --- a/libdd-profiling-heap-sampler/src/usdt_check.rs +++ b/libdd-profiling-heap-sampler/src/usdt_check.rs @@ -10,6 +10,13 @@ //! note; a regression (e.g. `static inline` + bindgen's `wrap_static_fns`, or //! LTO inlining across TUs) could duplicate the entry. This check catches that. //! +//! `ddheap:alloc` is expected in every build. `ddheap:free` is only expected +//! when compiled with live-heap tracking (the `live-heap` feature): its +//! absence is how external profilers detect that a binary doesn't support +//! live-heap correlation (see `probes.h`), so callers must pass the probe +//! list that matches the build under test rather than assume both are always +//! present. +//! //! Call [`sanity_check`] from within a shared object or statically linked //! executable, or point [`check_usdt_probes_in`] at a built artifact. //! @@ -23,23 +30,28 @@ use std::path::{Path, PathBuf}; /// USDT provider name emitted by `probes.c` (`USDT(ddheap, ...)`). const PROVIDER: &str = "ddheap"; -/// Probes we expect, each exactly once. -const EXPECTED_PROBES: &[&str] = &["alloc", "free"]; /// As [`sanity_check`], but takes the object file as an argument. Useful for a -/// test setting where the test code is separate from the artifact to validate. -pub fn check_usdt_probes_in(path: &Path) -> anyhow::Result<()> { +/// test setting where the test code is separate from the artifact to +/// validate. `expected_probes` lists the probe names (without the `ddheap:` +/// provider prefix) that must each appear exactly once. +pub fn check_usdt_probes_in(path: &Path, expected_probes: &[&str]) -> anyhow::Result<()> { let data = std::fs::read(path).with_context(|| format!("failed to read {}", path.display()))?; let elf = ElfBytes::::minimal_parse(&data) .with_context(|| format!("failed to parse ELF at {}", path.display()))?; - check_one_note_per_probe(&elf)?; + check_one_note_per_probe(&elf, expected_probes)?; Ok(()) } /// Check that the current running module carries exactly one `.note.stapsdt` -/// entry per ddheap probe. +/// entry per ddheap probe expected in this build (`alloc` always, `free` +/// only when the `live-heap` feature is enabled). pub fn sanity_check() -> anyhow::Result<()> { - check_usdt_probes_in(&own_elf_path()?) + let mut expected = vec!["alloc"]; + if cfg!(feature = "live-heap") { + expected.push("free"); + } + check_usdt_probes_in(&own_elf_path()?, &expected) } /// Locate the current running module (shared or not) via `/proc/self/maps`. @@ -79,7 +91,10 @@ fn own_elf_path() -> anyhow::Result { } /// Parse `.note.stapsdt` and assert each expected probe appears exactly once. -fn check_one_note_per_probe(elf: &ElfBytes<'_, AnyEndian>) -> anyhow::Result<()> { +fn check_one_note_per_probe( + elf: &ElfBytes<'_, AnyEndian>, + expected_probes: &[&str], +) -> anyhow::Result<()> { let shdr = elf .section_header_by_name(".note.stapsdt") .context("failed to read section headers")? @@ -118,7 +133,7 @@ fn check_one_note_per_probe(elf: &ElfBytes<'_, AnyEndian>) -> anyhow::Result<()> } } - for &probe in EXPECTED_PROBES { + for &probe in expected_probes { match counts.get(probe).copied().unwrap_or(0) { 1 => {} 0 => bail!("USDT probe '{PROVIDER}:{probe}' has no .note.stapsdt entry"),