feat(memtrack): support running the memory instrument in the macro-agent sandbox#443
Conversation
Switch the allocation uprobes/uretprobes from perf_event_open-based attach to uprobe_multi links, and the sched_process_fork tracepoint from a perf tracepoint to tp_btf. Both new paths attach through the bpf() syscall, which a delegated BPF token can authorize; perf_event_open is gated on CAP_PERFMON in the init user namespace and cannot be delegated into the sandbox. This lets the memory instrument run inside the macro-agent sandbox as an unprivileged workload. The eBPF program logic is unchanged (same maps, ringbuf, PID tracking, and fork following); only the attach mechanism and the matching SEC() annotations change. Refs COD-3047 Co-Authored-By: Claude <noreply@anthropic.com>
Attaching by symbol name via libbpf's uprobe_multi `syms` resolution failed with ENOENT for libc allocator symbols, so no probes bound and the tracker captured zero events. Resolve each symbol to its file offset from the ELF tables ourselves (as the previous perf-based attach already did) and pass it via `offsets`, which skips libbpf's name lookup. Verified on a real host run: a small allocation workload now yields thousands of events instead of zero. Refs COD-3047 Co-Authored-By: Claude <noreply@anthropic.com>
…mespace When memtrack runs inside a PID namespace (the macro-agent sandbox), the PIDs it registers are namespace-local while eBPF sees global PIDs, so is_tracked() never matched and zero events were captured. Read PIDs relative to the tracker's own PID namespace: pass its (dev, ino) into the programs and use bpf_get_ns_current_pid_tgid for the current task and a CO-RE walk of task->thread_pid->numbers[level] for the fork tracepoint's parent/child. When the tracker is in the init namespace this resolves to global PIDs, preserving the previous behavior. Verified on the macro-runner: an allocation workload tracked inside the sandbox now yields thousands of events (was zero), and the host/init-ns baseline is unchanged. Refs COD-3047 Co-Authored-By: Claude <noreply@anthropic.com>
…lege The memory executor required root or memtrack file-capabilities and would otherwise bail (and grant_privileges prompted for sudo to run setcap). Inside the macro-agent sandbox memtrack runs unprivileged with neither; its eBPF is authorized by a delegated BPF token instead. Treat a live LIBBPF_BPF_TOKEN_PATH as a satisfied privilege in ensure_privileges and privilege_status, and make grant_privileges a no-op when a token is present so it neither fails nor prompts. Verified on the macro-runner: `codspeed run -m memory` on an instrumented bench completes inside the sandbox and records memory results. Refs COD-3047 Co-Authored-By: Claude <noreply@anthropic.com>
Greptile SummaryThis PR makes memtrack work with the macro-agent sandbox. The main changes are:
Confidence Score: 4/5The sandbox memory path can produce incomplete traces until the remaining global-PID gates are fixed.
crates/memtrack/src/ebpf/c/memtrack.bpf.c, crates/memtrack/src/ebpf/memtrack.rs Important Files Changed
|
| __u64 tid = bpf_get_current_pid_tgid(); \ | ||
| __u32 pid = tid >> 32; \ |
There was a problem hiding this comment.
When memtrack runs inside the sandbox, userspace registers the namespace-local PID in tracked_pids, but this realloc entry probe still checks the global PID from bpf_get_current_pid_tgid(). That lookup misses, so the entry probe never stores the saved arguments and the return probe silently drops every realloc event for the tracked process.
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/memtrack/src/ebpf/c/memtrack.bpf.c
Line: 296-297
Comment:
**Realloc Gate Uses Global PID**
When memtrack runs inside the sandbox, userspace registers the namespace-local PID in `tracked_pids`, but this `realloc` entry probe still checks the global PID from `bpf_get_current_pid_tgid()`. That lookup misses, so the entry probe never stores the saved arguments and the return probe silently drops every realloc event for the tracked process.
How can I resolve this? If you propose a fix, please make it concise.| if name == symbol_name { | ||
| let addr = symbol.address(); | ||
| if addr != 0 { |
There was a problem hiding this comment.
Executable Symbols Keep Load Base
symbol.address() is the raw ELF symbol value. That works for shared libraries with a zero relative base, but statically linked allocator executables can be non-PIE ET_EXEC files where the value includes the executable load base, so the new offset-based uprobe_multi attach can use 0x401230 instead of the expected relative offset and miss the allocator entry point.
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/memtrack/src/ebpf/memtrack.rs
Line: 48-50
Comment:
**Executable Symbols Keep Load Base**
`symbol.address()` is the raw ELF symbol value. That works for shared libraries with a zero relative base, but statically linked allocator executables can be non-PIE `ET_EXEC` files where the value includes the executable load base, so the new offset-based `uprobe_multi` attach can use `0x401230` instead of the expected relative offset and miss the allocator entry point.
How can I resolve this? If you propose a fix, please make it concise.| fn has_delegated_bpf_token() -> bool { | ||
| std::env::var_os("LIBBPF_BPF_TOKEN_PATH") | ||
| .is_some_and(|p| !p.is_empty() && Path::new(&p).is_dir()) | ||
| } |
There was a problem hiding this comment.
This returns satisfied for any non-empty LIBBPF_BPF_TOKEN_PATH that points at a directory. If the environment contains a stale value like /tmp, the runner skips the normal capability grant, then libbpf treats the env var as mandatory and fails later when memtrack tries to load BPF programs from a non-token directory.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/executor/memory/executor.rs
Line: 104-107
Comment:
**Directory Treated As Token**
This returns satisfied for any non-empty `LIBBPF_BPF_TOKEN_PATH` that points at a directory. If the environment contains a stale value like `/tmp`, the runner skips the normal capability grant, then libbpf treats the env var as mandatory and fails later when memtrack tries to load BPF programs from a non-token directory.
How can I resolve this? If you propose a fix, please make it concise.
Merging this PR will not alter performance
|
Make the memory instrument work when the runner and
codspeed-memtrackrun inside the macro-agent sandbox — unprivileged, in a user + PID namespace, with a delegated BPF token instead of root/file-capabilities. Companion to the platform PR that sets up the token and the exe-gate.Three things had to change:
Attach via
bpf()-native links. memtrack attached uprobes throughperf_event_openand a perf tracepoint, which a BPF token cannot authorize (they are gated onCAP_PERFMONin the init user namespace). Switch the allocation probes touprobe_multilinks andsched_process_forktotp_btf, both of which attach through thebpf()syscall a token covers. The eBPF logic is unchanged. Symbols are resolved to file offsets ourselves and attached by offset, since libbpf'suprobe_multisymbol resolution returnedENOENTfor libc allocator symbols (which silently captured zero events).Resolve PIDs relative to the tracker's PID namespace. Inside a PID namespace the PIDs memtrack registers are namespace-local while eBPF sees global PIDs, so nothing matched. Read PIDs relative to memtrack's own PID namespace (
bpf_get_ns_current_pid_tgidfor the current task, a CO-RE walk oftask->thread_pid->numbers[level]for the fork tracepoint). In the init namespace this resolves to global PIDs, preserving existing behavior.Accept a delegated BPF token as a privilege source. The memory executor required root or memtrack file-capabilities and otherwise bailed (and prompted for sudo). Treat a live
LIBBPF_BPF_TOKEN_PATHas satisfied privilege, and makegrant_privilegesa no-op when a token is present.The token half is transparent to memtrack — libbpf picks the token up from
LIBBPF_BPF_TOKEN_PATH; no explicit token code.Proven end-to-end on the macro-runner with the platform side:
codspeed run -m memoryon an instrumented divan bench runs inside the sandbox and uploads to staging (run6a4dae30d60d9ec8e172062b, peak 28.5 KB / 140 allocations).Refs COD-3047