feat(memtrack): attach allocator probes on demand#445
Conversation
Merging this PR will not alter performance
|
Greptile SummaryThis PR replaces the eager startup library scan with an on-demand BPF-driven attach mechanism. A new
Confidence Score: 5/5The on-demand attach mechanism is safe to merge. The fixpoint stop-drain-attach loop is correctly ordered and the ContGuard covers every exit path. The core stop-the-world attach logic is sound: BPF submits the ring buffer entry before sending SIGSTOP, so a full consume() after all producers are stopped cannot miss requests. The fixpoint loop correctly discovers new child pids from each drain. ContGuard guarantees SIGCONT on every exit path including errors and panics. The only finding is that a libbpf-level ring buffer consume failure in the poll thread is silently swallowed, which does not affect normal operation. crates/memtrack/src/ebpf/poller.rs — the let _ = ringbuf.consume() in the drain handler discards consume errors Important Files Changed
|
8bce092 to
16ffc38
Compare
…tcher Instead of pre-attaching every allocator found on the system, a BPF fentry on security_mmap_file signals the first mapping of each unknown executable inode. A background worker stops the mapping processes, classifies the file by its symbols, attaches probes, and resumes them. This covers dlopen'd and statically linked allocators in spawned children, and skips libraries the benchmark never loads. The public API shrinks to Tracker (owns the attach worker) and Session (owns the spawned child and its event pipeline); ring buffers are polled through a generic RingBufferPoller with caller-supplied parsing. Fixes COD-1801
On-demand attach supersedes the startup scan: delete the system-wide library glob, the build-dir walk, and the CODSPEED_MEMTRACK_BINARIES env var (the runner no longer resolves exec target binaries for it). BREAKING CHANGE: CODSPEED_MEMTRACK_BINARIES is no longer read Refs COD-1801
Distro libjemalloc is built without the je_ symbol prefix, so symbol classification missed it and fell through to libc++ (jemalloc exports operator new), leaving plain malloc/aligned_alloc unprobed. Match on mallocx, which keeps its name in unprefixed builds and is unique to jemalloc.
16ffc38 to
5b76788
Compare
GuillaumeLagrange
left a comment
There was a problem hiding this comment.
Stopping here becuase I want us to dig the TRACEME thing before leaning in the stop wrapper
| return 0; | ||
| } | ||
|
|
||
| #include "attach.bpf.h" |
There was a problem hiding this comment.
that's slightly weird to attach.bpf.h like this no?
is this to keep a single .bpf.c entry to easy build.rs compilation?
Maybe we could have a main.bpf.c or sthing that just includes sibling module.bpf.c files to keep things a bit tidier?
There was a problem hiding this comment.
The goal swas to separate the eBPF tracepoints into multiple files since it's getting a bit messy imo. The vision I had in mind, was to put different tracepoint categories into their own file: memory tracking, fork/attach tracking, rss tracking. etc
I used a .h since it's very easy to just include it, but I'll check if we can have multiple .c source files.
There was a problem hiding this comment.
I agree with the initial goal. It's also possible to include .c files btw, although not common.
But the main thing that bothers me is that we have one main file that defines a bunch of eBPF tracepoints, then includes another file, the hierarchy is a bit weird, we may want to simply create two sibling included files and keep a clean main that only includes stuff
There was a problem hiding this comment.
Ah yes, I get what you mean. I'll see what I can do 👍
| /// `uid_gid` drops the child's privileges (a `Command`'s uid/gid cannot be | ||
| /// read back, so it cannot be preserved through the wrap). | ||
| pub fn spawn(&self, cmd: &Command, uid_gid: Option<(u32, u32)>) -> Result<Session> { | ||
| let mut wrapped = wrap_stopped(cmd); |
There was a problem hiding this comment.
Why do we need to have the command self-stopped ??
Isnt it precisely the role of the bpf exec mapper that we've attached??
There was a problem hiding this comment.
Well, we're only doing the fork/exec tracking for tracked_pids which we'll have to setup at some point. So there's a small race condition window at the start that drops allocations.
But maybe we can use pre_exec for this
| pub fn stopped_command(program: impl AsRef<OsStr>, args: &[OsString]) -> Command { | ||
| let mut cmd = Command::new("sh"); | ||
| cmd.arg("-c") | ||
| .arg(r#"kill -STOP "$$"; exec "$@""#) | ||
| .arg("sh") | ||
| .arg(program.as_ref()) | ||
| .args(args); | ||
| cmd | ||
| } |
There was a problem hiding this comment.
I am 99% sure that we'll get both simpler code and much more reliable results by adding a ptrace(PTRACE_TRACEME) syscall, WDYT?
Did you already try it and discard it? Because while I see why we dont do a self SIGSTOP, ptrace seems more tried and tested, as well as saving us from yet another shell wrapper
There was a problem hiding this comment.
The main reason we need this is to not have to manually parse the /proc/maps mappings to find the static/dynamic allocators after the execve if we use ptrace.
So basically what we're doing right here is:
- Spawn
sh, stop it - Track this pid in eBPF
- Exec the actual target program -> eBPF can now use the same logic to detect new mappings/forks/execs
If we wanted to use ptrace, we'd still do sh -c 'exec "$@"' and then use PTRACE_TRACEME to stop it in pre_exec. I don't like this as much since it separates the logic and makes it harder to understand for (almost) no benefit.
Wdyt?
Attach allocator uprobes only to libraries the tracked process tree actually maps, instead of pre-attaching every allocator discovered on the system at startup.
A BPF fentry on
security_mmap_filesignals the first mapping of each unknown executable inode. A background worker SIGSTOPs the mapping processes, classifies the file by its allocator symbols, attaches probes, and resumes them. This:dlopen'd allocators and statically linked allocators in spawned children (previously missed entirely)The eager discovery path and
CODSPEED_MEMTRACK_BINARIESare removed since the watcher supersedes them (breaking: the env var is no longer read).The crate API is now
Tracker(owns the attach worker) andSession(owns the spawned child and its event pipeline); ring buffers are polled through a genericRingBufferPollerwith caller-supplied parsing, replacing the keepalive forwarding thread.Review focus: the stop-the-world attach in
attach_worker.rs(fixpoint stop + synchronous ring buffer drain) — its correctness argument is that a fullconsume()after all producers are stopped cannot miss requests.Fixes COD-1801