Skip to content

Commit a3775ab

Browse files
feat(memtrack): allow memtrack usage with a bpf token
Refs COD-3047
1 parent 3566d11 commit a3775ab

13 files changed

Lines changed: 638 additions & 212 deletions

File tree

.github/workflows/ci.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,14 @@ jobs:
9696
# Each memtrack integration test binary runs its cases serially
9797
# (eBPF tracker can't overlap with itself in one process), so we
9898
# shard at the test-binary level to parallelize across jobs.
99-
test: [c_tests, cpp_tests, rust_tests, spawn_tests]
99+
test:
100+
[
101+
c_tests,
102+
cpp_tests,
103+
rust_tests,
104+
spawn_tests,
105+
flavor_equivalence_tests,
106+
]
100107
steps:
101108
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
102109
with:

crates/memtrack/build.rs

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,29 @@ fn build_ebpf() {
1212

1313
println!("cargo:rerun-if-changed=src/ebpf/c");
1414

15-
// Build the BPF program
15+
// Build the BPF program. The same shared source (memtrack.bpf.c) is compiled
16+
// into two skeletons via thin wrappers: memtrack_token.bpf.c defines
17+
// MEMTRACK_BPF_LINKS to attach through bpf() links (uprobe_multi/tp_btf) so a
18+
// delegated token authorizes them, while memtrack_perf.bpf.c uses the classic
19+
// perf-based paths for kernels without uprobe_multi. The runtime picks the
20+
// matching skeleton based on whether a BPF token is available.
1621
let arch = env::var("CARGO_CFG_TARGET_ARCH")
1722
.expect("CARGO_CFG_TARGET_ARCH must be set in build script");
18-
let memtrack_out = PathBuf::from(env::var("OUT_DIR").unwrap()).join("memtrack.skel.rs");
19-
SkeletonBuilder::new()
20-
.source("src/ebpf/c/memtrack.bpf.c")
21-
.clang_args([
22-
"-I",
23-
&vmlinux::include_path_root().join(arch).to_string_lossy(),
24-
])
25-
.build_and_generate(&memtrack_out)
26-
.unwrap();
23+
let vmlinux_inc = vmlinux::include_path_root()
24+
.join(arch)
25+
.to_string_lossy()
26+
.into_owned();
27+
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
28+
for (source, skel) in [
29+
("src/ebpf/c/memtrack_token.bpf.c", "memtrack_token.skel.rs"),
30+
("src/ebpf/c/memtrack_perf.bpf.c", "memtrack_perf.skel.rs"),
31+
] {
32+
SkeletonBuilder::new()
33+
.source(source)
34+
.clang_args(["-I", &vmlinux_inc])
35+
.build_and_generate(out_dir.join(skel))
36+
.unwrap();
37+
}
2738

2839
// Generate bindings for event.h
2940
let bindings = bindgen::Builder::default()

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

Lines changed: 89 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,19 @@
1212

1313
char LICENSE[] SEC("license") = "GPL";
1414

15+
/* Attach mechanism, selected at build time. With a delegated BPF token
16+
* (MEMTRACK_BPF_LINKS) the uprobes attach as uprobe_multi links and the fork
17+
* hook as tp_btf, both authorized through bpf(). Without it they use the classic
18+
* perf-based uprobe/tracepoint paths, which work on kernels predating
19+
* uprobe_multi (< 6.6) but cannot be delegated into the sandbox. */
20+
#ifdef MEMTRACK_BPF_LINKS
21+
#define UPROBE_SEC "uprobe.multi"
22+
#define URETPROBE_SEC "uretprobe.multi"
23+
#else
24+
#define UPROBE_SEC "uprobe"
25+
#define URETPROBE_SEC "uretprobe"
26+
#endif
27+
1528
/* Macros for common map definitions */
1629
#define BPF_HASH_MAP(name, key_type, value_type, max_ents) \
1730
struct { \
@@ -35,6 +48,50 @@ char LICENSE[] SEC("license") = "GPL";
3548
__uint(max_entries, size); \
3649
} name SEC(".maps")
3750

51+
/* PID namespace the userspace tracker resolves PIDs in. When set (ino != 0),
52+
* PIDs are read relative to this namespace so tracking works when the tracker
53+
* runs inside a PID namespace (e.g. the macro-agent sandbox) while eBPF sees
54+
* global PIDs. Zero means "use global PIDs" — the default outside a sandbox. */
55+
const volatile __u64 target_pidns_dev = 0;
56+
const volatile __u64 target_pidns_ino = 0;
57+
58+
/* Current task's PID in the configured namespace (or global when unset). */
59+
static __always_inline __u32 current_pid(void) {
60+
if (target_pidns_ino == 0) {
61+
return bpf_get_current_pid_tgid() >> 32;
62+
}
63+
struct bpf_pidns_info nsinfo = {};
64+
if (bpf_get_ns_current_pid_tgid(target_pidns_dev, target_pidns_ino, &nsinfo, sizeof(nsinfo)) !=
65+
0) {
66+
return 0;
67+
}
68+
return nsinfo.tgid;
69+
}
70+
71+
/* A task's PID as seen in the configured namespace (or global when unset).
72+
* Walks the task's pid struct: numbers[level].nr holds the PID at that namespace
73+
* depth, and numbers[level].ns identifies which namespace it is. */
74+
static __always_inline __u32 task_ns_pid(struct task_struct* task) {
75+
if (target_pidns_ino == 0) {
76+
return BPF_CORE_READ(task, pid);
77+
}
78+
struct pid* thread_pid = BPF_CORE_READ(task, thread_pid);
79+
if (!thread_pid) {
80+
return 0;
81+
}
82+
unsigned int level = BPF_CORE_READ(thread_pid, level);
83+
/* Bound the level to satisfy the verifier and match the pid array. */
84+
if (level >= 4) {
85+
return 0;
86+
}
87+
struct upid* up = &thread_pid->numbers[level];
88+
__u64 ino = BPF_CORE_READ(up, ns, ns.inum);
89+
if (ino != target_pidns_ino) {
90+
return 0;
91+
}
92+
return BPF_CORE_READ(up, nr);
93+
}
94+
3895
/* Map to store PIDs we're tracking */
3996
BPF_HASH_MAP(tracked_pids, __u32, __u8, 10000);
4097
/* Map to store parent-child relationships to detect hierarchy */
@@ -71,26 +128,37 @@ static __always_inline int is_tracked(__u32 pid) {
71128
return 0;
72129
}
73130

74-
SEC("tracepoint/sched/sched_process_fork")
75-
int tracepoint_sched_fork(struct trace_event_raw_sched_process_fork* ctx) {
76-
__u32 parent_pid = ctx->parent_pid;
77-
__u32 child_pid = ctx->child_pid;
78-
79-
/* Print process fork with PIDs */
80-
// bpf_printk("sched_fork: parent_pid=%u child_pid=%u", parent_pid, child_pid);
81-
82-
/* Check if parent is being tracked */
131+
/* Record a parent→child fork so the child inherits the parent's tracked state. */
132+
static __always_inline void follow_fork(__u32 parent_pid, __u32 child_pid) {
133+
if (parent_pid == 0 || child_pid == 0) {
134+
return;
135+
}
83136
if (is_tracked(parent_pid)) {
84-
/* Auto-track this child */
85137
__u8 marker = 1;
86138
bpf_map_update_elem(&tracked_pids, &child_pid, &marker, BPF_ANY);
87139
bpf_map_update_elem(&pids_ppid, &child_pid, &parent_pid, BPF_ANY);
88-
89-
// bpf_printk("auto-tracking child process: child_pid=%u", child_pid);
90140
}
141+
}
91142

143+
#ifdef MEMTRACK_BPF_LINKS
144+
/* tp_btf attaches via bpf(), so a delegated BPF token can authorize it inside
145+
* the sandbox. Reads task_struct pointers, resolving PIDs in the tracker's
146+
* namespace. */
147+
SEC("tp_btf/sched_process_fork")
148+
int BPF_PROG(tracepoint_sched_fork, struct task_struct* parent, struct task_struct* child) {
149+
follow_fork(task_ns_pid(parent), task_ns_pid(child));
92150
return 0;
93151
}
152+
#else
153+
/* Classic perf tracepoint for kernels without (or runs without) a BPF token.
154+
* The raw tracepoint context carries global PIDs directly; without a token we
155+
* never run in a PID namespace, so global PIDs are correct. */
156+
SEC("tracepoint/sched/sched_process_fork")
157+
int tracepoint_sched_fork(struct trace_event_raw_sched_process_fork* ctx) {
158+
follow_fork(ctx->parent_pid, ctx->child_pid);
159+
return 0;
160+
}
161+
#endif
94162

95163
/* == Helper functions for the allocation tracking == */
96164

@@ -110,9 +178,10 @@ static __always_inline int is_enabled(void) {
110178
/* Helper to store parameter value in map for tracking between entry and return
111179
*/
112180
static __always_inline int store_param(void* map, __u64 value) {
181+
/* Key by the global tid (stable, unique per thread across the entry/exit
182+
* pair); gate on the namespace-relative PID that the tracker registered. */
113183
__u64 tid = bpf_get_current_pid_tgid();
114-
__u32 pid = tid >> 32;
115-
if (is_tracked(pid)) {
184+
if (is_tracked(current_pid())) {
116185
bpf_map_update_elem(map, &tid, &value, BPF_ANY);
117186
}
118187
return 0;
@@ -134,7 +203,7 @@ static __always_inline __u64* take_param(void* map) {
134203
#define SUBMIT_EVENT(evt_type, fill_data) \
135204
{ \
136205
__u64 tid = bpf_get_current_pid_tgid(); \
137-
__u32 pid = tid >> 32; \
206+
__u32 pid = current_pid(); \
138207
\
139208
if (!is_tracked(pid) || !is_enabled()) { \
140209
return 0; \
@@ -210,9 +279,9 @@ static __always_inline int submit_mmap_event(__u64 addr, __u64 size, __u8 event_
210279
/* Macro to generate uprobe/uretprobe pairs for allocation functions with 1 argument */
211280
#define UPROBE_ARG_RET(name, arg_expr, submit_block) \
212281
BPF_HASH_MAP(name##_arg, __u64, __u64, 10000); \
213-
SEC("uprobe") \
282+
SEC(UPROBE_SEC) \
214283
int uprobe_##name(struct pt_regs* ctx) { return store_param(&name##_arg, arg_expr); } \
215-
SEC("uretprobe") \
284+
SEC(URETPROBE_SEC) \
216285
int uretprobe_##name(struct pt_regs* ctx) { \
217286
__u64* arg_ptr = take_param(&name##_arg); \
218287
if (!arg_ptr) { \
@@ -228,7 +297,7 @@ static __always_inline int submit_mmap_event(__u64 addr, __u64 size, __u8 event_
228297

229298
/* Macro for simple return value only functions like free */
230299
#define UPROBE_RET(name, arg_expr, submit_block) \
231-
SEC("uprobe") \
300+
SEC(UPROBE_SEC) \
232301
int uprobe_##name(struct pt_regs* ctx) { \
233302
__u64 arg0 = arg_expr; \
234303
if (arg0 == 0) { \
@@ -244,7 +313,7 @@ static __always_inline int submit_mmap_event(__u64 addr, __u64 size, __u8 event_
244313
__u64 arg1; \
245314
}; \
246315
BPF_HASH_MAP(name##_args, __u64, struct name##_args_t, 10000); \
247-
SEC("uprobe") \
316+
SEC(UPROBE_SEC) \
248317
int uprobe_##name(struct pt_regs* ctx) { \
249318
__u64 tid = bpf_get_current_pid_tgid(); \
250319
__u32 pid = tid >> 32; \
@@ -258,7 +327,7 @@ static __always_inline int submit_mmap_event(__u64 addr, __u64 size, __u8 event_
258327
bpf_map_update_elem(&name##_args, &tid, &args, BPF_ANY); \
259328
return 0; \
260329
} \
261-
SEC("uretprobe") \
330+
SEC(URETPROBE_SEC) \
262331
int uretprobe_##name(struct pt_regs* ctx) { \
263332
__u64 tid = bpf_get_current_pid_tgid(); \
264333
struct name##_args_t* args = bpf_map_lookup_elem(&name##_args, &tid); \
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/* Classic perf attach variant: perf-based uprobe/uretprobe + a perf
2+
* sched_process_fork tracepoint. Works on kernels predating uprobe_multi but
3+
* needs CAP_PERFMON in the init user namespace, so it cannot be delegated into
4+
* the sandbox. The program bodies live in the shared memtrack.bpf.c. */
5+
#include "memtrack.bpf.c"
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/* BPF-token attach variant: uprobe_multi links + tp_btf fork hook, authorized
2+
* through bpf() so a delegated token can load them inside the sandbox. Requires
3+
* a kernel with uprobe_multi (>= 6.6). The program bodies live in the shared
4+
* memtrack.bpf.c; only the SEC() annotations differ, keyed on this define. */
5+
#define MEMTRACK_BPF_LINKS 1
6+
#include "memtrack.bpf.c"

0 commit comments

Comments
 (0)