Skip to content

feat: run CPU isolation and the memory instrument inside the macro-agent sandbox#456

Open
GuillaumeLagrange wants to merge 2 commits into
mainfrom
macro-agent-sandbox
Open

feat: run CPU isolation and the memory instrument inside the macro-agent sandbox#456
GuillaumeLagrange wants to merge 2 commits into
mainfrom
macro-agent-sandbox

Conversation

@GuillaumeLagrange

@GuillaumeLagrange GuillaumeLagrange commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Runner-side changes to make CPU isolation and the memory instrument work
unprivileged inside the macro-agent sandbox. Two issues, combined here because
they share the sandbox's privilege model and the same branch.

CPU isolation (COD-3012). Drop the runner's built-in systemd-run /
CGROUP: isolation for a hook-based one: an Isolation type invokes
codspeed-{pre,wrap,post}-bench and stays oblivious to how cores are attributed.
If codspeed-wrap-bench exists it takes the (unprivileged) hook path; otherwise
it falls back to systemd-run --scope --slice=codspeed.slice, so non-sandbox
hosts are unchanged.

Memory instrument (COD-3047). Let memtrack run off a delegated BPF token
instead of root: attach via bpf()-native links (uprobe_multi + tp_btf),
resolve tracked PIDs in the tracker's PID namespace, and accept
LIBBPF_BPF_TOKEN_PATH as a privilege source.

Closes COD-3012
Closes COD-3047

GuillaumeLagrange and others added 2 commits July 15, 2026 14:00
Replace the runner's built-in CPU-isolation mechanisms with a single,
machine-driven one. The runner previously hard-coded `systemd-run --scope` and a
`CODSPEED_ISOLATION=CGROUP:<dir>` mode with per-spawn cgroup-dir plumbing, split
across a `HookScriptsGuard` (which ran the pre/post-bench hooks) and
`isolation.rs` (which did the wrapping).

Now a single `Isolation` type owns the whole lifecycle: `resolve()` runs the
pre-bench hook, `wrap_bench()` pins the benchmark leaf, and `Drop` runs
post-bench. Cpuset logic lives on the machine behind three hooks
(`codspeed-{pre,wrap,post}-bench`); the runner only invokes them and is otherwise
oblivious to how cores are attributed. Discovery is by hook presence:

- an executable `codspeed-wrap-bench` selects the hook path — unprivileged, and
  the benchmark stays a descendant of the profiler so it records without sudo;
- its absence falls back to `systemd-run --scope --slice=codspeed.slice`, so
  hosts without the hook keep working unchanged.

The pre-bench hook is invoked with the runner's PID so the machine places the
runner (and the profiler it spawns) onto the system cores; the runner makes no
cgroup writes of its own. The profiler's `wrap_command` flag is renamed
`isolate` -> `requires_sudo`, now true only for the systemd fallback.

Refs COD-3012
Co-Authored-By: Claude <noreply@anthropic.com>
@codspeed-hq

codspeed-hq Bot commented Jul 15, 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 macro-agent-sandbox (a3775ab) with main (58d994a)

Open in CodSpeed

@greptile-apps

greptile-apps Bot commented Jul 15, 2026

Copy link
Copy Markdown

Greptile Summary

This PR ships two sandbox-enablement features together: hook-based CPU isolation (replacing HookScriptsGuard + systemd-run with a unified Isolation enum that discovers machine hooks at runtime) and BPF-token memory instrumentation (splitting memtrack into two skeletons — uprobe_multi/tp_btf for the token path, classic perf for the fallback — while teaching eBPF programs to resolve PIDs in the runner's namespace rather than globally).

  • CPU isolation (Isolation enum): resolve() runs pre-bench, wrap_bench() pins the leaf, Drop fires post-bench. The Systemd fallback is preserved unchanged; requires_sudo() cleanly separates the privilege question from the isolation question.
  • Dual eBPF skeletons: memtrack_token.bpf.c defines MEMTRACK_BPF_LINKS to select uprobe.multi/tp_btf sections; memtrack_perf.bpf.c uses classic paths. MemtrackBpf::with_flavor() lets tests exercise either path, and a new flavor_equivalence_tests binary compares summaries across both.
  • PID namespace resolution: New current_pid() / task_ns_pid() BPF helpers call bpf_get_ns_current_pid_tgid when a target namespace is configured, with pidns_dev/pidns_ino rodata set from userspace at load time.

Confidence Score: 3/5

The CPU isolation refactor is clean and well-tested, but the eBPF namespace PID fix has a gap that silently breaks realloc tracking inside the sandbox.

The UPROBE_ARGS_RET macro in memtrack.bpf.c (used exclusively for realloc) still uses global pid = tid >> 32 for its is_tracked guard while every other probe was updated to current_pid(). Inside a PID namespace, namespace-local PIDs stored in tracked_pids never match the global PID the guard looks up, so realloc args are never stored and every call is silently dropped. The flavor_equivalence test does not catch this because it runs without a PID namespace.

crates/memtrack/src/ebpf/c/memtrack.bpf.c — the UPROBE_ARGS_RET macro entry probe PID guard on line 319

Important Files Changed

Filename Overview
crates/memtrack/src/ebpf/c/memtrack.bpf.c Adds PID namespace resolution via current_pid()/task_ns_pid() and a tp_btf fork hook for the token flavor. The UPROBE_ARGS_RET macro (realloc) still uses global pid = tid >> 32 for its is_tracked guard, silently breaking realloc tracking inside the sandbox.
crates/memtrack/src/ebpf/memtrack.rs Major refactor: dual-skeleton (Token/Perf) with a with_skel! macro for transparent dispatch; resolve_symbol_offset now returns the offset; pidns rodata configured at load time. has_delegated_bpf_token() lacks the is_dir() guard present in executor.rs.
src/executor/wall_time/isolation.rs Replaces HookScriptsGuard + separate requires_isolation()/wrap_isolation_scope() with a single Isolation enum that owns the pre-bench/wrap/post-bench lifecycle. Hook detection is clean; Drop fires post-bench only for Hooks mode. Well-tested.
src/executor/wall_time/executor.rs Simplified: Isolation::resolve() replaces HookScriptsGuard::setup() + requires_isolation(); isolation is now held for the run duration, and requires_sudo() drives the profiler privilege path.
src/executor/memory/executor.rs Adds BPF token as a third privilege source alongside root and file capabilities; has_delegated_bpf_token() correctly checks both non-empty and is_dir().
crates/memtrack/src/ebpf/tracker.rs Refactored to expose with_flavor() for test injection; logic split into with_bpf() and from_bpf_without_allocators() is clean.
crates/memtrack/build.rs Compiles two skeletons (memtrack_token, memtrack_perf) from their thin wrapper sources; clean loop over the pairs.
crates/memtrack/tests/flavor_equivalence_tests.rs New test exercises both attach flavors against the same deterministic workload and compares summaries. Skips gracefully when privilege or uprobe_multi is unavailable. Only runs on GITHUB_ACTIONS; the UPROBE_ARGS_RET bug is not caught here because the test runs without a PID namespace.
crates/memtrack/tests/shared.rs Refactored: track_command split into track_command_with_tracker; between_markers extracted as a pub fn; track_command_with_flavor added for test injection.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Runner
    participant Isolation
    participant PreHook as codspeed-pre-bench
    participant WrapHook as codspeed-wrap-bench
    participant PostHook as codspeed-post-bench
    participant Profiler
    participant Bench

    Runner->>Isolation: resolve()
    alt Hooks mode (wrap hook present)
        Isolation->>PreHook: run(runner_pid)
        PreHook-->>Isolation: runner moved to system cores
        Isolation-->>Runner: Isolation::Hooks
        Runner->>Isolation: wrap_bench(cmd)
        Isolation-->>Runner: wrap-bench bench_cmd
        Runner->>Profiler: "wrap_command(cmd, requires_sudo=false)"
        Profiler->>WrapHook: exec bench
        WrapHook->>Bench: exec on bench cores
        Bench-->>Profiler: exit
        Runner->>Isolation: drop()
        Isolation->>PostHook: run()
    else Systemd mode (no hook, can elevate)
        Isolation-->>Runner: Isolation::Systemd
        Runner->>Profiler: "wrap_command(cmd, requires_sudo=true)"
        Profiler->>Bench: sudo systemd-run (reparented)
        Bench-->>Profiler: exit
    else None mode
        Isolation-->>Runner: Isolation::None
        Runner->>Profiler: "wrap_command(cmd, requires_sudo=false)"
        Profiler->>Bench: exec
        Bench-->>Profiler: exit
    end
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"}}}%%
sequenceDiagram
    participant Runner
    participant Isolation
    participant PreHook as codspeed-pre-bench
    participant WrapHook as codspeed-wrap-bench
    participant PostHook as codspeed-post-bench
    participant Profiler
    participant Bench

    Runner->>Isolation: resolve()
    alt Hooks mode (wrap hook present)
        Isolation->>PreHook: run(runner_pid)
        PreHook-->>Isolation: runner moved to system cores
        Isolation-->>Runner: Isolation::Hooks
        Runner->>Isolation: wrap_bench(cmd)
        Isolation-->>Runner: wrap-bench bench_cmd
        Runner->>Profiler: "wrap_command(cmd, requires_sudo=false)"
        Profiler->>WrapHook: exec bench
        WrapHook->>Bench: exec on bench cores
        Bench-->>Profiler: exit
        Runner->>Isolation: drop()
        Isolation->>PostHook: run()
    else Systemd mode (no hook, can elevate)
        Isolation-->>Runner: Isolation::Systemd
        Runner->>Profiler: "wrap_command(cmd, requires_sudo=true)"
        Profiler->>Bench: sudo systemd-run (reparented)
        Bench-->>Profiler: exit
    else None mode
        Isolation-->>Runner: Isolation::None
        Runner->>Profiler: "wrap_command(cmd, requires_sudo=false)"
        Profiler->>Bench: exec
        Bench-->>Profiler: exit
    end
Loading

Comments Outside Diff (1)

  1. crates/memtrack/src/ebpf/c/memtrack.bpf.c, line 317-322 (link)

    P1 realloc silently drops all events inside the sandbox

    The UPROBE_ARGS_RET macro's entry probe uses __u32 pid = tid >> 32 (global PID) to gate whether args are stored, but tracked_pids is keyed on namespace-local PIDs when the tracker runs inside a PID namespace. In that case the guard always fails, bpf_map_update_elem is never called, uretprobe_realloc finds nothing in the args map and returns early — so every realloc call is silently missed.

    All the other entry probes were fixed: store_param (used by UPROBE_ARG_RET) was updated to use current_pid(), and SUBMIT_EVENT was updated too, but UPROBE_ARGS_RET was left behind. The fix is to replace __u32 pid = tid >> 32; in this macro with __u32 pid = current_pid();, matching the pattern used everywhere else.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: crates/memtrack/src/ebpf/c/memtrack.bpf.c
    Line: 317-322
    
    Comment:
    **`realloc` silently drops all events inside the sandbox**
    
    The `UPROBE_ARGS_RET` macro's entry probe uses `__u32 pid = tid >> 32` (global PID) to gate whether args are stored, but `tracked_pids` is keyed on namespace-local PIDs when the tracker runs inside a PID namespace. In that case the guard always fails, `bpf_map_update_elem` is never called, `uretprobe_realloc` finds nothing in the args map and returns early — so every `realloc` call is silently missed.
    
    All the other entry probes were fixed: `store_param` (used by `UPROBE_ARG_RET`) was updated to use `current_pid()`, and `SUBMIT_EVENT` was updated too, but `UPROBE_ARGS_RET` was left behind. The fix is to replace `__u32 pid = tid >> 32;` in this macro with `__u32 pid = current_pid();`, matching the pattern used everywhere else.
    
    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 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
crates/memtrack/src/ebpf/c/memtrack.bpf.c:317-322
**`realloc` silently drops all events inside the sandbox**

The `UPROBE_ARGS_RET` macro's entry probe uses `__u32 pid = tid >> 32` (global PID) to gate whether args are stored, but `tracked_pids` is keyed on namespace-local PIDs when the tracker runs inside a PID namespace. In that case the guard always fails, `bpf_map_update_elem` is never called, `uretprobe_realloc` finds nothing in the args map and returns early — so every `realloc` call is silently missed.

All the other entry probes were fixed: `store_param` (used by `UPROBE_ARG_RET`) was updated to use `current_pid()`, and `SUBMIT_EVENT` was updated too, but `UPROBE_ARGS_RET` was left behind. The fix is to replace `__u32 pid = tid >> 32;` in this macro with `__u32 pid = current_pid();`, matching the pattern used everywhere else.

### Issue 2 of 2
crates/memtrack/src/ebpf/memtrack.rs:30-32
`has_delegated_bpf_token()` is defined differently here than in `src/executor/memory/executor.rs`. The executor's version additionally verifies the path is a directory (`Path::new(&p).is_dir()`), guarding against a stale or invalid `LIBBPF_BPF_TOKEN_PATH`. This function skips that check, so if the variable is set to a non-existent path, the executor correctly falls through to capability checking while this code still selects the token flavor — causing `MemtrackBpf::new()` to fail with an opaque eBPF load error instead of the clear "no privilege" path.

```suggestion
fn has_delegated_bpf_token() -> bool {
    std::env::var_os("LIBBPF_BPF_TOKEN_PATH")
        .is_some_and(|p| !p.is_empty() && std::path::Path::new(&p).is_dir())
}
```

Reviews (1): Last reviewed commit: "feat(memtrack): allow memtrack usage wit..." | Re-trigger Greptile

Comment on lines +30 to +32
fn has_delegated_bpf_token() -> bool {
std::env::var_os("LIBBPF_BPF_TOKEN_PATH").is_some_and(|p| !p.is_empty())
}

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 has_delegated_bpf_token() is defined differently here than in src/executor/memory/executor.rs. The executor's version additionally verifies the path is a directory (Path::new(&p).is_dir()), guarding against a stale or invalid LIBBPF_BPF_TOKEN_PATH. This function skips that check, so if the variable is set to a non-existent path, the executor correctly falls through to capability checking while this code still selects the token flavor — causing MemtrackBpf::new() to fail with an opaque eBPF load error instead of the clear "no privilege" path.

Suggested change
fn has_delegated_bpf_token() -> bool {
std::env::var_os("LIBBPF_BPF_TOKEN_PATH").is_some_and(|p| !p.is_empty())
}
fn has_delegated_bpf_token() -> bool {
std::env::var_os("LIBBPF_BPF_TOKEN_PATH")
.is_some_and(|p| !p.is_empty() && std::path::Path::new(&p).is_dir())
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/memtrack/src/ebpf/memtrack.rs
Line: 30-32

Comment:
`has_delegated_bpf_token()` is defined differently here than in `src/executor/memory/executor.rs`. The executor's version additionally verifies the path is a directory (`Path::new(&p).is_dir()`), guarding against a stale or invalid `LIBBPF_BPF_TOKEN_PATH`. This function skips that check, so if the variable is set to a non-existent path, the executor correctly falls through to capability checking while this code still selects the token flavor — causing `MemtrackBpf::new()` to fail with an opaque eBPF load error instead of the clear "no privilege" path.

```suggestion
fn has_delegated_bpf_token() -> bool {
    std::env::var_os("LIBBPF_BPF_TOKEN_PATH")
        .is_some_and(|p| !p.is_empty() && std::path::Path::new(&p).is_dir())
}
```

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Codex

@GuillaumeLagrange GuillaumeLagrange changed the title [COD-3012] feat: run CPU isolation and the memory instrument inside the macro-agent sandbox feat: run CPU isolation and the memory instrument inside the macro-agent sandbox Jul 15, 2026
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