fix(live-debugger): fix probe hook use-after-free and tags leak on config removal#4036
fix(live-debugger): fix probe hook use-after-free and tags leak on config removal#4036Leiyks wants to merge 4 commits into
Conversation
…use-after-free The live debugger tracks installed probe hooks in `spans_map`, but keyed it inconsistently: apply_config and remove_config used the probe's `id` field, while the DI-enable reinstall path used the `active` map key (the config_id). `active` is keyed by config_id, which is unique per remote-config path, whereas probe.id is not: two distinct configs can carry the same probe id. When they do (or when a probe is installed via different paths), the second install overwrites the probe.id-keyed spans_map entry, orphaning the first hook. Removing a config then tears down the wrong hook (or none) and frees the parsed-config box while an orphaned hook still borrows its strings. The orphan's first fire afterwards reads the freed probe id in ddog_debugger_diagnostics_create_unboxed -> use-after-free. Only valgrind observes it (plain runs read freed-but-intact bytes), which is why it surfaced intermittently as a LEAKED test in test_extension_ci. Key spans_map by config_id everywhere (apply_config, remove_config, and the already-correct reinstall path) so every config's hook is tracked and removed independently. Adds tests/ext/live-debugger/debugger_remove_shared_probe_id.phpt, which reproduces the UAF (two configs sharing a probe id) and is clean with the fix. Verified under valgrind on PHP 7.1 and 8.3: the reproducer leaks before the fix and passes after; the whole live-debugger suite is clean.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2d4ab496dd
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The regression test used a distinct `tags` entry per config only to give the two configs different remote-config paths. But a non-empty `tags` makes probe.into() allocate an FFI CharSliceVec that the C side never frees (a separate latent leak for tagged probes), which LeakSanitizer flags in the ASAN 'multiple observers' job (32 bytes / 2 objects) and breaks the test's expected output. Use an ignored unknown field instead: same two distinct config paths, same probe id, no per-config allocation -> no unrelated LSan noise. Verified under valgrind (clean) and the two configs still install at distinct paths with the shared id.
probe.into() heap-allocates a CharSliceVec for the probe `tags`, but the C side (def->probe) only freed the nested span-decoration / log allocations in dd_probe_dtor -- never the tags vec -- so every probe carrying tags leaked it (LeakSanitizer: 32 bytes / 2 objects in the ASAN 'multiple observers' job). Add ddog_drop_probe, which consumes the FFI probe by value so its drop glue frees the tags CharSliceVec together with the nested span-decoration / log allocations, and call it from dd_probe_dtor in place of the piecemeal drops. Cleanup is now a single, consistent operation covering every FFI-owned field. Restore the shared-probe-id regression test to distinguish its two configs by `tags`, so it also exercises this path (a tags leak would resurface under LSan). Verified on PHP 8.3: the live-debugger suite is clean under valgrind (no UAF, no double-free), and valgrind --leak-check=full on a tagged probe shows 0 bytes definitely lost with no CharSliceVec leak record.
…; trim comments Address Codex review: - apply_config: remove any hook already installed for a config id before installing the replacement, so an in-place Add (Occupied entry) can't orphan the previous hook into the dropped parsed config. - regression test: await both colliding probes (2) before removing, so it actually exercises the shared-probe-id collision. Also condense the comments added in this PR.
Benchmarks [ tracer ]Benchmark execution time: 2026-07-08 16:34:27 Comparing candidate commit cef3cd0 in PR branch Found 0 performance improvements and 2 performance regressions! Performance is the same for 192 metrics, 0 unstable metrics.
|
Two related live-debugger probe-lifetime bugs surfaced from a flaky
LEAKEDintest_extension_ciondebugger_enable_dynamic_config.phpt.1. Use-after-free (the original failure)
spans_map(installed hook → id) was keyed inconsistently:apply_config/remove_configusedprobe.id, while the DI-enable reinstall path usedconfig_id(theactivekey).config_idis unique per RC path;probe.idis not. When a probe's hook is tracked under one key but looked up for removal under another (or two configs share aprobe.id), removal misses → the hook is orphaned → its parsed-config box is freed → the orphan's next fire reads the freedprobe.idinddog_debugger_diagnostics_create_unboxed→ UAF. Only valgrind observes it (plain runs read freed-but-intact bytes), hence the intermittentLEAKED. Reproduced identically on PHP 7.1 and 8.3 (not version-specific).Fix: key
spans_mapbyconfig_ideverywhere, so every config's hook is tracked and removed independently.2. Probe
tagsleakprobe.into()heap-allocates aCharSliceVecfor the probetags, butdd_probe_dtoronly freed the nested span-decoration / log allocations — never the tags vec. So any probe carrying tags leaked it (LeakSanitizer: 32 B / 2 objects in the ASAN "multiple observers" job).Fix: add
ddog_drop_probe, which consumes the FFI probe by value so its drop glue frees the tagsCharSliceVectogether with the nested allocations;dd_probe_dtorcalls it in place of the piecemeal drops (one consistent cleanup).Test
tests/ext/live-debugger/debugger_remove_shared_probe_id.phptinstalls two configs sharing aprobe.id(with distincttags), removes both without firing, then calls the target — exercising both the UAF (orphaned hook) and the tags allocation. It leaks/UAFs before the fixes and is clean after.Verification (under valgrind, PHP 7.1 & 8.3)
valgrind --leak-check=fullon a tagged probe: 0 bytes definitely lost, noCharSliceVecleak record.