Skip to content

feat(memtrack): capture allocation stacks in eBPF - #522

Draft
not-matthias wants to merge 28 commits into
mainfrom
cod-3222-add-ebpf-based-dwarffp-unwinding
Draft

feat(memtrack): capture allocation stacks in eBPF#522
not-matthias wants to merge 28 commits into
mainfrom
cod-3222-add-ebpf-based-dwarffp-unwinding

Conversation

@not-matthias

@not-matthias not-matthias commented Aug 28, 2026

Copy link
Copy Markdown
Member

Adds raw allocation-stack capture to memtrack and ships everything needed for off-box unwinding.

At every allocator entry the eBPF program copies the caller's user stack (chunked, budget-capped, FNV-1a-hashed) and walks frame pointers in-kernel. Definitions are deduplicated by hash in-kernel: one StackDefinition event (raw bytes, registers, FP chain) per distinct stack, referenced by a stack_hash on allocation events. Everything flows through the ordinary MemtrackArtifact event stream; unwinding and symbolication happen server-side later.

After a capture-enabled run, the runner decodes the artifact and reuses the perf walltime machinery to dump per-module symbols, unwind data, and debug info into the profile folder, plus a memory_metadata.json with per-pid process names.

Refs COD-3222

Copy the caller's user stack in chunks at allocator entry and fold an
FNV-1a digest over it in the kernel. The digest rides on the allocation
event as stack_hash; the copied bytes, a DWARF-numbered register
snapshot and a frame-pointer walk are emitted once per distinct digest
on a dedicated ring buffer, so unwinding and symbolication can happen
offline.

Capture stays off until userspace sets the rodata toggle, so allocator
probes are unchanged by default.

Refs COD-3222
Add the userspace half of allocation stack capture: env-driven
configuration, stack-definition ring parsing, loss counters, per-pid
module mapping tracking, a folding recorder that deduplicates
definitions and counts occurrences, and the report it produces.

Nothing constructs these yet; the tracker wiring follows.

Refs COD-3222
Wire the capture rodata and map sizing into skeleton load, poll the
stack-definition ring alongside the event ring, and expose the loss
counters and frame-pointer chains. The attach worker snapshots module
mappings while it holds a process stopped, which is the only point they
are guaranteed readable.

Guard the lifecycle: finishing with a live session would block forever
on the recorder, and a second spawn would leave the capture rings
undrained, so both now fail with a descriptive error.

With capture disabled the ring buffer and frame-pointer map shrink to
the allocator minimum rather than reserving tens of MiB.

Refs COD-3222
Add a fixture with two non-inlinable malloc call paths and privileged
tests over it: distinct call paths get distinct identities with module
mappings for the binary and libc, repeated calls deduplicate, and the
default-off path still reports allocations.

Two cases guard failure modes the default budget cannot reach. The
maximum copy budget is the only configuration that exercises the
verifier's instruction limit, since the frozen rodata makes the copy
and hash loops scale with the configured size. Shrinking the
frame-pointer map to one slot proves exhaustion costs only the fallback
chain, never an allocation event.

Refs COD-3222
@codspeed-hq

codspeed-hq Bot commented Aug 28, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 36.4%

⚠️ Unknown Walltime execution environment detected

Using the Walltime instrument on standard Hosted Runners will lead to inconsistent data.

For the most accurate results, we recommend using CodSpeed Macro Runners: bare-metal machines fine-tuned for performance measurement consistency.

❌ 3 regressed benchmarks
✅ 14 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
WallTime encode_events_realistic[16] 122.2 ms 212.1 ms -42.41%
WallTime encode_events_realistic[8] 143.9 ms 231.4 ms -37.8%
WallTime encode_events_realistic[4] 216.4 ms 301.3 ms -28.18%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing cod-3222-add-ebpf-based-dwarffp-unwinding (83de75e) with main (e89df29)

Open in CodSpeed

@not-matthias
not-matthias force-pushed the cod-3222-add-ebpf-based-dwarffp-unwinding branch from 573a77e to 83de75e Compare August 28, 2026 15:10
The symbol, unwind-data and debug-info extraction is not perf-specific: it
turns a set of mapped ELF modules into the deduplicated keyed artifacts a
profile references, whatever discovered the mappings. Memory mode needs the
same pipeline, so it moves out of wall_time/profiler/perf into
executor/shared/module_artifacts.
Memory mode needs the same per-pid module references walltime writes, so the
five artifact fields move into a flattened `ModuleArtifacts` shared by both
formats; walltime's JSON is unchanged, asserted against output captured from
the flat struct.

Flattening buffers those fields through serde's `Content`, which unlike
serde_json's direct deserializer cannot parse a string JSON key into a pid, so
pid-keyed maps get an explicit key-parsing helper.
Allocation stacks are raw addresses, so resolving them off-box needs the
module geometry perf gets from PERF_RECORD_MMAP2. No single hook provides it:
security_mmap_file has the file but runs before the VMA exists, and
perf_event_mmap has the addresses but cannot resolve a path. So an LSM program
caches the path once per inode and a perf_event_mmap fentry emits inode-keyed
address records, joined in userspace while the maps are still live.

Path resolution is only reachable from an LSM program at all, and only above
5.11 (bpf_d_path on the sleepable hook) or 6.12 (the bpf_path_d_path kfunc),
with the bpf LSM active. MappingSupport probes both, and when neither holds
stack capture is turned off rather than shipping stacks nothing can attribute.
Memory mode now turns the mappings memtrack recorded into the same keyed
unwind_data/symbols.map files walltime writes, plus a memtrack.metadata
referencing them per pid, so allocation stacks can be unwound off-box.

Each mapping's inode is rechecked against the path before its ELF is read: BPF
cannot produce a build id, so the recorded (dev, ino) is what proves the file
on disk is still the one that was mapped rather than a rebuilt binary whose
eh_frame would be bound to the wrong addresses.
Box the stack payload so ordinary events do not carry its three vector fields. Keep the stack fields flattened with serde so existing artifacts remain compatible.
Include the teardown module referenced by the tracked memtrack module and apply the formatter changes required by CI.
Skip capture assertions on kernels without BPF LSM path resolution, matching the runtime capability gate.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant