Skip to content

feat(memtrack): support running the memory instrument in the macro-agent sandbox#443

Draft
GuillaumeLagrange wants to merge 4 commits into
mainfrom
cod-3047-support-memory-instrument-on-macro-agent-sandbox
Draft

feat(memtrack): support running the memory instrument in the macro-agent sandbox#443
GuillaumeLagrange wants to merge 4 commits into
mainfrom
cod-3047-support-memory-instrument-on-macro-agent-sandbox

Conversation

@GuillaumeLagrange

Copy link
Copy Markdown
Contributor

Make the memory instrument work when the runner and codspeed-memtrack run 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 through perf_event_open and a perf tracepoint, which a BPF token cannot authorize (they are gated on CAP_PERFMON in the init user namespace). Switch the allocation probes to uprobe_multi links and sched_process_fork to tp_btf, both of which attach through the bpf() syscall a token covers. The eBPF logic is unchanged. Symbols are resolved to file offsets ourselves and attached by offset, since libbpf's uprobe_multi symbol resolution returned ENOENT for 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_tgid for the current task, a CO-RE walk of task->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_PATH as satisfied privilege, and make grant_privileges a 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 memory on an instrumented divan bench runs inside the sandbox and uploads to staging (run 6a4dae30d60d9ec8e172062b, peak 28.5 KB / 140 allocations).

Refs COD-3047

GuillaumeLagrange and others added 4 commits July 7, 2026 18:32
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-apps

greptile-apps Bot commented Jul 8, 2026

Copy link
Copy Markdown

Greptile Summary

This PR makes memtrack work with the macro-agent sandbox. The main changes are:

  • BPF-native links for allocation probes and fork tracking.
  • PID lookup relative to the tracker PID namespace.
  • Delegated BPF token handling in the memory executor.

Confidence Score: 4/5

The sandbox memory path can produce incomplete traces until the remaining global-PID gates are fixed.

  • Realloc, mmap, and brk events can be dropped for tracked processes in a PID namespace.
  • Offset-based attachment can miss allocator symbols in non-PIE executable targets.
  • A stale token env var can skip setup and fail later with a less clear memtrack load error.

crates/memtrack/src/ebpf/c/memtrack.bpf.c, crates/memtrack/src/ebpf/memtrack.rs

Important Files Changed

Filename Overview
crates/memtrack/src/ebpf/c/memtrack.bpf.c Adds namespace-aware PID helpers and BPF-native probe sections, but several changed gates still use global PIDs and drop events in the sandbox.
crates/memtrack/src/ebpf/memtrack.rs Switches probe attachment to uprobe_multi and manual symbol offsets, with a risk for non-PIE executable allocator targets.
src/executor/memory/executor.rs Adds delegated BPF token recognition, but accepts any directory as proof that the token path is usable.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Tracker registers namespace PID] --> B[tracked_pids map]
  C[eBPF probe gets global pid_tgid] --> D{Gate uses current_pid}
  D -->|yes| E[Namespace PID lookup matches]
  D -->|no| F[Global PID lookup misses]
  E --> G[Event is emitted]
  F --> H[Saved args missing and event is dropped]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
  A[Tracker registers namespace PID] --> B[tracked_pids map]
  C[eBPF probe gets global pid_tgid] --> D{Gate uses current_pid}
  D -->|yes| E[Namespace PID lookup matches]
  D -->|no| F[Global PID lookup misses]
  E --> G[Event is emitted]
  F --> H[Saved args missing and event is dropped]
Loading

Comments Outside Diff (2)

  1. crates/memtrack/src/ebpf/c/memtrack.bpf.c, line 362-364 (link)

    P1 Mmap Gate Uses Global PID

    Inside a PID namespace, tracked_pids contains the PID seen by the tracker, while tid >> 32 is the host PID seen by eBPF. This branch then skips storing mmap_temp, so the exit probe has no saved syscall arguments and mmap events are silently missing from memory traces.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: crates/memtrack/src/ebpf/c/memtrack.bpf.c
    Line: 362-364
    
    Comment:
    **Mmap Gate Uses Global PID**
    
    Inside a PID namespace, `tracked_pids` contains the PID seen by the tracker, while `tid >> 32` is the host PID seen by eBPF. This branch then skips storing `mmap_temp`, so the exit probe has no saved syscall arguments and mmap events are silently missing from memory traces.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Claude Code Fix in Codex

  2. crates/memtrack/src/ebpf/c/memtrack.bpf.c, line 412-414 (link)

    P1 Brk Gate Uses Global PID

    The sandbox path registers namespace-local PIDs, but this brk entry probe still checks the global PID. For a tracked process in a PID namespace, the lookup fails, brk_temp is never populated, and the return probe drops brk allocation events.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: crates/memtrack/src/ebpf/c/memtrack.bpf.c
    Line: 412-414
    
    Comment:
    **Brk Gate Uses Global PID**
    
    The sandbox path registers namespace-local PIDs, but this `brk` entry probe still checks the global PID. For a tracked process in a PID namespace, the lookup fails, `brk_temp` is never populated, and the return probe drops brk allocation events.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Claude Code Fix in Codex

Fix All in Claude Code Fix All in Codex

Prompt To Fix All With AI
Fix the following 5 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 5
crates/memtrack/src/ebpf/c/memtrack.bpf.c:296-297
**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.

### Issue 2 of 5
crates/memtrack/src/ebpf/c/memtrack.bpf.c:362-364
**Mmap Gate Uses Global PID**

Inside a PID namespace, `tracked_pids` contains the PID seen by the tracker, while `tid >> 32` is the host PID seen by eBPF. This branch then skips storing `mmap_temp`, so the exit probe has no saved syscall arguments and mmap events are silently missing from memory traces.

### Issue 3 of 5
crates/memtrack/src/ebpf/c/memtrack.bpf.c:412-414
**Brk Gate Uses Global PID**

The sandbox path registers namespace-local PIDs, but this `brk` entry probe still checks the global PID. For a tracked process in a PID namespace, the lookup fails, `brk_temp` is never populated, and the return probe drops brk allocation events.

### Issue 4 of 5
crates/memtrack/src/ebpf/memtrack.rs:48-50
**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.

### Issue 5 of 5
src/executor/memory/executor.rs:104-107
**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.

Reviews (1): Last reviewed commit: "feat(runner): accept a delegated BPF tok..." | Re-trigger Greptile

Comment on lines 296 to 297
__u64 tid = bpf_get_current_pid_tgid(); \
__u32 pid = tid >> 32; \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

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.

Fix in Claude Code Fix in Codex

Comment on lines 48 to 50
if name == symbol_name {
let addr = symbol.address();
if addr != 0 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Fix in Claude Code Fix in Codex

Comment on lines +104 to +107
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())
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

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.

Fix in Claude Code Fix in Codex

@codspeed-hq

codspeed-hq Bot commented Jul 8, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

⚠️ 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.

✅ 7 untouched benchmarks


Comparing cod-3047-support-memory-instrument-on-macro-agent-sandbox (3d86be9) with main (3278ec8)

Open in CodSpeed

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