feat: run CPU isolation and the memory instrument inside the macro-agent sandbox - #456
feat: run CPU isolation and the memory instrument inside the macro-agent sandbox#456GuillaumeLagrange wants to merge 2 commits into
Conversation
Merging this PR will not alter performance
|
Greptile SummaryThis PR wires CPU isolation and memtrack's memory instrument into the macro-agent sandbox by solving two orthogonal privilege problems. CPU isolation drops the runner's own
Confidence Score: 5/5Safe to merge — the isolation refactoring is well-structured RAII with correct pre/post hook pairing, and both BPF skeleton flavors have been verified to produce identical tracking output by the new equivalence test. The two previously-flagged concerns (UPROBE_ARGS_RET pid-gating bug and has_delegated_bpf_token() is_dir() consistency) are both confirmed resolved. The only remaining issues are two eprintln! calls in the new test file that have no effect on production behaviour. Files Needing Attention: crates/memtrack/tests/flavor_equivalence_tests.rs — two eprintln! calls should be replaced with tracing::info!.
|
| Filename | Overview |
|---|---|
| src/executor/wall_time/isolation.rs | Clean redesign: replaces the always-running HookScriptsGuard with an Isolation enum whose RAII Drop correctly pairs pre_bench/post_bench for the Hooks variant; Systemd remains the gen-1 fallback (no hooks). Logic, tests, and hook detection all look correct. |
| crates/memtrack/src/ebpf/memtrack/mod.rs | Splits the single skeleton into Token (uprobe_multi + tp_btf, BPF-token delegatable) and Perf (classic perf) flavors; has_delegated_bpf_token() now consistently includes the is_dir() check; PID namespace rodata is correctly propagated to both flavors before load. |
| crates/memtrack/src/ebpf/c/utils/flavor.h | New header defining UPROBE_SEC/URETPROBE_SEC macros, current_pid() (returns namespace-relative TGID), and task_ns_pid() for fork tracking. Level bounds check (< 4) satisfies the BPF verifier before the variable array index. |
| crates/memtrack/src/ebpf/c/allocator.h | All SEC("uprobe") annotations replaced with SEC(UPROBE_SEC), and all pid computations updated to use current_pid() — including the previously-flagged UPROBE_ARGS_RET macro. Change is complete and consistent. |
| crates/memtrack/tests/flavor_equivalence_tests.rs | New integration test that runs the deterministic workload under both flavors and asserts identical tracking summaries; two eprintln! calls violate the repo rule requiring tracing macros. |
| src/executor/memory/executor.rs | has_delegated_bpf_token() added with proper is_dir() guard; privilege check and grant_privileges() correctly short-circuit when a delegated token is present, avoiding an unnecessary sudo prompt. |
| src/executor/wall_time/executor.rs | HookScriptsGuard removed and replaced with the new Isolation lifecycle; isolate bool renamed to requires_sudo for clarity; logic is otherwise unchanged. |
Sequence Diagram
sequenceDiagram
participant E as WallTimeExecutor
participant I as Isolation::resolve()
participant H as Machine Hooks
participant P as Profiler
participant B as Benchmark
E->>I: Isolation::resolve()
alt codspeed-wrap-bench exists
I->>H: run_pre_bench(runner_pid)
H-->>I: (evacuates bench cores)
I-->>E: Isolation::Hooks
E->>P: "wrap_command(requires_sudo=false)"
H->>B: exec benchmark (bench cores)
P->>B: record (descendant, no sudo)
Note over I: Drop -> run_hook(post-bench)
else no wrap-bench, can elevate
I-->>E: Isolation::Systemd
E->>P: "wrap_command(requires_sudo=true)"
P->>B: record (system-wide, sudo)
else no hook, no elevation
I-->>E: Isolation::None
E->>P: "wrap_command(requires_sudo=false)"
P->>B: record (descendant, no sudo)
end
Reviews (3): Last reviewed commit: "feat(memtrack): allow memtrack usage wit..." | Re-trigger Greptile
cd83132 to
1142235
Compare
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>
Refs COD-3047
1142235 to
217413d
Compare
228b86f to
266f658
Compare
|
@greptileai review |
not-matthias
left a comment
There was a problem hiding this comment.
Overall, already pretty good. I think we can improve the naming a bit and let's check if we can keep the tests unified.
Lmk if you have some other thoughts on some comments.
|
|
||
| /// Which set of attach mechanisms a loaded skeleton uses. | ||
| #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||
| pub enum Flavor { |
There was a problem hiding this comment.
nit: When I read flavor I think of 🧁
Wdyt of MemtrackVariant or BpfVariant?
| /// Load the skeleton, selecting the attach flavor from the environment: a | ||
| /// delegated BPF token means we are the sandboxed workload and must use the | ||
| /// token path; otherwise the perf path, which also supports kernels | ||
| /// predating uprobe_multi. |
There was a problem hiding this comment.
this comment can be simplified/deslopped
| /// Load the skeleton for a specific attach flavor, bypassing environment | ||
| /// detection. Used by tests to exercise either path directly; both attach | ||
| /// fine on a privileged kernel without a token (the token only authorizes | ||
| /// `bpf()` from inside the unprivileged sandbox). |
There was a problem hiding this comment.
this comment is also a bit too verbose imo
the token only authorizes /// `bpf()` from inside the unprivileged sandbox
would it make sense to also test this behavior here to ensure it works? wdyt?
| /// Set the PID-namespace rodata on an open skeleton, so both flavors resolve | ||
| /// PIDs the same way. `$open` is the open skeleton, of either flavor. | ||
| macro_rules! set_pidns_rodata { | ||
| ($open:expr, $ids:expr) => { | ||
| if let (Some((dev, ino)), Some(rodata)) = ($ids, $open.maps.rodata_data.as_deref_mut()) { | ||
| rodata.target_pidns_dev = dev; | ||
| rodata.target_pidns_ino = ino; | ||
| } | ||
| }; | ||
| } |
There was a problem hiding this comment.
nit: since this macro is only used inside the with_flavor func, can we just move it there to not have it in the global scope (even if it's not exported)
| /* Attach mechanism, selected at build time. With a delegated BPF token | ||
| * (MEMTRACK_BPF_LINKS) the uprobes attach as uprobe_multi links and the fork | ||
| * hook as tp_btf, both authorized through bpf(). Without it they use the classic | ||
| * perf-based uprobe/tracepoint paths, which work on kernels predating | ||
| * uprobe_multi (< 6.6) but cannot be delegated into the sandbox. */ | ||
| #ifdef MEMTRACK_BPF_LINKS | ||
| #define UPROBE_SEC "uprobe.multi" | ||
| #define URETPROBE_SEC "uretprobe.multi" | ||
| #else | ||
| #define UPROBE_SEC "uprobe" | ||
| #define URETPROBE_SEC "uretprobe" | ||
| #endif |
There was a problem hiding this comment.
As discussed, let's check if we need uprobe.multi
| @@ -0,0 +1,5 @@ | |||
| /* Classic perf attach variant: perf-based uprobe/uretprobe + a perf | |||
There was a problem hiding this comment.
Does "perf" make sense here for the name of the variant/file? I associate it more with the perf tool.
| @@ -0,0 +1,123 @@ | |||
| //! Verifies the two eBPF attach flavors produce equivalent tracking results. | |||
There was a problem hiding this comment.
Wouldn't it make more sense to simply run the existing tests both with flavor::token and flavor::perf? because then we can reuse the existing test fixtures rather than causing a split and having to test everything separately.
it should result in the same snapshots, right?
| } | ||
| } | ||
|
|
||
| #ifdef MEMTRACK_BPF_LINKS |
There was a problem hiding this comment.
no idea if possible, but could we use the tp_btf solution for both?
|
|
||
| __u64 pid_tgid = bpf_get_current_pid_tgid(); | ||
| __u32 tgid = pid_tgid >> 32; | ||
| __u32 tgid = current_pid(); |
There was a problem hiding this comment.
tgid is actually the wrong name here, this should be named pid
| const volatile __u64 target_pidns_ino = 0; | ||
|
|
||
| /* Current task's PID in the configured namespace (or global when unset). */ | ||
| static __always_inline __u32 current_pid(void) { |
There was a problem hiding this comment.
don't we need to return both the tid and pid for the target namespace?
we could mimic the style of bpf_get_current_pid_tgid and have memtrack_current_pid_tgid or something like that
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: anIsolationtype invokescodspeed-{pre,wrap,post}-benchand stays oblivious to how cores are attributed.If
codspeed-wrap-benchexists it takes the (unprivileged) hook path; otherwiseit falls back to
systemd-run --scope --slice=codspeed.slice, so non-sandboxhosts 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_PATHas a privilege source.Closes COD-3012
Closes COD-3047