From 221ee830904a4458f30caa5f958da616673b3ee6 Mon Sep 17 00:00:00 2001 From: Matthew DeVenny Date: Thu, 17 Sep 2026 11:34:06 -0700 Subject: [PATCH 1/3] #135 translate pids across the namespace boundary so step attribution works on ARC runners MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Inside a container (ARC runners are Kubernetes pods) the pids userspace reads from /proc and the global pids the kernel hands to tracepoints and bpf_get_current_pid_tgid() are different numbering spaces. pkg/steps mixed them: the Runner.Worker pid found in /proc was compared against parent->tgid in step_fork (never equal, so no child ever got an ordinal and no step_boundary was ever emitted), seeding wrote /proc tids as keys the cgroup hooks could not hit, the reconciler read /proc/ for boundary events, and map_sock_pid carried global tgids that lookupProcessName could not resolve — every ARC event was unattributed with an empty process name. GitHub-hosted VMs run in the init namespace, which is why they were unaffected. Kernel-side identity stays global everywhere; translation happens at the boundary, in the kernel: - map_task_nspid (global tid → tgid in the daemon's namespace), owned by tcbpf.c. step_fork maintains it for every fork (a thread copies its process's entry, a process walks its upid chain to the namespace whose nsfs inode userspace passes in as pidns_ino); step_exit drops entries. - step_task_iter, an iter/task program: seeds the table for tasks that predate the attach and streams the process tree in global ids. The kernel scopes it to the opener's namespace. pkg/steps builds seeding and container-tagging views from it and no longer scans /proc; the worker's /proc pid is translated to the global tgid step_fork compares. - The connect/sendmsg hooks write the namespace tgid into map_sock_pid, falling back to the global tgid when there is no entry (step attribution off, or a task outside our subtree), so process names resolve again. step_child_event.tgid is the namespace pid. Kernel floor is unchanged: iter/task and bpf_seq_write are 5.8, like the ring buffer. The step maps shrink list gains the new table. Tests: TestStepFork and a new TestStart_TagsWorkerChild exercise the full contract and are re-executed under unshare --pid --fork --mount-proc, which reproduced the ARC failure exactly and now passes; the iterator record layout is pinned against BTF; the container-tag tests drive an injected snapshot with distinct global and namespace ids. Signed-off-by: Matthew DeVenny --- bpf/stepbpf.c | 149 ++++++++++++++++-- bpf/stepbpf_bpfeb.go | 14 ++ bpf/stepbpf_bpfeb.o | Bin 35352 -> 43800 bytes bpf/stepbpf_bpfel.go | 14 ++ bpf/stepbpf_bpfel.o | Bin 35352 -> 43800 bytes bpf/stepbpf_test.go | 126 ++++++++++++++-- bpf/tcbpf.c | 39 ++++- bpf/tcbpf_bpfeb.go | 4 + bpf/tcbpf_bpfeb.o | Bin 68776 -> 70288 bytes bpf/tcbpf_bpfel.go | 4 + bpf/tcbpf_bpfel.o | Bin 68944 -> 70448 bytes cmd/start.go | 11 +- design.md | 25 +++- pkg/steps/steps.go | 323 +++++++++++++++++++++++++++++----------- pkg/steps/steps_test.go | 266 +++++++++++++++++++++++++++------ 15 files changed, 803 insertions(+), 172 deletions(-) diff --git a/bpf/stepbpf.c b/bpf/stepbpf.c index 46bd2db..b1559ac 100644 --- a/bpf/stepbpf.c +++ b/bpf/stepbpf.c @@ -14,21 +14,41 @@ //go:build ignore -// Step-attribution programs that require kernel BTF (tp_btf tracepoints and -// the cgroup/sock_create hook). Kept in a separate collection from tcbpf.c -// so a kernel without BTF degrades attribution only — the firewall proper -// loads and enforces unaffected. The three shared maps below are declared -// with identical shapes in tcbpf.c, which owns them; this collection is -// loaded with MapReplacements pointing at the tcbpf map fds. +// Step-attribution programs that require kernel BTF (tp_btf tracepoints, +// the cgroup/sock_create hook and the task iterator). Kept in a separate +// collection from tcbpf.c so a kernel without BTF degrades attribution +// only — the firewall proper loads and enforces unaffected. The shared maps +// below are declared with identical shapes in tcbpf.c, which owns them; +// this collection is loaded with MapReplacements pointing at the tcbpf map +// fds. +// +// Pid numbering. Everything the kernel compares or keys on in this file is +// a global (init-namespace) id: that is what the tracepoint arguments and +// bpf_get_current_pid_tgid() hand out. Userspace, however, only ever sees +// pids as numbered in the daemon's own pid namespace, and the two differ +// whenever cargowall runs inside a container (ARC runners are Kubernetes +// pods). map_task_nspid is the bridge — global tid → daemon-namespace +// tgid — seeded for pre-existing tasks by step_task_iter and kept current +// by step_fork/step_exit. It is what lets the reconciler read +// /proc//cmdline for a boundary event, lets userspace resolve the +// process behind a socket, and lets pkg/steps translate the /proc-numbered +// pids it discovers into the global ids the kernel needs. #include "vmlinux.h" #include #include +#include + +// Inode of the daemon's pid namespace (stat /proc/self/ns/pid), written by +// userspace before load. Picks the entry of a task's upid chain that is +// "the" pid userspace can act on. Zero disables translation outright +// rather than matching garbage. +const volatile __u32 pidns_ino = 0; // ---- Shared with tcbpf.c (replaced at load time; shapes must match) ---- struct step_state { - __u32 worker_tgid; // Runner.Worker tgid (0 = not discovered) + __u32 worker_tgid; // Runner.Worker global tgid (0 = not discovered) __u32 enabled; // 0 = feature off, all step hooks no-op __u64 next_ordinal; // next step ordinal, atomically incremented }; @@ -54,10 +74,19 @@ struct { __uint(max_entries, 65536); } map_sock_step SEC(".maps"); +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __type(key, __u32); + __type(value, __u32); + __uint(max_entries, 32768); +} map_task_nspid SEC(".maps"); + // ---- Owned by this collection ---- // New-step notifications for the userspace reconciler: emitted once per new // direct child process of Runner.Worker, before the child runs user code. +// tgid is the child's pid as numbered in the daemon's namespace, so the +// reconciler can read its /proc entry directly (0 if untranslatable). struct step_child_event { __u32 tgid; __u32 ordinal; @@ -71,16 +100,64 @@ struct { __uint(max_entries, 64 * 1024); } map_step_events SEC(".maps"); +// One record per task in the daemon's pid-namespace subtree, streamed by +// step_task_iter. tid/tgid/ppid are global; ns_tgid is the process id +// userspace sees. pkg/steps builds its process-tree views from these +// instead of scanning /proc, which cannot reveal global ids from inside a +// namespace. +struct task_iter_rec { + __u32 tid; + __u32 tgid; + __u32 ppid; + __u32 ns_tgid; +}; + +const struct task_iter_rec *btf_anchor_task_iter_rec __attribute__((unused)); + +// MAX_PID_NS_LEVEL: a task's upid chain is at most this deep. +#define PIDNS_MAX_LEVEL 32 + static __always_inline struct step_state *step_state(void) { __u32 key = 0; return bpf_map_lookup_elem(&map_step_state, &key); } +// ns_tgid_of returns t's process id as numbered in the daemon's pid +// namespace, or 0 when t lives outside that namespace's subtree (another +// pod, the host) or translation is disabled. Walks the group leader's upid +// chain from the innermost namespace outward: a task directly in the +// daemon's namespace matches on the first probe, a task inside a nested +// container (docker under the runner) one or two probes later. +static __always_inline __u32 ns_tgid_of(struct task_struct *t) +{ + if (!pidns_ino) + return 0; + struct pid *p = BPF_CORE_READ(t, group_leader, thread_pid); + if (!p) + return 0; + unsigned int level = BPF_CORE_READ(p, level); + for (unsigned int i = 0; i < PIDNS_MAX_LEVEL; i++) { + if (i > level) + break; + unsigned int l = level - i; + struct pid_namespace *ns = BPF_CORE_READ(p, numbers[l].ns); + if (BPF_CORE_READ(ns, ns.inum) == pidns_ino) + return BPF_CORE_READ(p, numbers[l].nr); + } + return 0; +} + // Tag propagation. Runs synchronously inside fork(), in the parent's // context, so the child's tag exists before its first instruction — step // attribution has no detection race for host processes. // -// Two cases: +// The namespace bridge is maintained first and unconditionally (not gated +// on enabled): userspace relies on map_task_nspid being complete from +// attach time onward, so the seeding iterator only has to cover tasks that +// predate the attach. A new thread copies its process's entry (one lookup); +// a new process walks its upid chain. +// +// Then two tagging cases: // 1. New process (child tgid == child tid) whose parent process is // Runner.Worker → allocate the next step ordinal and notify userspace. // Worker *thread* creation deliberately falls through to case 2: a new @@ -92,22 +169,33 @@ static __always_inline struct step_state *step_state(void) { SEC("tp_btf/sched_process_fork") int BPF_PROG(step_fork, struct task_struct *parent, struct task_struct *child) { - struct step_state *st = step_state(); - if (!st || !st->enabled) - return 0; - __u32 parent_tid = parent->pid; __u32 parent_tgid = parent->tgid; __u32 child_tid = child->pid; __u32 child_tgid = child->tgid; + __u32 ns_tgid = 0; + if (child_tgid == parent_tgid) { + __u32 *pns = bpf_map_lookup_elem(&map_task_nspid, &parent_tid); + if (pns) + ns_tgid = *pns; + } + if (!ns_tgid) + ns_tgid = ns_tgid_of(child); + if (ns_tgid) + bpf_map_update_elem(&map_task_nspid, &child_tid, &ns_tgid, BPF_ANY); + + struct step_state *st = step_state(); + if (!st || !st->enabled) + return 0; + if (parent_tgid == st->worker_tgid && child_tgid == child_tid) { __u32 ord = (__u32)__sync_fetch_and_add(&st->next_ordinal, 1); bpf_map_update_elem(&map_task_step, &child_tid, &ord, BPF_ANY); struct step_child_event *ev = bpf_ringbuf_reserve(&map_step_events, sizeof(*ev), 0); if (ev) { - ev->tgid = child_tgid; + ev->tgid = ns_tgid; ev->ordinal = ord; bpf_ringbuf_submit(ev, 0); } @@ -120,14 +208,15 @@ int BPF_PROG(step_fork, struct task_struct *parent, struct task_struct *child) return 0; } -// PID-reuse hygiene: drop the tag when the task exits. Unconditional — a -// delete on an absent key is cheap, and gating on step_state would cost the -// same lookup. +// PID-reuse hygiene: drop the tag and the namespace entry when the task +// exits. Unconditional — a delete on an absent key is cheap, and gating on +// step_state would cost the same lookup. SEC("tp_btf/sched_process_exit") int BPF_PROG(step_exit, struct task_struct *task) { __u32 tid = task->pid; bpf_map_delete_elem(&map_task_step, &tid); + bpf_map_delete_elem(&map_task_nspid, &tid); return 0; } @@ -150,4 +239,32 @@ int cg_sock_create(struct bpf_sock *ctx) return 1; } +// Task walk for userspace. Two jobs in one pass: seed map_task_nspid for +// every task that predates the tracepoint attach, and stream the process +// tree (global ids plus the daemon-namespace tgid) so pkg/steps can seed +// step tags and tag container subtrees without /proc. The kernel already +// scopes the iteration to the opener's pid namespace — only the daemon's +// subtree is visited — and ns_tgid_of filters, defensively, anything it +// cannot map. Re-running it is idempotent, so container tagging takes a +// fresh pass per call. +SEC("iter/task") +int step_task_iter(struct bpf_iter__task *ctx) +{ + struct task_struct *t = ctx->task; + if (!t) + return 0; + __u32 ns_tgid = ns_tgid_of(t); + if (!ns_tgid) + return 0; + struct task_iter_rec rec = { + .tid = t->pid, + .tgid = t->tgid, + .ppid = BPF_CORE_READ(t, real_parent, tgid), + .ns_tgid = ns_tgid, + }; + bpf_map_update_elem(&map_task_nspid, &rec.tid, &ns_tgid, BPF_ANY); + bpf_seq_write(ctx->meta->seq, &rec, sizeof(rec)); + return 0; +} + char _license[] SEC("license") = "GPL"; diff --git a/bpf/stepbpf_bpfeb.go b/bpf/stepbpf_bpfeb.go index 085527e..6f8a2ae 100644 --- a/bpf/stepbpf_bpfeb.go +++ b/bpf/stepbpf_bpfeb.go @@ -27,11 +27,15 @@ const ( StepBpfMapMapSockStep = "map_sock_step" StepBpfMapMapStepEvents = "map_step_events" StepBpfMapMapStepState = "map_step_state" + StepBpfMapMapTaskNspid = "map_task_nspid" StepBpfMapMapTaskStep = "map_task_step" StepBpfProgCgSockCreate = "cg_sock_create" StepBpfProgStepExit = "step_exit" StepBpfProgStepFork = "step_fork" + StepBpfProgStepTaskIter = "step_task_iter" StepBpfVarBtfAnchorStepChildEvent = "btf_anchor_step_child_event" + StepBpfVarBtfAnchorTaskIterRec = "btf_anchor_task_iter_rec" + StepBpfVarPidnsIno = "pidns_ino" ) // LoadStepBpf returns the embedded CollectionSpec for StepBpf. @@ -79,6 +83,7 @@ type StepBpfProgramSpecs struct { CgSockCreate *ebpf.ProgramSpec `ebpf:"cg_sock_create"` StepExit *ebpf.ProgramSpec `ebpf:"step_exit"` StepFork *ebpf.ProgramSpec `ebpf:"step_fork"` + StepTaskIter *ebpf.ProgramSpec `ebpf:"step_task_iter"` } // StepBpfMapSpecs contains maps before they are loaded into the kernel. @@ -88,6 +93,7 @@ type StepBpfMapSpecs struct { MapSockStep *ebpf.MapSpec `ebpf:"map_sock_step"` MapStepEvents *ebpf.MapSpec `ebpf:"map_step_events"` MapStepState *ebpf.MapSpec `ebpf:"map_step_state"` + MapTaskNspid *ebpf.MapSpec `ebpf:"map_task_nspid"` MapTaskStep *ebpf.MapSpec `ebpf:"map_task_step"` } @@ -96,6 +102,8 @@ type StepBpfMapSpecs struct { // It can be passed ebpf.CollectionSpec.Assign. type StepBpfVariableSpecs struct { BtfAnchorStepChildEvent *ebpf.VariableSpec `ebpf:"btf_anchor_step_child_event"` + BtfAnchorTaskIterRec *ebpf.VariableSpec `ebpf:"btf_anchor_task_iter_rec"` + PidnsIno *ebpf.VariableSpec `ebpf:"pidns_ino"` } // StepBpfObjects contains all objects after they have been loaded into the kernel. @@ -121,6 +129,7 @@ type StepBpfMaps struct { MapSockStep *ebpf.Map `ebpf:"map_sock_step"` MapStepEvents *ebpf.Map `ebpf:"map_step_events"` MapStepState *ebpf.Map `ebpf:"map_step_state"` + MapTaskNspid *ebpf.Map `ebpf:"map_task_nspid"` MapTaskStep *ebpf.Map `ebpf:"map_task_step"` } @@ -129,6 +138,7 @@ func (m *StepBpfMaps) Close() error { m.MapSockStep, m.MapStepEvents, m.MapStepState, + m.MapTaskNspid, m.MapTaskStep, ) } @@ -138,6 +148,8 @@ func (m *StepBpfMaps) Close() error { // It can be passed to LoadStepBpfObjects or ebpf.CollectionSpec.LoadAndAssign. type StepBpfVariables struct { BtfAnchorStepChildEvent *ebpf.Variable `ebpf:"btf_anchor_step_child_event"` + BtfAnchorTaskIterRec *ebpf.Variable `ebpf:"btf_anchor_task_iter_rec"` + PidnsIno *ebpf.Variable `ebpf:"pidns_ino"` } // StepBpfPrograms contains all programs after they have been loaded into the kernel. @@ -147,6 +159,7 @@ type StepBpfPrograms struct { CgSockCreate *ebpf.Program `ebpf:"cg_sock_create"` StepExit *ebpf.Program `ebpf:"step_exit"` StepFork *ebpf.Program `ebpf:"step_fork"` + StepTaskIter *ebpf.Program `ebpf:"step_task_iter"` } func (p *StepBpfPrograms) Close() error { @@ -154,6 +167,7 @@ func (p *StepBpfPrograms) Close() error { p.CgSockCreate, p.StepExit, p.StepFork, + p.StepTaskIter, ) } diff --git a/bpf/stepbpf_bpfeb.o b/bpf/stepbpf_bpfeb.o index ea08050e14d2e11a4efacbb7683efd81ffc0792b..914e1de0de7698a7a8cfdeafb2752db8dfb2fa6b 100644 GIT binary patch literal 43800 zcmd75dz@WWdH=u8%;Z9lBtQ~E2yntBnLr4+5yC~j8%VgwEry8kf@jXmoXH$AGiNww zCKt7JtB81MrS(#@UZSF+wUt_1X!To;Rx8$O(ORX7Hnv`9wcnyrrS+2E`}3@4pR;F& zeC_Y``{%b{@AY|}_3U+d*0Y{<+51dhw(a_>dU{Hs6H4J{;2fiq0(jq}MkRh<97+eO zVXmkH#~UwHSIpibH1wXsKU55Tsl`j2|L((&J8k@zy!-IOqF?f!!;dJw^@MPY;)l-B zJ%l;s5Dp(c{KKUSjqKcB6;Ww%2)%+;Pak^s;qQok&cg6)#l`2UUeCn~!!1gGz@D4E ze?e6g4oQ?%AU`jJFx8VQ<9|J zZN=1Mo8p*X=sR#*^=5xr9>n#W8=+Zy;;eOFnSe`fgT;{q@kvCLeZN z`$Ku3$7e4L8%a5Rj^{UL()4inz&Y{x^G*oo>&e~l3FUnrKlEy&!|v4LzE1tk8anNp z`)k@WH){`iH9dJd(qHpjM-Nrk@6vLtPj;`K5z(n9MBAPf4*!!lnVu-GEjlY_!OVJh z_($pABg)V0kp4YZ`4;T?@rN!J7yDjk`u$GPP9I1tIk-N0=(iqmyuKmJKS!Is3gtXi z%-8eNxLK?G7&m>2splG}6$SeH5yiKz2+I|tR~xtRZM?!a3-aSyDA$URqP2hi5OhZaf-{go}sp1%XV9w$ygZ~r%c zGv{gKA1~JTA+Kk#T@S?K!<#+s_|4-x{_z)<|7}rV&Vbp+FOJB6WlVQH^!~c^V!02w zzsuRr6ISj_zxzZn-<BL_P26^M?)|j_sJ4-x2M|@_T49wrBBVm}m7^5@}Cg z`;vo)|3~$E=)(BiQS@Hw=zT@>gW0^ZxExw~GV{yDcIW1e3CTsh&EAjSctmvOH;&>T zI)2Tc)4XBh?NWA(fNOmyFk2MSK7C9e8mZms$INO}-s})tNV58}s-4 z1{U0|+$rzA|M9$W9pc@2BRyi?xLW!7{7~LUI-&f~sKsITfW>{C`frYWug}rujU)AR z@r-sHZQjV9>*xW0aY!W|ZQi)`gcL1C!;$6Q7V{l?|KTT9{^EgP zGgW3+)e}2@;gHfFuyHc`QuwxhnmG^5z9y^Bud~CB{rPw)$UpA(*U=C4v-R9VGyT$` z!aVk5aojxQeyTVgvFEnhlli|a?#QoGYFOy@|FWY0vef?{-mcz$z18Xm@3M(#X{_Qk zM7p-vtZx%}T(_2dy6Ph?OV$>%0&%|A+$E7&nH;#x6&C{`%m$u3R}7XCT&EvJ&eqgN z=AG&}O^_<5%%wGeXA!iboyC=KUEnw+zJyCfwz;mJ&y66TZO?hzY)(D0ytuY^nFT!a z9IyLA@C&?Pu@bJVjVA~nk(Ub>>i!JL5>$6pE#WHG{djn7t_iQZ<}J&uke8S6KhncX zJ+H-CUfxFQ|0C+|O>-ITI9^BD5|bX@!liyycn_Cwnd?Jb(pkb)?6Z@^BNY2gYqXS0 zeWv!bxgPMkp5nYr&!<5jspk+Rr}9)?)lW8ZDwl%tl|B=$zxI4j2b=En071`aktLp3 zU*%fC^>sQ+yfcVia7PHg0;b^I@XqEUlr$$}*QJ!}C5{h*UkyH&xXtxjVCtl-OK;?c zd`}ttR>ukWAQ*w30q_UF>%b}aqhREv`qE!=TM16UUk2kDdsFZ?!93gB2LHR`L*SoA zjAlYfYaL|z%0z0Da6+VUAUr^%zM!nW6s-1biCx>%58fKhwTCu^tLSFQtOf51;iLqN zY2^# zd9Esh{|HPzs}k^MoZkjN8u?Ov?++utKLsCle2{*UIa8nhHh2Y?d{z_nZE&1|6EJ0+ zL)3Qzw{bAMKKf1a=L*vgVFKI+ldirykEriu+!|oXnz86gXYg6z? z93KS#x#Kq2+qL!(`0wGPpYw_OzRqn9I01i8xD?JG0IM&AtGM??jt9Z&K=D&Bwr}|5 z;Hw-{$64DQzY?r|k^CLtQOB2j!B>OLC3EJf6Vdgz@G%G zEp6}@!MEG64YM8tQ$_m&)jhL*%#Ai|NWi~xoHE{Kf$5hGZSeevHzwdE zVB|N7FFhc$iD>q@j#IG4AZ@)s{42T9rVA4A4PfLi6n-9<_FdQp4})o+9-G~CoPepj z#>JKt{5xO_cuO1ndl6qmG+X+Ce^CN{FBsiil!EUE|1KsZ{-1#{m`mE=PlIVY)d;iy z#&H7v5}150O~H>jZi9aSM*f*Zvwz|^0skEQ7WgS$bu9SpA;1oE%G}-`!sTtSj2CYPVF@%3{7!HRUJU+Qa2tFk7@NM9Xzm(r$X{Cq zZw7w>oPaL_{~fpuz6rd_@rxpU7SY^2+#U(xSqXSA_;ug`@Na?{=g&&PuL8dn+y>tb zra!Jrz|zk@fK%{?z|`ToHkkgA?Ojha7g}@i^$GZM;IDyG@E5_4g4^K7!2cM+4McOl z&kdh)LjwL882#LkhH%Vm@HfD1@QL7WhHxX%F-y5o?oHyK0j7=H#b4(*0m}fzPr+*R zx4>=ibHRs#9pN4`3hn`y;irxhFzpWi3?JSx?*?BAZi7Dv{+AGTB;Y>%<~c<3)W76&i||Hn-v_5)wdHYe z8@$c&BjBB2^19^^co0nfYTmq-<1+XzFfz|gz`qYhx6e(%e*|6yZiD|k;$1}ZzU(*w zQ)YMvONA8tWAML*@I0b(_!V{c25*@2?N_%ci!Jh|{=dEq9`cC)|7~QKrFCsejzasxdNeIuF5B^X1Dfm<{ zI{d1z#-nf>d?lECzDD#6wNbbXu7a_HuO(o$aUnPbzZAR{+y=ka@geX#!PJ2v6P}^A z{0y9cKMsBhoPxgyCjUndg1-fx18#$nmHa;v9oNT=HvD4(o(KMKa0*@u{yDe}J`K#X zUne?FZ4oYmF9ZJqoPe(Zzr(QxI&xoc!l0KG# z?}5*=kF~)c0H13^I~;dEGUW5k1pX($z2Fo~f0by%Hzofln7XN{$9)4l7c6;fvxF^v z3;EJ+gl8kL4*W)9bf|FL6WmsDC0q*qTq%6|M0E&l@ZpH5YM8G)u+>rmo+l20^7Q=t zr2>Qny3MloD{cOANR>gD&$D5*L2lHEwwcTZVxx#8T=X7ud<_lol2iY+E>W% z#Mtcl-zMoIrNjRow~M(_FymZ$x4wGHG5iHoNj|mI=2AaXr=A49%0Rv+1)t)$4VLZ0 z$MnL2D;y`_>%feK-W0su`E9UlA9-pR77TNv{(XtB4KD|$@NWyYsOf8iZ;x2jTCkVf zN@Nr8o!~2@*)Di3xw#$|yd}zT4+|K3W-AH!K4etMrQrLC$wy(qr?_3hm4H9*I3>0W ziO|=AzwhR6efdnK!~Z5X;e;!bM|TVU+572#fxqV%nFT-eKATINe?~U#&zJ5+?x&Q; z*p!TQE&5Vecsw`Z1iZ{~3Kmaeq77c>_z;-#)lQYV@EXSn_&JVK@Qc86Vt+4`9{|e_ zEZj#-TNDnGjSRIdteAFpAUhzffHhHqxRSfKXOM1i(p}U z3+^M9T@n^P#7*sKga0OCtN&LWC-4tBPQl-E+y?&$jLr2BW%k{ZP`1V>_3TNJ`7hy8 z=sC!7$!y2(21~c%Q}^S~bo^eP(KrtjJQwuDlqfbY~gFcH(PPx_~(P4 z2TtIRfOmmY@K|5RFCISyUwuP3{s1?gS9_1YliRbR-j06*_(pIF|4rbVz-{o`96t=E z4aR>2{6WW$g8!JZq>wULWx1Y@|0Gy-ZNvZETmS*xMPVY1-|VbNbC<#m$bM;d6d~K= zza81f|Ae?Ctbz#WD8r?Xgm6Ny=f531+wrr($J#^jS!2iNbuM&5c9y!co0l#6vO3)& zJ^`JcaK7jHT=1qWPuFo)r>jW2*yYqO8UF>s$Hh9RPZh|slYXo4Jl#U+3%Yw9|Cao& zyCXU>in7h9ZPegve9h@)Aq1`nQ>Xdh5yuON%^4{sNJe>(^$9J&sj)gIkiCrsq7!5T ztGE*IJ&sc_Hkyr>6FwZVG;qS7J5Io#cASE-56PF>;ID&yyqxgE$nQzuKjk=O8c}m4 z(*`e!n3jbTsiX4gO~9&`a0-@v3Ae%OBQPxrC$=0X;MY4&!S8n527CQy!8-91kv}T| z|DEF$e8_Pd{NsqR$goI-P}b}Oe7fTlta(@bHu#E&=MXJ=4i89Cn8cz1;vp^zYB1qd zx;d^1M;p+Y+M*G+qtTp<_>-Pz0^UzrUFizan1_|bs;kQV4O*%qpclP{cn=q0(d)RK zV~`t(lIOC$^G%VzNb3<{<@0983&HPnyp-7MU-}}C-{AR_k$FE!ss~}wA9CZlkboa> zoPs~?xD9@UXVrv$RQ3yESIZ*SlQu^r$b8pvn>Z`?z8t2^Szq*gwjlo>q-AxGPi}H0 zTnc-+QUNbWwv&xf`7niP^7w^1kWidYZx<8YdYuyquf*BBAYXbUXmR>^KEuU$U{#2CpIJ*;q5Lzx5^g^S{;~i#HO#m`g$O zFXbw8Df|Xkf^YpKvGPojzl2U!cnLUL^OE)AT~t_YB;Um&CJ`1-5L5VK)!&MD8zYOw zuYmWPF86BUmqncExg_7x!&PuKne81y1 zahB(o;XmkYc_sKS9a}x^7E1K*;xACHNbJ(qTSLkN{-fOP+e#$gKn7q92*!9V6%^sD?AA12l~BrKWh zGQSO8+Lf`yH`tw<+oYbAU9HXd#CPYg>SO^y>49s>8N@#J?R?C~zEwmrh%Q-8yormj zWD7T8bU6Ed66b)|!G9*P`jW8ZDsH5Q1bcfEvHVDi&Sw)#{$Nf&?XG^*{Onw`l#tJM zZ##J`c_wAKewJu%RX&6z!(=L3QavY)6_&{UkX`axVzp6qpkJ1}ix|1tKj)@?+79mz zh<~4pu;h=qk#A1|e#r3vII|o2b;-Bkui_#s`B!fDa3$a;yneL%Bv?ALQ#ffZeCaXc zzs=a;Bne%@l}O&7rMb3d72kp?>EZW?5mUYA91BnqeR0y6#2PDvllr;6n=1v&253u3 zcnk7g2eW(ma>7aKC+Q^NG65pXOTvVMx*ZqwE~vn>C-svb|A#X8I^y?m5l(tGH`#Uq zRv*2OYXDp!zLYBkXZz##g7zf=adL)3Qy{yZ34Dy86WIBtXgHDcBLq@OuX%w3p#PYQkpeDv7U1}_EwN$l#S z>RaIiKK*Vu1z+vBO;1S|=fO5pGJ zevuBcenG9~TmC>iWM1Yn_`s#Fay$q87RP@iDISudvS+17!qRuR%>Cf^=AQkS+T^H1<{{cih&&x(gT>~R^MTehUD!~C4xX$yX$gDp?Dw?jF;%FlG7`eqnk zxU65b6^od@@1*^67svJa)7YZ1yu5!QueH=wJXp)FB>q3j1AGm)TMUL}&vi@_mkl7_ z37!wSU8eI^-6Vxo(($m;I7_PNdsYK6^Tr{O9R8@JV23_0=h?A4=HDBH=SgB>zPR z!RLX`1-HQ$M!cBll*^Gno_Q((Z*`o4ZzK`DE^dQg81WLKQ+6YB0_&Ls%p6?84o(;U z<=iOi{1lAtOSi{wBTjh-xP^)&ula2ixDEbj#2blD`QMHc@TVN7V67F8gB}HcftdOz zobn}Z!U-5X8&1K@C5GEz`8#B!!c)HOI04Hh#ZSTL)NmX8?-BP9o$@2c3HU!9r{Jd? zx4}P;SVPk~3(!-BFZ=U)E=M1RlMt4pW9cfS;L~Uq|M$>O%h$o551+7nGq>5%^(?={ zaT$Js%mVnt%db<52t2F2X4s8%MZTfTm9r=F6XMgjQ=`ha3kC0zey|w z?Ak^e=g!auon_ai{R@$!cB1m%Dz@U()sf#TzVu7oC^4LR zh2t{#2FD3_7nnBp4bT-BeCb4R3cn6!!1T4jzzB;C+?rd(;bj>JQ;Fz>ZJBPr`o>xDEa#nEISRbQ*OxoPf^j5XeCl(%ne?5HU7p3eEt^;3)?;tu&`XtZA33v#+Hu{d!u({GEa0(w^ zp=X!0!7m1{B##6vy^;S(DHy$#E&#W|_khWNsrc_g2AwYz|BoCW0)IT>lZj6IyyG(X z8({K3IRQUTK3l*kSo(PfJ{D}BDU;_Zuuh+k40Sst0heJ=|5H-%8u-ryx4{>IFT+m7 zmmbJ}x%jujM{lRXI(;|ze}NO)K$$A*)D(UL{^gV}nI<>v;j{!i4W^GyOTn)|<_d5d zd>42tG0CUDmD{<*3D-xRpMoC-Q}*d?t}h@%o@JubABF!Sa6+5uqf!N&!v8jS0Ne)4 zW~kFjaJDDo@o>6)B6?Vrfd30T8{8&V2N6o2;`!b?qha@f7xDYVUl5-|{7d45_*cZw zB|c0%W{-t3kAxERJLejsv{Vrpz|(wLz7Ke^C57^bTxQvkGTC0a{LwqCp8C$s&Jn4n zLT8OsCQs_x{Z^U$mqVxUKj-{5_#YxxHOk)>E`?G8mR}OS9QE7xaYEP2hdbC+`(6cf14q2FEvp-|l!P_(P7L1O6+=w}8Ly__^Rij(34Gojecx zQ|CXQ?{DQG)!r9?nJQSmTJp5@4C-Wf z2wZbK44!ff?~IoUFAJp+VnXRut`z=jAh31%wvp$`;6v~~8%$o^e+FYsGQA1-d*BPe1K^)Ip5!~^KCom`u=FE*5WE6R zxxH<$*@=w^n6qNV7zSr_d4foJoIV*kn|;^4UX7Sqf*ixMlcd zIsCdVp7HD(yZkfXmc#GQ;XldY?7Iz=d*&ypz%xi|a}i2s#Q4m=BW65Ehw`!VZ_;7r zSEVaEmn9F=&sTZ&q^KX$$tp1N3Z-@6l(N1J0Uzy!GoNsrfPd~d4dJX~9k+=kDZLE{ zlh2tkHhgvtugqcc&qAp`Lp*zahH39S@BqHzQ^YQR)^f)Qbx_{QGX-lb2)Dr+3$(XH zbe8O2xC~}I8&06r7vc}Vr>{$Aai!qlh)dGCwBmSF-v-ZBjaYC#-C4;`sdJoyJ77zRZF#T?A z{|GX@VCsCE_v|I5g0oTT8d-4Q#Q$Ci^8&N-1A{QnTf;6=)=&Y@r?__KcE z7{6e1$T>8U@qd=~TB;lMKl>Q+*E0m2#gteBgarNqWTX?aHU4>E3wkhn#P)?QF`? zcu}4m{i`f|bVp{?aYAeb@yuW4@Mn;pqOye2X7HU}7VWZkM2}cfIQuQcvWG;pDvbOm zkk@x$>eDi|BwxLbi=Z?8(nWw2nR~%{w#}72d$I6wBq)xdbctfm8-1PqVenX}2+rX;$d%>!*EyUB7s@yD^78lRWTkV-Dxmwbzv24YCWgz? zUXzh7JZQbBP`cXVor+!l8uIxG?Z9@_#FfCT&sHAqIDt?_?oVa_seIM z;`rel{#+NU-YdUBi@e;G|A({%N{^wmJwwz@3Z!l;<(t$f!b;6m>W?xiP#(gJn^nhj z^R#yj`8;;{jflXa9UjQok~{r;{;t zyUzbuGLG9h{7I^(cR_M%@XcRWmG1N3Fd(6fHxgqDA*H=^xz=!!@0Z^~z69Bi-IPYT z67V|W+bB^o_;~TvivBCPkxyR=zR_{J>%Zxp(9gc1v?uz6{@qb#7Bc-UZ|@+ytSv*r zRGubug6gT@aY=0;lxn)W4hC{~guGu)2D9N!(*Npnl}qUVEpFt~lh9ZH={SY|Hpgx6 zLuAyx&KY6?k?#}It&SLN|2rs4!sw{~4=JlIS@2&tPQZT+P z2dJ~QgI~$x%_y__F2^Z+bSJ)SWA*Pk7XSAmRw=9BPCf$4^FCtfiLm+)xCtlVKSD;C zNG4a-ES5Nw z%byamI)pa-AsDo!lz?vsUkFaY%%}Q&vg(|*SJkj{J<9D#ka-R1FO6-<-nsX*nF2Hp zm`By3HE-a?gT0B@`8U8R{I^7YAN(~H>M3F7jx`@7&liIUYwqVp-9iffbH@k4pK^Kn zYt852Kf*;=^CfOiah1VeCzcOMxLj$a2XmP8HUCUL{BPy{koY}HhyNsX(0U>PD?hJ8 z#|93<%ktEo*EVT9^C4jrULvB+cCNaDVfgZA7*BfbJoid|2BH38DwU0|e?IYSrGs~K z6E1Vf=7baYj4#6jVC=?l3Z@SX9|XV3aT~1haR>Av@S7vQGyr}leB@N0^&fElLGVYS ze5vjGWyd07>%>#=d}L!enfx05h`QWG9%p-9(9fpa?-`U!>2BTO zW4~GhsQgWzbet0B%GreNm3~J&1+U9{h3DxO=QLYIK%lElUr=Ym$5&Z<^176LyDm3< z&E?c*YIm0RJ;I*%TgmT`>khrlIz*ECgRtpG+=R>E|MI-#JMwwG9XiVke{{iO@>2Qq zgRN;juTH*YWX>dA`~=+ZcmTZ4%lLhGlySke3$iMQ(%axEP{$H>X90Y{jmXfg7LN6} zOuQ(Em*lYK5QVZ|!qoMGo#bQfB6^dU1ln=I3v+z=8U^u(=S0kX!EIfEa8Y zmBYJoxR%4M9DZpIzcz<+ZNKnfj{lJ^p8Lfd{(cu<_)rc%lEaU8@w^{*@v&RG_`;uc z@s>F`yflZ;%Hg$LeB3uWxIQrzDwWH3?6~}Sm7A}9{Lg$&u-q-eRixdvAe9B ziM1L-F4lQyqBU6`4tkX5t5dbGuhHC7YgVRq!>&zK2gho|VWPHws?ul<*C(oDF{7@$ zaK zTpOw$sL=MYYHO+@`C4e!hNxs^x%pnRx+l^+eGSL{WMKsiC?5R6$3=K_BQb1$EpD?BD zZZ@VT!!%EiPga_>+Z|e6XnC_%8=ssa$Eo^Az1D1nLDL0AQ|HE5eds`#XsSW2N_A|k zu@B=g0+pdc?50B*7{@Ndczt)%>QR}jO<+UVVuv$aZ$%U9vSJ@Jho-3>{Wd0>ov5{@ z^k9cOJz@OTWNoO9O^Poqct438lmN;%<%pr?w<;5Rhg$ngKGLI&vEjz_RE|?0-amsH zt62wjv+zeMWA#@4x!V5vR6){MCN|X^vgV+ntSw^IC+n4|hCP*&$XX@VjKahi9NXiK z>QJcKyV#7}8W!lHQK>ezyGvq6QDA&LREMVOdutWYNOgK_s#VFlqr#}3l)|kAah$~V z$1YhtRwSFX@y1lm1{SvKys64KPwvNaRBHQcLwjpN^~SzxeM-oT(OM;HtT|nY z#01*5F;MAPTo`FKUR;}y{Z__nH$3jrsZQ}!sgsYh6V8_)z+w#Q=MvzGk)D* zG0`d`*X%CHshSIgiTw^$C#uZ@I0X%Hb6>$Db6EJZ(5erPDbrZFF-)JK9zD^p$8O#3 z&{PB$W;n5@m3cX{_Pz3*mEp!j4flaf$|qK)5Fa92hG>U&UylRYE7w0gRUhXOyYIF; z{$Q`H(}=xx$77K<(}j)gIGiTxHmafmOv-i9XiOP7j^0MbrkFc!vv{)6s_)M-#>*7w ziYn%froK%q!Bl4`-l}d^w@;&YhPmd(p$3*bQ5~<Us4>l~YW~_eR?m@E7^#mjsn#2n>884a`LWs9j~|W(PBdDb7!K}TVzn~DXO;SH zQ%&aVTGdvCNw0X#pK3DI)r@nkhxM!CdM|c;neGltNN5&z4@ZJmJrSyDDwxs zH$B*@PB6Zwn~du?nGT{Pb8eGk)41jub4q>OCsmTNHVjoKr>2>uL^m5ahd85*(;`i| zu@CCBlli&cs1Ak6R^7)`b!cd+(nNU-#r1eAsJ-JkLVdP>ayV$DcIKlzVe^(|B8CBc zI>F|YW@D<+w22FTYpgn0NU$-;N-B+npj)kr;cN|nyMn(C<_JI5(eWZ0$g=N!Zo{qvf0n3~=_$*x4TA{XAGxkWF`fOD-F+EFnD z@-9V2^#%*MPI6>0i1$aP@~Jf$X%!in$+;J1-kQR9+4M@aF+J-tvssgy9AmXKP9wZ8 zr^ln+H5p=ZC?;eh+E8497>u({m}=l=D|t>1%Ua`1V$4Zw279mtgxrD_Cc0I|@Sz46 zq7g*J#wEyjWr${3#m6JgT8Hz8!3-Mtl{hB`#G@CQ8XLs;heB(T)laQ5S!W?`Z69e? zcL$p+T3Qng*PByyYSe{zdJHcp;XuS>IZ8FhFqVm-11Of|8_PrYMR{hB=9;oit$tdR zW4gkN&tMNtH=F7o86b<;4vBxB8f8dk2_tdL>FL;ok#4Y@q?dvyoLz-=+T^HLJ4#T+ z>83o7#u(!PPta(LI{~Wf9`TtVri(?Jn7Sg3He!#dsLCkbv$N<>exWgV8+T?jCJNRn zI5U>wQZF74*Ny?n+ptv5QV59z#=2Tm0FAO;bKz3FhzrOb~-LG{bWf>~BJIZ(Ity z{f+zF9HyoQo9kG_UJ^L8jN)>e)_B8#soeEyQCLoMM90tTj<%l(L}G3>dUi z%&J;pIqJ2hqS`EpW=@Igl&SbUeN$mU!qm~C&r!Oq8OF@Nw3mAE)lzo9T&(P3XPBNmFotX0^&Vnlm0=Ma}WRs}8NWXQ^< z=tI!J-pUc&PA+??U1({5YW#Ipb{aPt2ty++ZMwz}RB#s46Sk|Z?5kmoQ$e<*ie|L4 z>l1r3PIE9epi%+FWgBC5j2Q&FX_)}VY9r{4EkR|JhN=Vih88`^mXmvTA09PirE0f0 zJ;_>yMYnMW77hMH#mu>TCeK1NZpH7QJm~;m)+Pmvz?-g~3b+*dX)@PrV z55tY!(mmc?Y#fht$$H%+2jyI1#o0c1KA@}>($fjrd`u zAE?*HhOuSfgf`t;{KSnSoXkf7<6z|wB5qqENx#N@ab=Vxm0U+FV@$GC)k=oJHi~n` zbJ{ExDy_x{v+`t}LBs?-L3TK}I4JE5q>j1NSobqPkyJcdZL*+O7I>v;A2V4Ndz8T7 z^FlN>&5EztvGdv+w(Y3gc+-w;H}AZB$Ig&(GJ1N8hS8SpC>o-m?pD9sq{>JbY_NE$ zT%kL<@HVT7r1^fHh7L}%>z&d9&?ho^MS5U_k(ceKsNEEXS|8FW1`Cr|TZ38;7>KSW zCS!YXW93to6SB$HSS_9$Y0w&04y0f~O3Aw17L#63#F<>AGCA`BmSnT7(2N1CfoF5< zNR@T^aBiqqsv~Nn#E(oP`w3gkKV9Nz1D{Glqd7DqM=`ZZp60Zt3oESy6GM607ZQ1v zEm`u6xP#4ey0!Hpy#AHy)Ra8`j3OsF>S3dvf4)9E=I1IJlgjNa_V$xZ)7dODJ{TsWC7?m9NIPR&L{*#GGmb^Vh6FCp ziZ)lSBdzEB^!gO_bxT#}Kt7na)PX<@y zZeRn;p$@j~F^#IQ^J`Aj#wuNw9#GRNc4)eDC*$wNZ9(znJFmUP(3Lmcu>JC#p*cRq z1net%;pvfKRH2(W6Ec#6oMP&!4Jn#XtW9^W;7CTj$HtrQS4~5+x(_Ya<6#xEagH>? zgiJ9xsoOJ*mUM*WQRnn*3e(quD0YkuUei&=qH=M$ga@}hyT%>c`&;CVD%ak4`IV*y zC%0a|%~NiE-p!7lbMv+x4zGR=uFHHQXAJ10$wtfEuPmkBJdjZ{ckpqAqoqS_FZ#RP zrbK%@x=*7udvZWvvnVlZ5?j&uTsq#scx~4Zw?a1kOzSjSYZuK(aiW#~4vl>iICt|_ zrqV|1cBk-j+4@H|hi>9(X!9muW0E?VC$wIwSmlED+dBPj*~Z(>!G&r&&O&Hry~r7MB^+hp&dH8einJL3m(;|AD! zXc|$xcTyo^st-2WfYa1j_Rw^&NlT|K3=`HX6T?3CBa4%l)+k#na{_G6v=_%h*`;x1 zha{f(Vojr+LR%b^ zu6%@hZL0B!0z}Bw5OFd`_Pdw&U7o&6WaL<+L@1B2QMwemKm_18MYdj5t?6l z+Q+mCg73(HXfB`!2;=mq?KE2)ZZ~6k zYcIV&h3{`}sMN;Rh5hR<3i~$@Z&=T@fomhzCaw#(HgjFbwS^{OgiNY>GZx?FO-*ia zG^286!!tO0n>Aie!D@1&boL{hs`E;SU10XEu!0+!T2DDwZLhH8+_|lC<#TrI*mmR2 zF5{~0H(z_h)-HWBQ=;jJM>&FPO*3~+upCFpyiXjQ?7kHU`|`x-AXbRw?+HS-iO3yb zg}D2T_Tp#T(S&^UZX-VK{^}IB-(L!4uW;gg8Xxy3BEd?`vtzLmkIt$r&|*&b)(q~h zF(t&;Uz6k0flDvd?3|k=7E_HIQ9KQgb7?$?lDV_ViSMOytZWfEh4rvcB9~^+7+`GJ z(7|u!NplL8FuKo**Lgv#-ij(9R6p7wi+2s@JY-W_9xX3}O{SE85aJfllgn$-yo1oNy`g|6ouol{qt z2eyrqX>fqaslL`yOqDJAPpAC0W(?Y4j_@v%-PQ!l=;jfOj%#YL+qVtr3~}b?-SI#u zzI|n6$CCZjt$|JQ&WVA~DS_4_mW??$)(F`FaCAxAx9zy9vh5ZItPkI<+b-W4j^J+F zb?wfl;qKUR(~cwAH(hnrk<89A@QA#9dB_A=9kW$ZRSQs#;il-lPIb-t*{P3c9yfJH zmu*(hmSnt&=ChZ(zBFRxJfU4^ZT}EcmNqnocx`J-mW*((n~|e?J7Tu!kxlc;j&onn z)&{bFtDskmHXWFO;gnc}RrM~3gUmYb_v(|J^D;c*3!+$W#w{z#P9dXOcxHv~*3zm& zY30e}&-qoMj@<-niHcye!t1D58JOAr1g^c>sl?Y#*?NIlcb|?J^!AGPIXXt%(>gFd z$Yx*1j?tcE)9goh9VdIzdwjhls!TTaXx}e?f`MmBi4!aiXB_+Ps3jYq-MOQ(o)Ti; z4KkSI7jxa|STm2$dodAfWdUxD&5JPkG;Qd0HSfu+Ce%;tiE>46Fj8>x4qTB}B(Bj;6be|UaS?_S6i`V$OcjGv1k#wY7Ts%p! znt?gDVa?gh9aJ>qN|G)2Xx!7)89ADvCiZafBse6ynxsE$6^TkrFY9BA-5g>R&tc=6 z*l}K5)p>IhKlT}GaO`1Q#O@vxpH|@5j2@V(;@cm91T;%pZ2g^^e>fS`X|IltXjf;3 zGO^;kk~vVf+q?^kr|aI3;RC$A(^{NY_C8^2eH^E599YNM=m>!x_HmVgs4V4P(Q*FN ze%_09hOZ$TrQH!63uBV3Rmbh%PzNxRqpXB&09a18W3WnTO>OUV;$!kjS!F55*Q3^c zZ>Jxqk4)oZ_g0(hE5rK~Z>U(bk;tTZUu1*bDlMXct0o#_m9Mxp(a1PDSut@oH<^rH z9~$wW~>zeqQeWJk{8+SLn zA+&~Te6+*6ob{~gP(IcbUm3BXvP1l#J^DOCC&pNhPSn`jYnv=@88bJOl{86Da5^K0 zk}Xi9#hRCjQ?G`GomA^YcrqTH-@&=m_8V^6dhJzw3CTZ0{y&r4zKvt5LY`Z%z4_@fHBo(e zzyXO)N_;k}PS&e|+aR}Lp<$ujRr^T6MOpoX(R!ZK%BKHy7jM}T$^`pjv+VDN7`EkS zI&t1*Y!NRB2wwCwf}E=4B=x<}B{?bI37Cvw&#YUZOT=tkq?=29q}-pE$m(R@cCIeJ z#ER->cFyEPi`rM+K%sn5A^VLlZg$X8wXjzPUUo`%Gi^Wb_bytA#wW0vjV zBWf_YCMH;M7ur*-e#`wc9=E@D1@2XtSGXHv*=boGIi*9=> zowZ>39P62xO{ZY5_6x>tU5nN)v4iQ(L#zn!;&E-u^4-oMZO@6D$M0E^ow%~c*OxW8 zKi)9$iB@fVL?0N&x3NPFj$z2D!U>cjTYL!EYvKL82H=gr{%Rcm#Bm}6#A%UX&-~4K z5I(M@*VS^re9<&66|cW|gXybGP!G*)-$aWBw$@5`b@`dtZr3>UKo)ra&L>F)vnc4M zX`Qn!WY2U>(ov-`5=RPx5>jnun5UT8!05TaguU#MrYhqHUfaTb|6-b46>a{L+_}iO##s4=uVwDjTLY0H0ZwjP}>eLpCN? zt}M@V5S`lRO)}P(uR5r2zY(ih|MC{BGE&pGbnKe7Qs`f)>}40(r{ZhM>%F3CW7fE* z*PmKQ+d35hQ({qd%11$Y>8yNkTJyIpI--*@McLvQ^U=D5o;|0wclDa`x?EM%zc6zy zP?^^Do_CqMD(|_jYs=E9lRkn+6QG&N`8u_x2RXH%jBdG6i|0f&X1X|{qs_^>F0#%u zU#1GE+C1O&uUR(#QPGkq;wv}q}Pm@hWEeVPyRRry%C*w{D`ikq9c zvO4oJvBtS-VHkPSdD6S<|1O?QM^Rp~`iTRt%6SRF>&m^A`UuBav)K zui*Pst>@wn{ro&jx|}J+PaW(x4Oz|0=Ws}PqzZl1Ns29IMaSsu2KW|@H#H3He%ICi zdlT#GIkT|2&o67Mg1sHr*x>}@4*kM<{>f0R$lF?e>2!k{=c<06^*JuRgt?@U4 zp4O+R6-J^ zTyxgC1)KS<2^xd>!mO?=u5sT(WbzUdkkDFpBltt1HClq52+u-cdH3L&TR+>bUbRlZwx~3?@(Ue8**iSEGk@7xQ1Y zTVjXAFOlOX#<$zHONSes={ay(z)xTFft$K6I&|~MoO{M!5b=v3CXFFpYq51L$aK0I zzs@NH*>Z4eR?bgRI*-kiT06)Y#9Hi*d$VEt>P{VKuXg6yQ3Y?+^wS;==3+y#X~bJ5#xr(Ea78 zZFXa#UWRb6WS^Ml*fUs6t$Yw1x5&8zk9go7*`0$3Kbx?u^o;{uM_)&w>`>9p{!GHv z5sy&Wtj15jtxMynrd~d*2|gO|aS+oAQlWbu^#MCke03b20n41La$(DUGF;B`4# z@>`tV5_&$t-vrZd`%V6Zq37XazynUdJoNmne&3ur{W<>Gas91^yPW=d==sV?;Jb75 zV(>jq|1|VGru+^@`rNspXOVsy{$P$)`lC*-2tCVrzU1{iH=`3z9|=8W->h|pgreOH0rA88Zj z){{p1yuQ$rpr0PEPlvuImwqnk_vdKIKkD>Fp=T?}epqDy*d+0fIJn3bp-x7Mh zgFbrKIsF0t`FGJ?zw2O+TPvEM!1t94>06xs ze(0S`{(ZY5eg65VhWhlSPB%mEBGKNy^X~|~`u92d?)G%i*l*uGPTw1Pm!ATCFh`#Z zeV@~Rg=&?bx99wS3%#38fPNyTuRAaFZlOMykm+mP#i937(d#0ejaM7{*-wwr!@Bo{ z-WTd2*TcH6h2CL}n}wc_XpPHqq`Ak?Po(F69C}Cf{56qYzasRGi5~TI&)<9u_F;kN z*8eE<-Yvq*$@>3jcX>J4Y-1VW&SIdOxP;pNRCv z1(A7;GJAP_Qj#m1B)9#nDdS!kptJk^ir?Pr< z{F0Z~p|kumKegTS&-~Q>NN0Yi)Q&WF3ng!NC;d^_w68-y73nCApAjIvCG0JUyfDj`YTT{1prOz2}}tpYzku*N6Uk?(_7w;R5)3Y?&Y2 z_{X8|8D;3Ur++#0Ey54>c>6l(j~AYQ!s#bM-%|O-g-&~ZnV;O4rLQZbCr*2PdUr*7 z)7hc#EXG%F>hxIX>qj>>n9$!Q(WgOs`;3X`pwT{ZX8?LX zw(?6)Zx`ebMfvl_V|r&jGZ#8nPg}=mOcLyG6|R>-WGXsP9W} z=+cqwVq`rA34e@SiipnvZs z<6E0E{-1RDX6G6IueyAj*D^l($nxoX*qIhlsOF+00{l5gFhl8m@G|}r`E=yvV+%q5 z#zw}s@#X0y`Xl4ZF0x>MzsK__ZOORAUtyx5hBq%OitouuAn z707Gu&Ii?h5&Zb4kh&>zk-sR%hqjQ#qfq$kU=<-g$c2B-X!G+qy7wB=a`Lvm)Vjf+ z?^C~MUy;$fz*-Mw^rJJ=XQ>`JdGnQdx=@~_=gPBx-!m`&F!?R(mKVz4T>X2N&ZKQ# z$hBvy>VGPi^hwydl^gRxhkhYg;w3iM=wP7Czi1^QrtzP~`X3-lue`cQ#>VkT{7p0hVw*X8MQ zL4I93iJ~*X&aWg_S$|qPnQew@<+Mx z-Y0*Ur*{=-`J;UL{sMhhfxf3etGzjY($_A~YHvRMP=Qu^bLln>=IMon^m2h-SD=#u zy}dvW6zItUofhc33-rMPeSd*|7&_-qXFXb=A1}~P&7^0~EznB~bbo=~QlPIX(7Ous zXo22epzkWs_Y~;+3iN{o`r!ioXn}sbKtDB;o-?;VFD=mh1$s+?zNSF$D$t_^dVhhw zt3cmVpzkZt4;JW$3-qG}`tbt&)J)p`Iq97J=v+B(PjmYV^j7GcKbX6-Ko1vatvB-W zcNFM173g~l^v4SHrwjB~3iNjh^pi8`V>H%t{^yvppmXg%W>bOQTA+6p=-~oARiN)E z&~GZx_ZH}n73fbF=&uy$?-b}KXVUZfGJ5$LXP$jp`K(h{p1vZiwRb{m`SttSD|TMB zmXA5|uicJ#TPMM%{S$xo?SP3J(mUZu#Xl05dt+zp&ig$3>w-p!Z&c^iaqbx@#>(bMTC}mUNz{fIt*dNw z(G4E2k1;<)vkcd-qm%!?htdx6DC|?ST<>*$>F+a+#~RyN5YFh?8PnsWKxd@vGYfwK z_%+XK-#jDC%~5{_&i128jt{)&KBqOmF5=4CnLWP{-X^Z2ls_vaO<{kGI``~(G1Lu5 zDgUio{E0HL%Fp)S+4Gcd|7PXU%9r0JWXX}@o;|NLz27=o`MdlH)lubV`~2*Awg1Ch zSpkLqo9*Rme?KE+&x?5<*U`$q%b&{1&*ZY_)&4VH2s?bKKG(!6+-@?R(0Ak5RV`H(B$Y%1&5Brg=g z+pdZ9G4~TI4~~2+WbIe^-)bDb-t?BnwA}r@PdTAF=0mQ&rRS4Su$xTu&FzSK9w*i_ M99>z+%3u2b0P!2bxc~qF literal 35352 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 bB`ET3N5-AE2QNCqmy^G_TrvLnQsVv}nI^%T literal 35352 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 against — so its next direct + // child gets ordinal 5. Uses the bpf2go-generated state struct so kernel + // and Go layouts cannot drift. + selfTgid, ok := globalTgid(t, iter, os.Getpid()) + require.True(t, ok, "the iterator must list this process") const wantOrdinal = 5 require.NoError(t, tcObjs.MapStepState.Put(uint32(0), TcBpfStepState{ - WorkerTgid: uint32(os.Getpid()), + WorkerTgid: selfTgid, Enabled: 1, NextOrdinal: wantOrdinal, })) @@ -94,11 +142,17 @@ func TestStepFork(t *testing.T) { childPID := uint32(child.Process.Pid) defer func() { _ = child.Wait() }() - // The fork hook runs synchronously inside fork(), so the tag must be - // visible as soon as Start returns. + // The fork hook runs synchronously inside fork(), so both the tag (under + // the child's global tid) and its namespace-pid entry must be visible as + // soon as Start returns. + childTgid, ok := globalTgid(t, iter, int(childPID)) + require.True(t, ok, "the iterator must list the child") var got uint32 - require.NoError(t, tcObjs.MapTaskStep.Lookup(childPID, &got), + require.NoError(t, tcObjs.MapTaskStep.Lookup(childTgid, &got), "child tid must be tagged immediately after fork") + var nsPid uint32 + require.NoError(t, tcObjs.MapTaskNspid.Lookup(childTgid, &nsPid)) + require.Equal(t, childPID, nsPid, "map_task_nspid must carry the child's pid as we number it") // The new-step notification must arrive for the reconciler. Other test // machinery may fork too, so scan until the child's event or deadline, @@ -119,17 +173,41 @@ func TestStepFork(t *testing.T) { break } - // Exit hygiene: once the child is reaped, its tag must be gone. + // Exit hygiene: once the child is reaped, its tag and namespace entry + // must be gone. require.NoError(t, child.Wait()) require.Eventually(t, func() bool { var v uint32 - return tcObjs.MapTaskStep.Lookup(childPID, &v) != nil - }, 2*time.Second, 50*time.Millisecond, "exit tracepoint must delete the child's tag") + return tcObjs.MapTaskStep.Lookup(childTgid, &v) != nil && + tcObjs.MapTaskNspid.Lookup(childTgid, &v) != nil + }, 2*time.Second, 50*time.Millisecond, "exit tracepoint must delete the child's entries") // Disable so later tests in this package see inert step hooks. require.NoError(t, tcObjs.MapStepState.Put(uint32(0), TcBpfStepState{})) } +// TestStepFork_InPidNamespace re-executes TestStepFork inside a fresh pid +// (and mount, for /proc) namespace — the shape of an ARC runner pod, where +// /proc pids and the kernel's global pids disagree and the pre-iterator +// contract failed on the very first lookup. +func TestStepFork_InPidNamespace(t *testing.T) { + if os.Geteuid() != 0 { + t.Skip("needs root") + } + unshare, err := exec.LookPath("unshare") + if err != nil { + t.Skip("unshare not available") + } + out, err := exec.Command(unshare, "--pid", "--fork", "--mount-proc", + os.Args[0], "-test.run", "^TestStepFork$", "-test.v").CombinedOutput() + t.Logf("inside pid namespace:\n%s", out) + if strings.Contains(string(out), "--- SKIP") { + t.Skip("inner test skipped") + } + require.NoError(t, err) + require.Contains(t, string(out), "--- PASS: TestStepFork") +} + // TestBlockedEventLayoutMatchesBTF pins the kernel↔userspace event layout: // every member of the C struct blocked_event (as compiled, via BTF) must sit // at the same offset as its field in the Go mirror, and the sizes must @@ -196,6 +274,25 @@ func TestStepChildEventLayoutMatchesBTF(t *testing.T) { require.Equal(t, uintptr(st.Members[1].Offset.Bytes()), unsafe.Offsetof(ev.Ordinal)) } +// TestTaskIterRecLayoutMatchesBTF pins the iterator record layout that +// pkg/steps decodes by hand (taskIterRecSize and the four u32 offsets). +func TestTaskIterRecLayoutMatchesBTF(t *testing.T) { + spec, err := LoadStepBpf() + require.NoError(t, err) + + typ, err := spec.Types.AnyTypeByName("task_iter_rec") + require.NoError(t, err) + st, ok := typ.(*btf.Struct) + require.True(t, ok, "task_iter_rec must be a struct, got %T", typ) + + require.Equal(t, uint32(16), st.Size, "struct size") + require.Len(t, st.Members, 4) + for i, name := range []string{"tid", "tgid", "ppid", "ns_tgid"} { + require.Equal(t, name, st.Members[i].Name) + require.Equal(t, uint32(4*i), st.Members[i].Offset.Bytes()) + } +} + // TestStepMapsShrunkLoad pins the memory optimization in cmd/start.go: // with step attribution off, the step maps are resized down before load, // and the collection must still pass the verifier at the small sizes. @@ -205,6 +302,7 @@ func TestStepMapsShrunkLoad(t *testing.T) { require.NoError(t, err) spec.Maps["map_task_step"].MaxEntries = 64 spec.Maps["map_sock_step"].MaxEntries = 64 + spec.Maps["map_task_nspid"].MaxEntries = 64 var objs TcBpfObjects require.NoError(t, spec.LoadAndAssign(&objs, nil)) diff --git a/bpf/tcbpf.c b/bpf/tcbpf.c index 72d9945..ffb1058 100644 --- a/bpf/tcbpf.c +++ b/bpf/tcbpf.c @@ -101,7 +101,9 @@ struct { __uint(max_entries, 256 * 1024); } map_events SEC(".maps"); -// LRU hash map: socket cookie → PID (populated by cgroup programs, read by TC) +// LRU hash map: socket cookie → PID (populated by cgroup programs, read by +// TC). The PID is numbered in the daemon's pid namespace when the step +// tracker is running — see map_task_nspid — so userspace can resolve it. struct { __uint(type, BPF_MAP_TYPE_LRU_HASH); __type(key, __u64); @@ -153,7 +155,7 @@ struct { #define STEP_ORD_PREDAEMON 0xFFFFFFFEu // worker children that predate cargowall struct step_state { - __u32 worker_tgid; // Runner.Worker tgid (0 = not discovered) + __u32 worker_tgid; // Runner.Worker global tgid (0 = not discovered) __u32 enabled; // 0 = feature off, all step hooks no-op __u64 next_ordinal; // next step ordinal, atomically incremented }; @@ -183,6 +185,31 @@ struct { __uint(max_entries, 65536); } map_sock_step SEC(".maps"); +// global tid → tgid as numbered in the daemon's pid namespace. Maintained +// by stepbpf.c (fork/exit tracepoints plus the task iterator that seeds +// pre-existing tasks); empty when step attribution is off. Kernel-side +// identity stays global everywhere — this table exists so the pid handed +// to userspace is one it can look up in its own /proc, which differs from +// the global number whenever cargowall runs inside a container (ARC +// runners). Keyed by tid so any thread resolves its process. +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __type(key, __u32); + __type(value, __u32); + __uint(max_entries, 32768); +} map_task_nspid SEC(".maps"); + +// Helper: the calling thread's process id as userspace should see it — the +// daemon-namespace tgid when the step tracker has translated this task, +// the global tgid otherwise (identical outside a pid namespace, and the +// best available answer while step attribution is off). +static __always_inline __u32 sock_owner_pid(void) { + __u64 pid_tgid = bpf_get_current_pid_tgid(); + __u32 tid = (__u32)pid_tgid; + __u32 *ns = bpf_map_lookup_elem(&map_task_nspid, &tid); + return ns ? *ns : (__u32)(pid_tgid >> 32); +} + // Helper: copy the calling thread's step tag onto a socket cookie. // Shared by the connect/sendmsg hooks below (fallback for sockets created // before attach) and cg_sock_create in stepbpf.c (primary path). @@ -543,7 +570,7 @@ static __always_inline int handle_ipv6(struct __sk_buff *skb, __u32 l3_offset) { SEC("cgroup/connect4") int cg_connect4(struct bpf_sock_addr *ctx) { __u64 cookie = bpf_get_socket_cookie(ctx); - __u32 pid = bpf_get_current_pid_tgid() >> 32; + __u32 pid = sock_owner_pid(); bpf_map_update_elem(&map_sock_pid, &cookie, &pid, BPF_ANY); step_tag_socket(cookie); return 1; @@ -552,7 +579,7 @@ int cg_connect4(struct bpf_sock_addr *ctx) { SEC("cgroup/connect6") int cg_connect6(struct bpf_sock_addr *ctx) { __u64 cookie = bpf_get_socket_cookie(ctx); - __u32 pid = bpf_get_current_pid_tgid() >> 32; + __u32 pid = sock_owner_pid(); bpf_map_update_elem(&map_sock_pid, &cookie, &pid, BPF_ANY); step_tag_socket(cookie); return 1; @@ -563,7 +590,7 @@ int cg_connect6(struct bpf_sock_addr *ctx) { SEC("cgroup/sendmsg4") int cg_sendmsg4(struct bpf_sock_addr *ctx) { __u64 cookie = bpf_get_socket_cookie(ctx); - __u32 pid = bpf_get_current_pid_tgid() >> 32; + __u32 pid = sock_owner_pid(); bpf_map_update_elem(&map_sock_pid, &cookie, &pid, BPF_ANY); step_tag_socket(cookie); return 1; @@ -572,7 +599,7 @@ int cg_sendmsg4(struct bpf_sock_addr *ctx) { SEC("cgroup/sendmsg6") int cg_sendmsg6(struct bpf_sock_addr *ctx) { __u64 cookie = bpf_get_socket_cookie(ctx); - __u32 pid = bpf_get_current_pid_tgid() >> 32; + __u32 pid = sock_owner_pid(); bpf_map_update_elem(&map_sock_pid, &cookie, &pid, BPF_ANY); step_tag_socket(cookie); return 1; diff --git a/bpf/tcbpf_bpfeb.go b/bpf/tcbpf_bpfeb.go index 5dd01ab..6934e0f 100644 --- a/bpf/tcbpf_bpfeb.go +++ b/bpf/tcbpf_bpfeb.go @@ -77,6 +77,7 @@ const ( TcBpfMapMapSockPid = "map_sock_pid" TcBpfMapMapSockStep = "map_sock_step" TcBpfMapMapStepState = "map_step_state" + TcBpfMapMapTaskNspid = "map_task_nspid" TcBpfMapMapTaskStep = "map_task_step" TcBpfProgCgConnect4 = "cg_connect4" TcBpfProgCgConnect6 = "cg_connect6" @@ -153,6 +154,7 @@ type TcBpfMapSpecs struct { MapSockPid *ebpf.MapSpec `ebpf:"map_sock_pid"` MapSockStep *ebpf.MapSpec `ebpf:"map_sock_step"` MapStepState *ebpf.MapSpec `ebpf:"map_step_state"` + MapTaskNspid *ebpf.MapSpec `ebpf:"map_task_nspid"` MapTaskStep *ebpf.MapSpec `ebpf:"map_task_step"` } @@ -195,6 +197,7 @@ type TcBpfMaps struct { MapSockPid *ebpf.Map `ebpf:"map_sock_pid"` MapSockStep *ebpf.Map `ebpf:"map_sock_step"` MapStepState *ebpf.Map `ebpf:"map_step_state"` + MapTaskNspid *ebpf.Map `ebpf:"map_task_nspid"` MapTaskStep *ebpf.Map `ebpf:"map_task_step"` } @@ -212,6 +215,7 @@ func (m *TcBpfMaps) Close() error { m.MapSockPid, m.MapSockStep, m.MapStepState, + m.MapTaskNspid, m.MapTaskStep, ) } diff --git a/bpf/tcbpf_bpfeb.o b/bpf/tcbpf_bpfeb.o index 91cc6074f2864cebcd3cdc63221f4ff05e3d975d..3503beb0e26900ad699980553b47f6a78b616613 100644 GIT binary patch literal 70288 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 literal 68776 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 diff --git a/bpf/tcbpf_bpfel.go b/bpf/tcbpf_bpfel.go index 43c3529..4434a5c 100644 --- a/bpf/tcbpf_bpfel.go +++ b/bpf/tcbpf_bpfel.go @@ -77,6 +77,7 @@ const ( TcBpfMapMapSockPid = "map_sock_pid" TcBpfMapMapSockStep = "map_sock_step" TcBpfMapMapStepState = "map_step_state" + TcBpfMapMapTaskNspid = "map_task_nspid" TcBpfMapMapTaskStep = "map_task_step" TcBpfProgCgConnect4 = "cg_connect4" TcBpfProgCgConnect6 = "cg_connect6" @@ -153,6 +154,7 @@ type TcBpfMapSpecs struct { MapSockPid *ebpf.MapSpec `ebpf:"map_sock_pid"` MapSockStep *ebpf.MapSpec `ebpf:"map_sock_step"` MapStepState *ebpf.MapSpec `ebpf:"map_step_state"` + MapTaskNspid *ebpf.MapSpec `ebpf:"map_task_nspid"` MapTaskStep *ebpf.MapSpec `ebpf:"map_task_step"` } @@ -195,6 +197,7 @@ type TcBpfMaps struct { MapSockPid *ebpf.Map `ebpf:"map_sock_pid"` MapSockStep *ebpf.Map `ebpf:"map_sock_step"` MapStepState *ebpf.Map `ebpf:"map_step_state"` + MapTaskNspid *ebpf.Map `ebpf:"map_task_nspid"` MapTaskStep *ebpf.Map `ebpf:"map_task_step"` } @@ -212,6 +215,7 @@ func (m *TcBpfMaps) Close() error { m.MapSockPid, m.MapSockStep, m.MapStepState, + m.MapTaskNspid, m.MapTaskStep, ) } diff --git a/bpf/tcbpf_bpfel.o b/bpf/tcbpf_bpfel.o index 6670b133fdc16019a5630c3f38201221f7097f54..ea37333a94e06e8ef79de05df9d09206749305a7 100644 GIT binary patch literal 70448 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 Z1G>O0Q~3Shq~~`TuYgPQmt!aH{|C9^O=oRsEa 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 N-VspDU{(lOhnGXN} diff --git a/cmd/start.go b/cmd/start.go index 463edca..f5fbac3 100644 --- a/cmd/start.go +++ b/cmd/start.go @@ -503,14 +503,15 @@ func startCargoWall(cmd *StartCmd, hooks *StartHooks, teardowns *teardownList) e if err != nil { return fmt.Errorf("failed to load TC eBPF spec: %w", err) } - // The step-attribution maps preallocate ~7MB of kernel memory at their - // full size (LRU hash always preallocates). Shrink them when the feature - // is off — every non-GitHub run — but never when it is on: stepbpf.c - // declares the full sizes and MapReplacements rejects mismatched specs. + // The step-attribution maps preallocate ~8MB of kernel memory at their + // full size (hash maps preallocate by default). Shrink them when the + // feature is off — every non-GitHub run — but never when it is on: + // stepbpf.c declares the full sizes and MapReplacements rejects + // mismatched specs. if !cmd.StepAttribution { // Present unless the generated spec is stale (go generate not run); // verify-bpf-generated-code guards that, but don't panic if it slips. - for _, name := range []string{"map_task_step", "map_sock_step"} { + for _, name := range []string{"map_task_step", "map_sock_step", "map_task_nspid"} { if m := spec.Maps[name]; m != nil { m.MaxEntries = 64 } diff --git a/design.md b/design.md index c5911a7..6535bb6 100644 --- a/design.md +++ b/design.md @@ -398,10 +398,33 @@ sequenceDiagram | `cg_connect6` | cgroup/connect6 | Maps IPv6 TCP socket cookie → PID | | `cg_sendmsg4` | cgroup/sendmsg4 | Maps IPv4 UDP socket cookie → PID | | `cg_sendmsg6` | cgroup/sendmsg6 | Maps IPv6 UDP socket cookie → PID | -| `step_fork` / `step_exit` | tp_btf tracepoints (stepbpf.c, separate collection; needs kernel BTF) | Step attribution: mint/inherit per-step process tags, drop at exit | +| `step_fork` / `step_exit` | tp_btf tracepoints (stepbpf.c, separate collection; needs kernel BTF) | Step attribution: mint/inherit per-step process tags, drop at exit. Also maintains `map_task_nspid` (global tid → tgid as numbered in the daemon's pid namespace) for every fork | +| `step_task_iter` | iter/task (stepbpf.c) | Task walk run by `pkg/steps` at start and per container tag: seeds `map_task_nspid` for tasks that predate the attach and streams the process tree in global ids, so userspace never has to translate pids through `/proc` | | `cg_sock_create` | cgroup/sock_create (stepbpf.c) | Copies the creating thread's step tag onto each socket cookie | | `cg_origin_egress` | cgroup_skb/egress (originbpf.c, separate collection) | Flow-origin recorder and — in enforce mode — the primary egress verdict (issue #106). Runs in socket context, pre-NAT, inside the originating netns, so it can both attribute and enforce container traffic. Behavior is set by a runtime mode: observe (pass, record only), shadow (compute the verdict, report would-blocks, pass), enforce (drop denied traffic). Shares the rule maps with `tc_egress` via `bpf/verdict.h` | +### Pid namespaces (issue #135) + +The kernel numbers tasks globally; everything the daemon reads from `/proc` +— the `Runner.Worker` pid, its own pid, container leaders reported by +dockerd — is numbered in the daemon's pid namespace. On a GitHub-hosted VM +the two coincide. Inside a container (ARC runners are Kubernetes pods) they +do not, and before #135 the worker's `/proc` pid was compared against +`parent->tgid` in `step_fork`, seeding wrote `/proc` tids as map keys, and +`map_sock_pid` handed userspace global tgids it could not resolve — every +event came out unattributed with an empty process name. + +The rule now is: kernel-side identity is global everywhere; translation +happens at the boundary, in the kernel, through `map_task_nspid`. +`step_task_iter` (scoped by the kernel to the opener's namespace) seeds it +and gives `pkg/steps` the process tree in global ids; `step_fork` keeps it +current (a thread copies its process's entry, a process walks its upid +chain to the daemon's namespace, identified by the nsfs inode passed in as +`pidns_ino`); the cgroup hooks write the namespace tgid into `map_sock_pid`; +boundary events carry the namespace pid so the reconciler's `/proc` read +works. Kernel floor is unchanged: `iter/task` and `bpf_seq_write` are 5.8, +like the ring buffer. + ## Audit Mode Audit mode allows CargoWall to run in a log-only configuration — all traffic decisions are recorded but no packets are dropped. diff --git a/pkg/steps/steps.go b/pkg/steps/steps.go index 9633c72..390841b 100644 --- a/pkg/steps/steps.go +++ b/pkg/steps/steps.go @@ -21,17 +21,31 @@ // thread's tag, and the cgroup hooks copy the tag onto each socket cookie — // so every TC event carries the step that (transitively) created its socket. // Attribution only: no verdict consults these maps. +// +// Pid numbering: the kernel side keys everything by global (init-namespace) +// ids; everything this package reads from /proc — the worker pid, the +// daemon's own pid, container leaders reported by dockerd — is numbered in +// the daemon's pid namespace, a different space whenever cargowall runs +// inside a container (ARC runners are Kubernetes pods). The step_task_iter +// BPF iterator bridges the two: it walks the daemon's namespace subtree, +// seeds the kernel's global→namespace table (map_task_nspid, which is what +// makes boundary events and socket pids resolvable) and streams the +// process tree in global ids, so seeding and container tagging never scan +// /proc. On a plain VM the translation is the identity. package steps import ( + "encoding/binary" "errors" "fmt" + "io" "log/slog" "math" "os" "strconv" "strings" "sync" + "syscall" "time" "unicode/utf8" "unsafe" @@ -78,10 +92,12 @@ const maxOrdinalBase = uint64(events.MaxRealStepOrdinal) // Tracker owns the step-attribution BPF programs and the reconciler // goroutine that turns new-step ringbuf notifications into audit events. type Tracker struct { - workerPID int + workerPID int // as numbered in the daemon's pid namespace (/proc) + workerTgid uint32 // the same process, global — what step_fork compares workerCmdline string objs bpf.StepBpfObjects links []link.Link + iter *link.Iter // step_task_iter; also in links for Close reader *ringbuf.Reader stateMap *ebpf.Map taskMap *ebpf.Map @@ -96,6 +112,10 @@ type Tracker struct { // DNS-path client attribution (sock_diag lookups, caching, map joins) — // see clientResolver. Constructed in Start, closed in Close. resolver *clientResolver + + // snapshotFn produces the process tree the seeding and container-tagging + // paths work from; iterSnapshot in production, injectable for tests. + snapshotFn func() (*procSnapshot, error) } // Start loads the step-attribution collection against tcObjs' shared maps, @@ -129,10 +149,24 @@ func Start(tcObjs *bpf.TcBpfObjects, opts Options, auditLogger *events.AuditLogg return nil, fmt.Errorf("worker pid %d out of range", workerPID) } + // Which pid namespace "our" pids belong to. Every /proc-derived id in + // this package (the worker pid above, seeding, container leaders) is + // numbered in it; the kernel numbers tasks globally. Identical on a + // plain VM, different inside a container (ARC runners). + pidnsIno, err := pidNamespaceInode() + if err != nil { + return nil, err + } + spec, err := bpf.LoadStepBpf() if err != nil { return nil, fmt.Errorf("failed to load step BPF spec: %w", err) } + if v := spec.Variables[bpf.StepBpfVarPidnsIno]; v == nil { + return nil, errors.New("step BPF spec has no pidns_ino variable (stale generated code)") + } else if err := v.Set(pidnsIno); err != nil { + return nil, fmt.Errorf("failed to set pidns_ino: %w", err) + } t := &Tracker{ workerPID: workerPID, @@ -146,14 +180,16 @@ func Start(tcObjs *bpf.TcBpfObjects, opts Options, auditLogger *events.AuditLogg done: make(chan struct{}), resolver: newClientResolver(tcObjs.MapSockStep, tcObjs.MapSockPid, logger), } + t.snapshotFn = t.iterSnapshot - // The three shared maps are owned by the tcbpf collection; replacing them - // here makes both collections operate on the same kernel maps. + // The shared maps are owned by the tcbpf collection; replacing them here + // makes both collections operate on the same kernel maps. if err := spec.LoadAndAssign(&t.objs, &ebpf.CollectionOptions{ MapReplacements: map[string]*ebpf.Map{ "map_task_step": tcObjs.MapTaskStep, "map_sock_step": tcObjs.MapSockStep, "map_step_state": tcObjs.MapStepState, + "map_task_nspid": tcObjs.MapTaskNspid, }, }); err != nil { return nil, fmt.Errorf("failed to load step BPF objects (kernel BTF required): %w", err) @@ -164,22 +200,47 @@ func Start(tcObjs *bpf.TcBpfObjects, opts Options, auditLogger *events.AuditLogg return nil, err } + // First task walk: seeds map_task_nspid for everything that predates the + // attach (the tracepoints keep it current from here on) and hands back + // the process tree in global ids — which is how the worker's /proc pid + // becomes the global tgid step_fork compares against. + snap, err := t.snapshotFn() + if err != nil { + t.Close() + return nil, err + } + workerTgid, ok := snap.global[workerPID] + if !ok { + t.Close() + return nil, fmt.Errorf("%s pid %d is not visible to the task iterator", workerComm, workerPID) + } + t.workerTgid = workerTgid + if pidnsIno != initPidNamespaceInode { + logger.Info("Running inside a pid namespace; translating task ids", + "pidns_ino", pidnsIno, "worker_pid", workerPID, "worker_tgid", workerTgid) + } + // Seed while the programs are attached but still disabled (enabled=0 makes - // them no-ops), then enable, then seed once more: the second pass catches - // processes forked during the first scan, so the only unattributed window - // is forks that both start and create sockets between the enable flip and - // the rescan. Seeding is strictly create-only (BPF_NOEXIST), so the second - // pass can never clobber an ordinal the now-live fork tracepoint assigned. - t.seedExisting() + // the tagging hooks no-ops), then enable, then seed once more from a fresh + // walk: the second pass catches processes forked during the first, so the + // only unattributed window is forks that both start and create sockets + // between the enable flip and the rescan. Seeding is strictly create-only + // (BPF_NOEXIST), so the second pass can never clobber an ordinal the + // now-live fork tracepoint assigned. + t.seedExisting(snap) if err := t.stateMap.Put(uint32(0), bpf.TcBpfStepState{ - WorkerTgid: uint32(workerPID), + WorkerTgid: workerTgid, Enabled: 1, NextOrdinal: max(opts.OrdinalBase, 1), }); err != nil { t.Close() return nil, fmt.Errorf("failed to seed step state: %w", err) } - t.seedExisting() + if snap, err := t.snapshotFn(); err == nil { + t.seedExisting(snap) + } else { + logger.Warn("Second seeding pass skipped", "error", err) + } rd, err := ringbuf.NewReader(t.objs.MapStepEvents) if err != nil { @@ -246,73 +307,67 @@ func (t *Tracker) attach() error { return fmt.Errorf("failed to attach cgroup sock_create: %w", err) } t.links = append(t.links, sockLink) + + iter, err := link.AttachIter(link.IterOptions{Program: t.objs.StepTaskIter}) + if err != nil { + return fmt.Errorf("failed to attach task iterator: %w", err) + } + t.links = append(t.links, iter) + t.iter = iter return nil } -// seedExisting tags processes that predate the daemon. Worker threads get -// StepOrdinalRunner so infra traffic (log upload, action downloads) is -// labeled as runner overhead; existing worker child subtrees — steps that -// ran or started before cargowall attached — get StepOrdinalPreDaemon, -// except the daemon's own subtree, which gets StepOrdinalRunner (cargowall -// is infrastructure, not a workflow step). Every write is create-only: -// once the fork tracepoint is live it is the sole authority on new tags, -// and the post-enable rescan must never replace a kernel-assigned ordinal. -// Best-effort by nature: /proc scans race process creation, which is why -// Start runs it twice around the enable flip. -func (t *Tracker) seedExisting() { - children := buildChildrenMap() - +// seedExisting tags processes that predate the daemon, from one task-walk +// snapshot. Worker threads get StepOrdinalRunner so infra traffic (log +// upload, action downloads) is labeled as runner overhead; existing worker +// child subtrees — steps that ran or started before cargowall attached — +// get StepOrdinalPreDaemon, except the daemon's own subtree, which gets +// StepOrdinalRunner (cargowall is infrastructure, not a workflow step). +// Every write is create-only: once the fork tracepoint is live it is the +// sole authority on new tags, and the post-enable rescan must never replace +// a kernel-assigned ordinal. Best-effort by nature: a walk races process +// creation, which is why Start runs it twice around the enable flip. +func (t *Tracker) seedExisting(snap *procSnapshot) { // Decide each pid's intended tag before writing anything, so a pid in // the daemon's subtree is written exactly once with the right value // (create-only writes mean there is no second chance). - self := make(map[int]bool) - for _, pid := range subtreePids(os.Getpid(), children) { - self[pid] = true + self := make(map[uint32]bool) + selfTgid, selfVisible := snap.global[os.Getpid()] + if selfVisible { + for _, pid := range snap.subtree(selfTgid) { + self[pid] = true + } } - t.tagTasks(t.workerPID, events.StepOrdinalRunner, ebpf.UpdateNoExist) - for _, root := range children[t.workerPID] { - for _, pid := range subtreePids(root, children) { + t.tagTasks(snap, t.workerTgid, events.StepOrdinalRunner, ebpf.UpdateNoExist) + for _, root := range snap.children[t.workerTgid] { + for _, pid := range snap.subtree(root) { ordinal := events.StepOrdinalPreDaemon if self[pid] { ordinal = events.StepOrdinalRunner } - t.tagTasks(pid, ordinal, ebpf.UpdateNoExist) + t.tagTasks(snap, pid, ordinal, ebpf.UpdateNoExist) } } // Standalone runs (daemon not under the worker): still label our own // traffic as infrastructure. Create-only, so a no-op when the loop // above already covered us. - for _, pid := range subtreePids(os.Getpid(), children) { - t.tagTasks(pid, events.StepOrdinalRunner, ebpf.UpdateNoExist) - } -} - -// subtreePids returns pid plus all its descendant process ids. -func subtreePids(pid int, children map[int][]int) []int { - pids := []int{pid} - for i := 0; i < len(pids); i++ { - pids = append(pids, children[pids[i]]...) + if selfVisible { + for _, pid := range snap.subtree(selfTgid) { + t.tagTasks(snap, pid, events.StepOrdinalRunner, ebpf.UpdateNoExist) + } } - return pids } -// tagTasks writes ordinal for every thread of pid. Seeding passes -// UpdateNoExist so a tid the fork tracepoint already tagged keeps its -// kernel-assigned ordinal; container leaders pass UpdateAny (see -// TagContainerProcess for why overwrite is safe there and only there). -func (t *Tracker) tagTasks(pid int, ordinal uint32, flags ebpf.MapUpdateFlags) { - tids, err := os.ReadDir("/proc/" + strconv.Itoa(pid) + "/task") - if err != nil { - return // process exited mid-scan - } - for _, tid := range tids { - n, err := strconv.ParseUint(tid.Name(), 10, 32) - if err != nil { - continue - } - _ = t.taskMap.Update(uint32(n), ordinal, flags) +// tagTasks writes ordinal for every thread of the (global) tgid as listed +// in snap. Seeding passes UpdateNoExist so a tid the fork tracepoint +// already tagged keeps its kernel-assigned ordinal; container leaders pass +// UpdateAny (see TagContainerProcess for why overwrite is safe there and +// only there). +func (t *Tracker) tagTasks(snap *procSnapshot, tgid, ordinal uint32, flags ebpf.MapUpdateFlags) { + for _, tid := range snap.tids[tgid] { + _ = t.taskMap.Update(tid, ordinal, flags) } } @@ -322,6 +377,12 @@ func (t *Tracker) tagTasks(pid int, ordinal uint32, flags ebpf.MapUpdateFlags) { // userspace bridge that puts the launching step's ordinal on them, after // which sock_create tags their sockets like any other tagged task. // +// pid is as dockerd reported it, i.e. numbered in the daemon's namespace +// when dockerd shares it (the caller's cgroup identity check has already +// confirmed that pid is the expected container in our /proc). A pid the +// task walk cannot see — exited, or a dockerd outside our namespace — is +// skipped. +// // The leader's threads are written with UpdateAny: a freshly shim-forked // leader is untagged in the normal case, an exec re-tag into a long-lived // container must replace the container's older ordinal, and overwrite @@ -329,10 +390,13 @@ func (t *Tracker) tagTasks(pid int, ordinal uint32, flags ebpf.MapUpdateFlags) { // create-only — children forked after an earlier tag already carry correct // kernel-inherited ordinals that must never be clobbered. func (t *Tracker) TagContainerProcess(pid int, ordinal uint32) { - t.tagTasks(pid, ordinal, ebpf.UpdateAny) - children := buildChildrenMap() - for _, p := range subtreePids(pid, children)[1:] { - t.tagTasks(p, ordinal, ebpf.UpdateNoExist) + snap, leader, ok := t.resolve(pid) + if !ok { + return + } + t.tagTasks(snap, leader, ordinal, ebpf.UpdateAny) + for _, p := range snap.subtree(leader)[1:] { + t.tagTasks(snap, p, ordinal, ebpf.UpdateNoExist) } } @@ -346,9 +410,119 @@ func (t *Tracker) TagContainerProcess(pid int, ordinal uint32) { // adoption is not an exec re-tag, and a recycled-tid stale entry is the // rarer wrong to optimize for than demoting a live container. func (t *Tracker) AdoptContainerProcess(pid int, ordinal uint32) { - for _, p := range subtreePids(pid, buildChildrenMap()) { - t.tagTasks(p, ordinal, ebpf.UpdateNoExist) + snap, leader, ok := t.resolve(pid) + if !ok { + return + } + for _, p := range snap.subtree(leader) { + t.tagTasks(snap, p, ordinal, ebpf.UpdateNoExist) + } +} + +// resolve takes a fresh task walk and translates a daemon-namespace pid to +// the global tgid the step maps are keyed by. +func (t *Tracker) resolve(pid int) (*procSnapshot, uint32, bool) { + snap, err := t.snapshotFn() + if err != nil { + t.logger.Warn("Task walk failed", "error", err) + return nil, 0, false + } + leader, ok := snap.global[pid] + if !ok { + return nil, 0, false // exited mid-flight, or not numbered in our namespace + } + return snap, leader, true +} + +// initPidNamespaceInode is PROC_PID_INIT_INO, the fixed nsfs inode of the +// initial pid namespace — what a plain VM sees; anything else means the +// daemon is inside a container. +const initPidNamespaceInode uint32 = 0xEFFFFFFC + +// pidNamespaceInode identifies the daemon's pid namespace the way the +// kernel does (ns_common.inum), for the step_task_iter/step_fork walk. +func pidNamespaceInode() (uint32, error) { + fi, err := os.Stat("/proc/self/ns/pid") + if err != nil { + return 0, fmt.Errorf("failed to stat pid namespace: %w", err) + } + st, ok := fi.Sys().(*syscall.Stat_t) + if !ok { + return 0, errors.New("pid namespace stat has no inode") } + return uint32(st.Ino), nil +} + +// taskIterRecSize is sizeof(struct task_iter_rec) in bpf/stepbpf.c: four +// native-endian u32s — tid, tgid, ppid (global) and ns_tgid. +const taskIterRecSize = 16 + +// procSnapshot is one pass of the step_task_iter walk: every task in the +// daemon's pid-namespace subtree, keyed by the global ids the step maps +// use, plus the translation from the daemon-namespace pids userspace +// discovers. +type procSnapshot struct { + tids map[uint32][]uint32 // global tgid → its threads' global tids + children map[uint32][]uint32 // global parent tgid → child global tgids + global map[int]uint32 // daemon-namespace tgid → global tgid +} + +// iterSnapshot runs the task iterator once and parses its output. +func (t *Tracker) iterSnapshot() (*procSnapshot, error) { + rd, err := t.iter.Open() + if err != nil { + return nil, fmt.Errorf("failed to open task iterator: %w", err) + } + defer rd.Close() + data, err := io.ReadAll(rd) + if err != nil { + return nil, fmt.Errorf("failed to read task iterator: %w", err) + } + return parseTaskRecords(data), nil +} + +// parseTaskRecords decodes the iterator's fixed-size record stream. Threads +// contribute their tid to the leader's list and nothing else: they share +// the leader's tree position and namespace pid. A trailing partial record +// (a truncated read) is ignored. +func parseTaskRecords(data []byte) *procSnapshot { + snap := &procSnapshot{ + tids: make(map[uint32][]uint32), + children: make(map[uint32][]uint32), + global: make(map[int]uint32), + } + for off := 0; off+taskIterRecSize <= len(data); off += taskIterRecSize { + rec := data[off : off+taskIterRecSize] + tid := binary.NativeEndian.Uint32(rec[0:4]) + tgid := binary.NativeEndian.Uint32(rec[4:8]) + ppid := binary.NativeEndian.Uint32(rec[8:12]) + nsTgid := binary.NativeEndian.Uint32(rec[12:16]) + snap.tids[tgid] = append(snap.tids[tgid], tid) + if tid != tgid { + continue + } + snap.global[int(nsTgid)] = tgid + if ppid != tgid { + snap.children[ppid] = append(snap.children[ppid], tgid) + } + } + return snap +} + +// subtree returns root plus all its descendant tgids, breadth-first. The +// seen set guards against a cyclic snapshot; real trees terminate. +func (s *procSnapshot) subtree(root uint32) []uint32 { + pids := []uint32{root} + seen := map[uint32]bool{root: true} + for i := 0; i < len(pids); i++ { + for _, c := range s.children[pids[i]] { + if !seen[c] { + seen[c] = true + pids = append(pids, c) + } + } + } + return pids } // boundary records one step_child_event as observed by run(), stamped with @@ -414,6 +588,8 @@ func (t *Tracker) run() { // resolve against it promptly. t.recordBoundary(ev.Ordinal, time.Now()) + // ev.Tgid is numbered in our pid namespace (see map_task_nspid), so + // /proc reads work from inside a container too. cmdline := sanitizeCmdline(t.stepCmdline(int(ev.Tgid))) t.logger.Info("Workflow step process started", "step_ordinal", ev.Ordinal, @@ -571,27 +747,6 @@ func readComm(pid int) string { return strings.TrimSpace(string(comm)) } -// buildChildrenMap snapshots the process tree as parent pid → child pids. -func buildChildrenMap() map[int][]int { - children := make(map[int][]int) - procs, err := os.ReadDir("/proc") - if err != nil { - return children - } - for _, p := range procs { - pid, err := strconv.Atoi(p.Name()) - if err != nil { - continue - } - ppid, ok := readPPid(pid) - if !ok { - continue - } - children[ppid] = append(children[ppid], pid) - } - return children -} - // readPPid extracts the parent pid from /proc//stat. The comm field // (2) can contain spaces and parentheses, so parse from the last ')' — // state is the field after it, ppid the one after that. diff --git a/pkg/steps/steps_test.go b/pkg/steps/steps_test.go index 7cc60d7..20fdb3c 100644 --- a/pkg/steps/steps_test.go +++ b/pkg/steps/steps_test.go @@ -17,18 +17,21 @@ package steps import ( + "encoding/binary" + "errors" "log/slog" "os" "os/exec" - "runtime" + "strconv" + "strings" "testing" "time" "github.com/cilium/ebpf" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "golang.org/x/sys/unix" + "github.com/code-cargo/cargowall/bpf" "github.com/code-cargo/cargowall/pkg/events" ) @@ -67,15 +70,63 @@ func TestReadComm_Self(t *testing.T) { assert.LessOrEqual(t, len(comm), 15) } -func TestBuildChildrenMap_ContainsSelf(t *testing.T) { - children := buildChildrenMap() - assert.Contains(t, children[os.Getppid()], os.Getpid()) +// rec encodes one task_iter_rec as the step_task_iter iterator streams it. +func rec(tid, tgid, ppid, nsTgid uint32) []byte { + b := make([]byte, taskIterRecSize) + binary.NativeEndian.PutUint32(b[0:], tid) + binary.NativeEndian.PutUint32(b[4:], tgid) + binary.NativeEndian.PutUint32(b[8:], ppid) + binary.NativeEndian.PutUint32(b[12:], nsTgid) + return b } -func TestSubtreePids_IncludesSelfAndDescendants(t *testing.T) { - children := map[int][]int{100: {200, 300}, 300: {400}} - assert.ElementsMatch(t, []int{100, 200, 300, 400}, subtreePids(100, children)) - assert.Equal(t, []int{400}, subtreePids(400, children)) +func concat(recs ...[]byte) []byte { + var out []byte + for _, r := range recs { + out = append(out, r...) + } + return out +} + +func TestParseTaskRecords_BuildsTreeAndTranslation(t *testing.T) { + // Global ids on the left, namespace pids on the right — deliberately + // different so a test that conflates them fails. + snap := parseTaskRecords(concat( + rec(1, 1, 0, 1), + rec(100, 100, 1, 7), + rec(101, 100, 1, 7), // thread of 100: contributes a tid, nothing else + rec(200, 200, 100, 8), + rec(300, 300, 200, 9), + rec(400, 400, 1, 10), + rec(5, 5, 5, 5), // self-parented: no child edge to itself + []byte{1, 2, 3}, // truncated trailing record: ignored + )) + + assert.Equal(t, []uint32{100, 101}, snap.tids[100]) + assert.Equal(t, uint32(100), snap.global[7]) + assert.Equal(t, uint32(200), snap.global[8]) + _, threadListed := snap.global[0] + assert.False(t, threadListed) + assert.Equal(t, []uint32{100, 400}, snap.children[1]) + assert.Equal(t, []uint32{200}, snap.children[100]) + assert.Empty(t, snap.children[5]) + assert.Equal(t, []uint32{100, 200, 300}, snap.subtree(100)) + assert.Equal(t, []uint32{400}, snap.subtree(400)) +} + +func TestProcSnapshot_SubtreeTerminatesOnCycle(t *testing.T) { + snap := &procSnapshot{children: map[uint32][]uint32{1: {2}, 2: {1}}} + assert.Equal(t, []uint32{1, 2}, snap.subtree(1)) +} + +func TestPidNamespaceInode_MatchesProcLink(t *testing.T) { + got, err := pidNamespaceInode() + require.NoError(t, err) + link, err := os.Readlink("/proc/self/ns/pid") + require.NoError(t, err) + want, err := strconv.ParseUint(strings.TrimSuffix(strings.TrimPrefix(link, "pid:["), "]"), 10, 32) + require.NoError(t, err) + assert.Equal(t, uint32(want), got) } func TestFindAncestorByComm_FindsParent(t *testing.T) { @@ -232,50 +283,173 @@ func newTaskMap(t *testing.T) *ebpf.Map { return m } -func TestTagContainerProcess_LeaderOverwriteDescendantCreateOnly(t *testing.T) { +// containerSnapshot is a synthetic task walk: leader global tgid 1000 (pid +// 50 in our namespace) with two threads, a pre-tagged child 2000 and an +// untagged child 3000, each with a grandchild. +func containerSnapshot() *procSnapshot { + return &procSnapshot{ + tids: map[uint32][]uint32{1000: {1000, 1001}, 2000: {2000}, 3000: {3000}, 3100: {3100}}, + children: map[uint32][]uint32{1000: {2000, 3000}, 3000: {3100}}, + global: map[int]uint32{50: 1000, 51: 2000, 52: 3000, 53: 3100}, + } +} + +func newSnapshotTracker(t *testing.T, snap *procSnapshot, err error) (*Tracker, *ebpf.Map) { + t.Helper() m := newTaskMap(t) - tr := &Tracker{taskMap: m} - - // Pin this goroutine to one OS thread so its tid is guaranteed to exist - // in /proc//task both when pre-seeding and when asserting — the Go - // runtime creates and parks threads at will, so any other tid could race. - runtime.LockOSThread() - defer runtime.UnlockOSThread() - selfTid := uint32(unix.Gettid()) - - // Two descendants of the leader (this test process): one already carries - // an ordinal — standing in for a child the fork tracepoint tagged — and - // one is untagged. - startSleeper := func() int { - cmd := exec.Command("sleep", "30") - require.NoError(t, cmd.Start()) - t.Cleanup(func() { - _ = cmd.Process.Kill() - _ = cmd.Wait() - }) - return cmd.Process.Pid + tr := &Tracker{ + taskMap: m, + logger: slog.Default(), + snapshotFn: func() (*procSnapshot, error) { return snap, err }, } - preTaggedPid := startSleeper() - untaggedPid := startSleeper() + return tr, m +} + +func TestTagContainerProcess_LeaderOverwriteDescendantCreateOnly(t *testing.T) { + tr, m := newSnapshotTracker(t, containerSnapshot(), nil) - // Pre-seed: our own tid simulates a stale leader entry (exec re-tag into - // a long-lived container / recycled tid), the first child a + // Pre-seed: a leader thread simulates a stale entry (exec re-tag into a + // long-lived container / recycled tid), the first child a // kernel-inherited descendant tag. - require.NoError(t, m.Put(selfTid, uint32(99))) - require.NoError(t, m.Put(uint32(preTaggedPid), uint32(7))) + require.NoError(t, m.Put(uint32(1001), uint32(99))) + require.NoError(t, m.Put(uint32(2000), uint32(7))) - tr.TagContainerProcess(os.Getpid(), 42) + // The caller speaks namespace pids; the map is keyed by global tids. + tr.TagContainerProcess(50, 42) - // Leader threads are written UpdateAny: the stale ordinal must be replaced. var got uint32 - require.NoError(t, m.Lookup(selfTid, &got)) - assert.Equal(t, uint32(42), got, "leader tid must be overwritten (UpdateAny)") - - // Descendants are create-only: a kernel-inherited ordinal survives... - require.NoError(t, m.Lookup(uint32(preTaggedPid), &got)) + require.NoError(t, m.Lookup(uint32(1000), &got)) + assert.Equal(t, uint32(42), got, "leader must be tagged under its global tid") + require.NoError(t, m.Lookup(uint32(1001), &got)) + assert.Equal(t, uint32(42), got, "leader thread must be overwritten (UpdateAny)") + require.NoError(t, m.Lookup(uint32(2000), &got)) assert.Equal(t, uint32(7), got, "pre-tagged descendant must keep its ordinal (UpdateNoExist)") - - // ...while an untagged descendant picks up the container's ordinal. - require.NoError(t, m.Lookup(uint32(untaggedPid), &got)) + require.NoError(t, m.Lookup(uint32(3000), &got)) assert.Equal(t, uint32(42), got, "untagged descendant must be tagged") + require.NoError(t, m.Lookup(uint32(3100), &got)) + assert.Equal(t, uint32(42), got, "grandchild must be tagged") + assert.Error(t, m.Lookup(uint32(50), &got), "the namespace pid itself must never be used as a key") +} + +func TestAdoptContainerProcess_CreateOnlyForLeaderToo(t *testing.T) { + tr, m := newSnapshotTracker(t, containerSnapshot(), nil) + require.NoError(t, m.Put(uint32(1000), uint32(7))) + + tr.AdoptContainerProcess(50, 42) + + var got uint32 + require.NoError(t, m.Lookup(uint32(1000), &got)) + assert.Equal(t, uint32(7), got, "adoption must not demote a live leader") + require.NoError(t, m.Lookup(uint32(1001), &got)) + assert.Equal(t, uint32(42), got) + require.NoError(t, m.Lookup(uint32(3000), &got)) + assert.Equal(t, uint32(42), got) +} + +func TestTagContainerProcess_UnknownPidAndWalkFailureAreNoOps(t *testing.T) { + tr, m := newSnapshotTracker(t, containerSnapshot(), nil) + tr.TagContainerProcess(999, 42) // exited, or numbered in another namespace + var got uint32 + assert.Error(t, m.Lookup(uint32(999), &got)) + + failing, m2 := newSnapshotTracker(t, nil, errors.New("iterator closed")) + failing.TagContainerProcess(50, 42) + assert.Error(t, m2.Lookup(uint32(1000), &got)) +} + +// loadTcObjects loads the tcbpf collection that owns the tracker's shared +// maps, exactly as cmd/start.go does before steps.Start. +func loadTcObjects(t *testing.T) *bpf.TcBpfObjects { + t.Helper() + spec, err := bpf.LoadTcBpf() + require.NoError(t, err) + var objs bpf.TcBpfObjects + if err := spec.LoadAndAssign(&objs, nil); err != nil { + t.Skipf("tcbpf objects not loadable (needs root): %v", err) + } + t.Cleanup(func() { objs.Close() }) + return &objs +} + +// TestStart_TagsWorkerChild is the production path end to end: Start with +// this test process declared as Runner.Worker, then fork a child and check +// every id crossing — seeding keyed by global tids, the boundary event and +// map_task_nspid carrying our namespace's numbering. Run on its own it +// covers a plain VM; TestStart_InPidNamespace re-runs it inside a fresh pid +// namespace, where every one of those crossings used to be wrong. +func TestStart_TagsWorkerChild(t *testing.T) { + objs := loadTcObjects(t) + tr, err := Start(objs, Options{WorkerPID: os.Getpid(), OrdinalBase: 100}, nil, slog.Default()) + if err != nil && strings.Contains(err.Error(), "BTF") { + t.Skipf("step tracker needs kernel BTF: %v", err) + } + require.NoError(t, err) + t.Cleanup(tr.Close) + + snap, err := tr.snapshotFn() + require.NoError(t, err) + selfTgid, ok := snap.global[os.Getpid()] + require.True(t, ok, "the daemon must see itself in the task walk") + assert.Equal(t, selfTgid, tr.workerTgid) + + // Seeding: the worker's (our) threads are runner infrastructure, keyed + // by global tid, and the namespace table names us by our own pid. + var got uint32 + require.NoError(t, objs.MapTaskStep.Lookup(selfTgid, &got)) + assert.Equal(t, uint32(events.StepOrdinalRunner), got) + require.NoError(t, objs.MapTaskNspid.Lookup(selfTgid, &got)) + assert.Equal(t, uint32(os.Getpid()), got) + + child := exec.Command("sleep", "5") + require.NoError(t, child.Start()) + t.Cleanup(func() { _ = child.Process.Kill(); _ = child.Wait() }) + childPID := child.Process.Pid + + snap, err = tr.snapshotFn() + require.NoError(t, err) + childTgid, ok := snap.global[childPID] + require.True(t, ok, "child must be visible to the task walk") + var ordinal uint32 + require.NoError(t, objs.MapTaskStep.Lookup(childTgid, &ordinal), + "worker child must be tagged under its global tid") + // Ordinals are opaque group ids, not positions: transient runtime forks + // (Go's one-time pidfd probe) consume them too, so only the base is a + // floor — see Options.OrdinalBase. + assert.GreaterOrEqual(t, ordinal, uint32(100)) + require.NoError(t, objs.MapTaskNspid.Lookup(childTgid, &got)) + assert.Equal(t, uint32(childPID), got) + + // The reconciler saw the boundary event (its tgid is our numbering, so + // the cmdline read behind it works too — visible in -v output). + assert.Eventually(t, func() bool { return tr.OrdinalAt(time.Now()) >= ordinal }, + 2*time.Second, 20*time.Millisecond, "boundary must reach the reconciler") +} + +// TestStart_InPidNamespace re-executes TestStart_TagsWorkerChild inside a +// fresh pid (and mount, for /proc) namespace — the shape of an ARC runner +// pod, where /proc pids and kernel pids disagree. +func TestStart_InPidNamespace(t *testing.T) { + runInPidNamespace(t, "^TestStart_TagsWorkerChild$") +} + +// runInPidNamespace runs the named test of this binary under +// unshare --pid --fork --mount-proc and requires it to pass there. Needs +// root (like every BPF test here) and util-linux. +func runInPidNamespace(t *testing.T, run string) { + t.Helper() + if os.Geteuid() != 0 { + t.Skip("needs root") + } + unshare, err := exec.LookPath("unshare") + if err != nil { + t.Skip("unshare not available") + } + out, err := exec.Command(unshare, "--pid", "--fork", "--mount-proc", + os.Args[0], "-test.run", run, "-test.v").CombinedOutput() + t.Logf("inside pid namespace:\n%s", out) + if strings.Contains(string(out), "--- SKIP") { + t.Skip("inner test skipped") + } + require.NoError(t, err) + require.Contains(t, string(out), "--- PASS") } From 7401a191363b6d217860e6f0184fee1e06137d8f Mon Sep 17 00:00:00 2001 From: Matthew DeVenny Date: Thu, 17 Sep 2026 12:38:27 -0700 Subject: [PATCH 2/3] #135 review follow-up: forward upid walk, generated task_iter_rec mirror - ns_tgid_of walks the upid chain from numbers[0] upward with a plain bounded index, capping level at PIDNS_MAX_LEVEL-1 and reading each struct upid once via bpf_core_read. The daemon's namespace appears at most once per chain, so the inward-out order bought nothing and cost a data-dependent index expression. - task_iter_rec now has a bpf2go-generated Go mirror (-type task_iter_rec); parseTaskRecords and the bpf test helper decode with it instead of hand-written offsets, and the BTF layout test pins the C struct against the generated type's offsets. - design.md states explicitly that BPF_ITER_CREATE shipped in 5.8 with the iter/task target and bpf_seq_write (uapi bpf.h at v5.8 lists it, v5.7 does not), so the kernel floor claim is unchanged. Signed-off-by: Matthew DeVenny --- bpf/stepbpf.c | 18 +++++++++++------- bpf/stepbpf_bpfeb.go | 8 ++++++++ bpf/stepbpf_bpfeb.o | Bin 43800 -> 43448 bytes bpf/stepbpf_bpfel.go | 8 ++++++++ bpf/stepbpf_bpfel.o | Bin 43800 -> 43448 bytes bpf/stepbpf_test.go | 35 ++++++++++++++++++++++------------- cargowall.go | 2 +- design.md | 6 ++++-- pkg/steps/steps.go | 33 +++++++++++++-------------------- pkg/steps/steps_test.go | 10 ++++------ 10 files changed, 71 insertions(+), 49 deletions(-) diff --git a/bpf/stepbpf.c b/bpf/stepbpf.c index b1559ac..5ac35c3 100644 --- a/bpf/stepbpf.c +++ b/bpf/stepbpf.c @@ -125,9 +125,10 @@ static __always_inline struct step_state *step_state(void) { // ns_tgid_of returns t's process id as numbered in the daemon's pid // namespace, or 0 when t lives outside that namespace's subtree (another // pod, the host) or translation is disabled. Walks the group leader's upid -// chain from the innermost namespace outward: a task directly in the -// daemon's namespace matches on the first probe, a task inside a nested -// container (docker under the runner) one or two probes later. +// chain — numbers[0] is the init namespace, numbers[level] the task's own — +// looking for the namespace whose inode userspace passed in. The daemon's +// namespace appears at most once in a chain, so order is irrelevant; a +// plain bounded index keeps the flexible-array access simple. static __always_inline __u32 ns_tgid_of(struct task_struct *t) { if (!pidns_ino) @@ -136,13 +137,16 @@ static __always_inline __u32 ns_tgid_of(struct task_struct *t) if (!p) return 0; unsigned int level = BPF_CORE_READ(p, level); + if (level >= PIDNS_MAX_LEVEL) + level = PIDNS_MAX_LEVEL - 1; for (unsigned int i = 0; i < PIDNS_MAX_LEVEL; i++) { if (i > level) break; - unsigned int l = level - i; - struct pid_namespace *ns = BPF_CORE_READ(p, numbers[l].ns); - if (BPF_CORE_READ(ns, ns.inum) == pidns_ino) - return BPF_CORE_READ(p, numbers[l].nr); + struct upid up; + if (bpf_core_read(&up, sizeof(up), &p->numbers[i])) + break; + if (BPF_CORE_READ(up.ns, ns.inum) == pidns_ino) + return up.nr; } return 0; } diff --git a/bpf/stepbpf_bpfeb.go b/bpf/stepbpf_bpfeb.go index 6f8a2ae..d0c96f5 100644 --- a/bpf/stepbpf_bpfeb.go +++ b/bpf/stepbpf_bpfeb.go @@ -20,6 +20,14 @@ type StepBpfStepState struct { NextOrdinal uint64 } +type StepBpfTaskIterRec struct { + _ structs.HostLayout + Tid uint32 + Tgid uint32 + Ppid uint32 + NsTgid uint32 +} + // Names of all BPF objects in the ELF. // // Used for safe lookups in a Collection or CollectionSpec. diff --git a/bpf/stepbpf_bpfeb.o b/bpf/stepbpf_bpfeb.o index 914e1de0de7698a7a8cfdeafb2752db8dfb2fa6b..2fe07419f6295c69f23207c57b4d75cda1bf106c 100644 GIT binary patch delta 6026 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 delta 6292 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 f-mJ8LfFa*pH2q{$D)9{s$r& zgFGy3h!<2AhKHjQIX+r$1QrtMghF~vyp64*z{ zYjV!X_#sDX%4GiI*LkU{fKNDXHpY07voK$-><@K3&fA?E)8yVZ(JUb0XUF-c&gJqP z@DkT_V{EX(^|~?rqL`1zY!P9*vqrD?T*^rg3!*GKu;QJ^5XJ8|i+F2d!9sg|Za5YL z>R7nzVlAD}h9BWi<23!xvmuVBh!($GOm2Dn(%NUEIlM&%XXi~9%Iw6DIvdLbCnBLA zJ{xkeU94Ed$IikdvR`o)_+i8Qe@?*X>I4+Z*3ZgT_7M)tlg*m$mG{t-FiQl8wSyKe zw(p}{sdI^p(M=5(%lq1t^`+<^S@3tP+xbm~JBL=#K7md5UX>@WUyYl<+Z=OvV^Wqe z%6BJyZOk0JKl#wAmG1eBrQ%LT5jF#Qr<^pagz+$JBY3i4$MCEiz5sMuzb@!Yc*Pz0 zMmgVx-)s5aJG|j)cyV-DIZ~nCaBQ7Ea3l0xs^@#4Q*T{(T9w`hy;H^0o^(MU#hauX z`Zc`$czU?2Du4Dy_RG{cffAL2ps|2=wjHAH^tR&>vFvMsNiBV|dfbbSrxw^r(s- zfxa(Ij$V#M>V@5p-TQHMwsjIZ#Jz-vr=;^}wKMB9ENGDOIDRQHpMZX)guhU|&@lLC z)vHqz7R#W$a6G{mtn;QGEM=?^@#T1q^15|i;}qYvZjtc>=M$bAU#hI&b8#W;v_rZ)p05nm(rK|J3vlwiqHt{I_QJ zXw!D6N=ShosmrMr@VfQhaFOO-uIXzv{Z384Tho84>AN(2kETcY+w1c)`!vfLO@Eua zHslvy)GXtg{+*_~LNoz6)v1Gi$8E%?b?RbHR~%H)>quRCeYrxkpH@C zCO;oJ$15W%`9HeyHc>D|ZfR|4XnLTnxuLB&{FA_(_SX6S9ou#_KhT0!bLac#v@R&y z-hTg<=C&PoZM%E!To$}}8B+s^S6lPW_O|VQ46(iK7I=e$u08!p8=v{YV1AJzV{7oH z=S6JHH9W83R?v=I#-lC>%ZGknaFX}HF)Zr=JdFJk?BT|f-pDP28T$|3DX_YROz}63 z_|J23l}AiQ_1ETPuwU^6{CRJhETDMCJjQY^m<$7pc;aL*EiSbWfpgzA8C6l@>4< zSr<*#s0@1$FyoTRx)i?+P8~B@x8hI0-peM7YU}~`D*hIJ>r0dMDRvhzmgF)_JFNFV^%^Ot% zhPy@yyox_St`N3BV+IarO#X7kR}r5!VX}I~nG29WC4XgaRz#{;tQ%Y=8&JFnq24>O zIB{K#DE=Az)>V^@D()t}hScyUnNs}-oO~JQQZYw1>$eQXpI^Nz){6}$e-b0y&6vi4+zH2+5saf-~t+0J_j39JZ%vY@;jK@ zDQC2SxWK`tDQ*V)Fdd)9Gr)eukHJ54zJmoc&IAXEF+Mv^9R@KY74Qyt`Wgr8P&@@U z#lmuQE1pSQ-)FS>I%*AMy5RZX~v;DoSkX^OL<3d%(4t-vZYw{vN&h zUc{zqoD6Q&nEV}z7nWejhOo(!BkW!ntk)c|JKhQQVTVtv2EVXMncn{4W8w6#@4h7Y`G&&=2roBaS3Tm#idcPT}$l5Y#vg6yS9ue zwrdL&aZ14EfL$x7R77e`wpBNPTQ#QQ(xEXGBfD7HwI!hvNJf;`+N7*WZNVgwh(^ol+-55fL-2#s^+s z$jML?Vw?lUosY2#j5{t^9JX?Ks4inAllfP?sjeIgyc5sU*kO{>&>zxx2s&-*i?m@7 znm)yEM1dta4W{@VitpoR_IcCjKu?hYj6V10^1tpYjEumdj}?SRA1egb#~KCeWBKF8 zDhC&!&o}T{fCT4F@E3~fZ{#AjRT55?K(GY`d6M!M`!_CE2R^t@!-V zqi7lwvk_KE&4KdgM6e~O^ZwELqtu-)NHxnSF-^ct3dH7#*o07_Xhxwm!p|tN96W0@ z&INDQm2m@Jy^?vwk*3r> z&5|$`CVL>x z(3!iA4JK#j7tG1co1IhWpEc+>{*Yk?8+gxi>CRw7@eQbV z#p1oVp}0ia?Kl2of^gV_xW>0~U%$^fbW|qs8DHKXF>z*J>0fGg9pqQ~%M3R!KC#N2 z*2~*Yl=0vRHy=6?GK0PR%84?oOZ|JHo-ZF*W;P;bpv=5z@w0f1S^UaCnbFG640w6v zN&IIziMO3BvpSOGSd00~Czlyf{&jykcb-~c{YsiYwD`@ZmRkO&B)0O-Q_IZgQGWJR XS)gm5G}`xzRy-Y#T2k?Ch$8+A?!SmD delta 6154 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 Date: Thu, 17 Sep 2026 13:04:45 -0700 Subject: [PATCH 3/3] #135 clear map_task_nspid at Close; test the connect hook inside a pid namespace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tcbpf connect/sendmsg hooks are attached by cmd/start.go and outlive the step tracker — on shutdown, and for the whole run when steps.Start fails after the iterator has already seeded the table. With the tracepoints detached nothing maintained map_task_nspid, so a recycled tid could resolve to a dead task's process. Tracker.Close now empties the table after detaching, which puts the hooks on their documented global-tgid fallback. The hooks stay attached before steps.Start on purpose (a comment now says why): a socket connecting in that startup window records the global tgid, but attaching later would leave the same sockets with no pid at all, and only a process name from before the job's step can run is affected. TestStart_TagsWorkerChild now attaches cgroup/connect4 like start.go, connects a UDP socket, and asserts map_sock_pid holds our namespace's pid and map_sock_step the seeded runner tag — meaningful only in the in-namespace run, where the two numberings differ — and that the table is empty once the tracker is closed. Signed-off-by: Matthew DeVenny --- cmd/start.go | 6 +++++ pkg/steps/steps.go | 27 +++++++++++++++++++++ pkg/steps/steps_test.go | 54 ++++++++++++++++++++++++++++++++++++++--- 3 files changed, 83 insertions(+), 4 deletions(-) diff --git a/cmd/start.go b/cmd/start.go index f5fbac3..d3773ec 100644 --- a/cmd/start.go +++ b/cmd/start.go @@ -537,6 +537,12 @@ func startCargoWall(cmd *StartCmd, hooks *StartHooks, teardowns *teardownList) e // Attach cgroup programs for PID tracking via socket cookie. // Best-effort: if attachment fails, TC filtering still works but PID will be 0. + // Attached before steps.Start on purpose: a socket connecting before + // the step tracker has seeded map_task_nspid records the global tgid + // (unresolvable from inside a pid namespace), but attaching later would + // leave those same sockets with no pid at all. The window is daemon + // startup — the job's step is still waiting on readiness — and only the + // process name of a connection made in it is affected. cgroupProgs := []struct { prog *ebpf.Program attach ebpf.AttachType diff --git a/pkg/steps/steps.go b/pkg/steps/steps.go index 7ee2e2a..02e4042 100644 --- a/pkg/steps/steps.go +++ b/pkg/steps/steps.go @@ -100,6 +100,7 @@ type Tracker struct { reader *ringbuf.Reader stateMap *ebpf.Map taskMap *ebpf.Map + nspidMap *ebpf.Map // map_task_nspid; cleared at Close, see there auditLogger *events.AuditLogger logger *slog.Logger done chan struct{} @@ -174,6 +175,7 @@ func Start(tcObjs *bpf.TcBpfObjects, opts Options, auditLogger *events.AuditLogg workerCmdline: readCmdline(workerPID), stateMap: tcObjs.MapStepState, taskMap: tcObjs.MapTaskStep, + nspidMap: tcObjs.MapTaskNspid, auditLogger: auditLogger, logger: logger, done: make(chan struct{}), @@ -273,9 +275,34 @@ func (t *Tracker) Close() { for _, l := range t.links { _ = l.Close() } + // The tcbpf connect/sendmsg hooks outlive this tracker (they are + // attached by cmd/start.go, and stay up when Start fails after the + // iterator has already seeded the table). With the tracepoints gone + // nothing maintains map_task_nspid, so a recycled tid would resolve to + // a dead task's process. Empty it: the hooks then take their documented + // global-tgid fallback, exactly as when attribution was never on. + clearMap(t.nspidMap) t.objs.Close() } +// clearMap deletes every entry of a u32→u32 hash map. +func clearMap(m *ebpf.Map) { + if m == nil { + return + } + var ( + keys []uint32 + k, v uint32 + ) + it := m.Iterate() + for it.Next(&k, &v) { + keys = append(keys, k) + } + for _, k := range keys { + _ = m.Delete(k) + } +} + func (t *Tracker) attach() error { forkLink, err := link.AttachTracing(link.TracingOptions{ Program: t.objs.StepFork, diff --git a/pkg/steps/steps_test.go b/pkg/steps/steps_test.go index f3a0ac0..a747548 100644 --- a/pkg/steps/steps_test.go +++ b/pkg/steps/steps_test.go @@ -29,8 +29,10 @@ import ( "time" "github.com/cilium/ebpf" + "github.com/cilium/ebpf/link" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "golang.org/x/sys/unix" "github.com/code-cargo/cargowall/bpf" "github.com/code-cargo/cargowall/pkg/events" @@ -369,20 +371,48 @@ func loadTcObjects(t *testing.T) *bpf.TcBpfObjects { return &objs } +// connectedUDPCookie opens a UDP socket, connects it (which runs the +// cgroup/connect4 hook in this thread's context) and returns its socket +// cookie — the key the hook wrote map_sock_pid and map_sock_step under. +func connectedUDPCookie(t *testing.T) uint64 { + t.Helper() + fd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM|unix.SOCK_CLOEXEC, 0) + require.NoError(t, err) + t.Cleanup(func() { _ = unix.Close(fd) }) + require.NoError(t, unix.Connect(fd, &unix.SockaddrInet4{Port: 9, Addr: [4]byte{127, 0, 0, 1}})) + cookie, err := unix.GetsockoptUint64(fd, unix.SOL_SOCKET, unix.SO_COOKIE) + require.NoError(t, err) + return cookie +} + // TestStart_TagsWorkerChild is the production path end to end: Start with // this test process declared as Runner.Worker, then fork a child and check // every id crossing — seeding keyed by global tids, the boundary event and -// map_task_nspid carrying our namespace's numbering. Run on its own it -// covers a plain VM; TestStart_InPidNamespace re-runs it inside a fresh pid -// namespace, where every one of those crossings used to be wrong. +// map_task_nspid carrying our namespace's numbering, and the tcbpf connect +// hook recording a pid we can resolve. Run on its own it covers a plain +// VM; TestStart_InPidNamespace re-runs it inside a fresh pid namespace, +// where every one of those crossings used to be wrong. func TestStart_TagsWorkerChild(t *testing.T) { objs := loadTcObjects(t) + // The connect hook is attached by cmd/start.go, not by Start; mirror + // that so the socket-owner path is exercised too. + connectLink, err := link.AttachCgroup(link.CgroupOptions{ + Path: "/sys/fs/cgroup", Attach: ebpf.AttachCGroupInet4Connect, Program: objs.CgConnect4, + }) + require.NoError(t, err, "attach cgroup/connect4") + t.Cleanup(func() { _ = connectLink.Close() }) + tr, err := Start(objs, Options{WorkerPID: os.Getpid(), OrdinalBase: 100}, nil, slog.Default()) if err != nil && strings.Contains(err.Error(), "BTF") { t.Skipf("step tracker needs kernel BTF: %v", err) } require.NoError(t, err) - t.Cleanup(tr.Close) + closed := false + t.Cleanup(func() { + if !closed { + tr.Close() + } + }) snap, err := tr.snapshotFn() require.NoError(t, err) @@ -421,6 +451,22 @@ func TestStart_TagsWorkerChild(t *testing.T) { // the cmdline read behind it works too — visible in -v output). assert.Eventually(t, func() bool { return tr.OrdinalAt(time.Now()) >= ordinal }, 2*time.Second, 20*time.Millisecond, "boundary must reach the reconciler") + + // Socket owner: the connect hook stores the pid as we number it (what + // lookupProcessName reads), and the step tag we were seeded with. + cookie := connectedUDPCookie(t) + require.NoError(t, objs.MapSockPid.Lookup(cookie, &got)) + assert.Equal(t, uint32(os.Getpid()), got, "map_sock_pid must carry our namespace's pid") + require.NoError(t, objs.MapSockStep.Lookup(cookie, &got)) + assert.Equal(t, uint32(events.StepOrdinalRunner), got) + + // After Close the connect hook is still attached but nothing maintains + // the translation table, so it must be empty: the hook then falls back + // to the global tgid rather than resolving a recycled tid to a dead task. + tr.Close() + closed = true + var k, v uint32 + assert.False(t, objs.MapTaskNspid.Iterate().Next(&k, &v), "map_task_nspid must be empty after Close") } // TestStart_InPidNamespace re-executes TestStart_TagsWorkerChild inside a