55#include "utils/event_helpers.h"
66#include "utils/process_tracking.h"
77
8- /* (rss_stat mm_id << 32 | member) -> {owning tgid, that tgid's mm at seeding time,
9- * last in-context size}. Keyed per counter so an external (curr==0) update is
10- * attributed only once that mm/member was established in-context. An external event
11- * may only lower a counter: any size above the last in-context value is dropped, so
12- * neither a stale/racing reclaim read nor an mm_id hash collision with another task
13- * can invent a peak. mm_id is only a hash, so the pointer is stored alongside and
14- * revalidated against pid_mm on the external path: an entry whose mm the owner no
15- * longer holds describes a freed mm_struct whose slab slot (and therefore hash) has
16- * been recycled by an unrelated address space. */
8+ /* (rss_stat mm_id << 32 | member) -> {owning tgid, its mm when seeded, last
9+ * in-context size}. An external (curr==0) update may only lower the counter, and
10+ * only while pid_mm still binds the owner to the seeded mm: mm_id is a hash, so
11+ * without both guards a stale reclaim read, a hash collision, or a recycled
12+ * mm_struct slab slot could invent a peak. */
1713struct rss_owner {
1814 __u32 pid ;
1915 __u64 mm ;
@@ -29,19 +25,11 @@ struct {
2925/* Foreign-actor rmap attribution: rmap events run by a task other than the mm's
3026 * owner (kswapd reclaim, another process's process_madvise, khugepaged, KSM,
3127 * uffd) carry no owning-pid context, so mm_owner recovers it from the mm_struct
32- * pointer. pid_mm is the inverse, letting the exec and exit hooks remove an entry
33- * by value.
28+ * pointer. pid_mm is the inverse, letting exec and exit remove an entry by
29+ * value; attribution requires both to agree, so a stale mm fails closed .
3430 *
35- * Lifecycle invariant: every mm_owner entry is removed when its process execs
36- * (the old mm is freed mid-life) or when its thread group dies, whichever comes
37- * first; LRU eviction is only a backstop. A stale entry surviving mm-pointer
38- * reuse would misattribute another process's events, so ownership is only ever
39- * registered from an in-context (task->mm == mm) event.
40- *
41- * pid_mm is a plain hash on purpose: an LRU inverse could be evicted while its
42- * forward twin stays lookup-hot, leaving exec/exit unable to remove the live
43- * mm_owner entry. Like tracked_pids, its entries are bound to the process
44- * lifecycle and removed at group death. */
31+ * pid_mm must not use LRU eviction: losing the inverse binding would leave exec
32+ * and exit unable to remove the forward entry. */
4533struct {
4634 __uint (type , BPF_MAP_TYPE_LRU_HASH );
4735 __uint (max_entries , 10240 );
@@ -50,13 +38,42 @@ struct {
5038} mm_owner SEC (".maps" );
5139BPF_HASH_MAP (pid_mm , __u32 , __u64 , 10240 );
5240
53- /* Guard-then-write keeps the common case a read-only lookup (pid_mm is a plain
54- * hash, so a fresh entry never needs rewriting). The recorded mm is what the
55- * external rss_stat path validates against to detect a recycled mm_struct. */
56- static __always_inline void refresh_pid_mm (__u32 pid , __u64 mm ) {
57- __u64 * cur_mm = bpf_map_lookup_elem (& pid_mm , & pid );
58- if (!cur_mm || * cur_mm != mm ) {
41+ /* Rebind pid's address space; mm == 0 unbinds at exit. The mm_owner entry is only
42+ * dropped while it still names pid: a live CLONE_VM sibling shares the mm and must
43+ * keep its registration. */
44+ static __always_inline void set_pid_mm (__u32 pid , __u64 mm ) {
45+ __u64 * cur = bpf_map_lookup_elem (& pid_mm , & pid );
46+ if (cur && * cur == mm ) {
47+ return ;
48+ }
49+ if (cur ) {
50+ __u32 * owner = bpf_map_lookup_elem (& mm_owner , cur );
51+ if (owner && * owner == pid ) {
52+ bpf_map_delete_elem (& mm_owner , cur );
53+ }
54+ }
55+ if (mm ) {
5956 bpf_map_update_elem (& pid_mm , & pid , & mm , BPF_ANY );
57+ } else {
58+ bpf_map_delete_elem (& pid_mm , & pid );
59+ }
60+ }
61+
62+ /* Claim mm for pid without stealing from a live owner: CLONE_VM siblings share the
63+ * mm, and overwriting would let the child's exec-time cleanup delete the entry out
64+ * from under the still-live parent. */
65+ static __always_inline void mm_owner_take (__u64 mm , __u32 pid ) {
66+ __u32 * reg = bpf_map_lookup_elem (& mm_owner , & mm );
67+ if (!reg ) {
68+ bpf_map_update_elem (& mm_owner , & mm , & pid , BPF_ANY );
69+ return ;
70+ }
71+ if (* reg == pid ) {
72+ return ;
73+ }
74+ __u64 * reg_mm = bpf_map_lookup_elem (& pid_mm , reg );
75+ if (!reg_mm || * reg_mm != mm ) {
76+ bpf_map_update_elem (& mm_owner , & mm , & pid , BPF_ANY );
6077 }
6178}
6279
@@ -87,15 +104,13 @@ int tracepoint_rss_stat(struct trace_event_raw_rss_stat* ctx) {
87104 return 0 ;
88105 }
89106 owner = cur ;
90- /* curr means current->mm is the mm the counter belongs to. Recording it
91- * (and keeping pid_mm current, which the rmap hooks may never do when
92- * only rss_stat is attached) is what lets the external path below tell a
93- * live mm from a recycled slab slot. */
107+ /* curr == 1 means current->mm is the counter's mm. pid_mm is maintained
108+ * here too because the rmap hooks may not be attached. */
94109 struct task_struct * task = bpf_get_current_task_btf ();
95110 __u64 mm = (__u64 )BPF_CORE_READ (task , mm );
96111 struct rss_owner state = {.pid = cur , .mm = mm , .size = size };
97112 bpf_map_update_elem (& rss_counter_owner , & key , & state , BPF_ANY );
98- refresh_pid_mm (cur , mm );
113+ set_pid_mm (cur , mm );
99114 } else {
100115 struct rss_owner * found = bpf_map_lookup_elem (& rss_counter_owner , & key );
101116 if (!found ) {
@@ -108,10 +123,6 @@ int tracepoint_rss_stat(struct trace_event_raw_rss_stat* ctx) {
108123 if (cur == owner ) {
109124 return 0 ;
110125 }
111- /* mm_id is a hash of a pointer the tracepoint never exposes, so the key
112- * survives the mm it was seeded from. Once the owner no longer holds that
113- * mm (it execed, or the entry predates a pid reuse), the hash now belongs
114- * to a recycled mm_struct and its counters describe another address space. */
115126 __u64 * owner_mm = bpf_map_lookup_elem (& pid_mm , & owner );
116127 if (!owner_mm || * owner_mm != found -> mm ) {
117128 return 0 ;
@@ -205,14 +216,8 @@ static __always_inline int submit_rmap(struct vm_area_struct* vma, __s32 member,
205216 return 0 ;
206217 }
207218
208- /* Register ownership so foreign actors can later attribute to this pid.
209- * The guarded updates keep the hot path read-only in the common case and
210- * keep the LRU mm_owner entry fresh even when nothing else touches it. */
211- __u32 * reg = bpf_map_lookup_elem (& mm_owner , & mm );
212- if (!reg || * reg != pid ) {
213- bpf_map_update_elem (& mm_owner , & mm , & pid , BPF_ANY );
214- }
215- refresh_pid_mm (pid , mm );
219+ mm_owner_take (mm , pid );
220+ set_pid_mm (pid , mm );
216221 owner = pid ;
217222 } else {
218223 /* Foreign actor (task->mm != mm, including kthreads whose task->mm is NULL):
@@ -226,6 +231,12 @@ static __always_inline int submit_rmap(struct vm_area_struct* vma, __s32 member,
226231 if (!is_tracked (owner )) {
227232 return 0 ;
228233 }
234+ /* An mm_struct address may be reused while a stale owner entry remains.
235+ * Accept only the current inverse binding. */
236+ __u64 * owner_mm = bpf_map_lookup_elem (& pid_mm , & owner );
237+ if (!owner_mm || * owner_mm != mm ) {
238+ return 0 ;
239+ }
229240 }
230241
231242 /* header.tid is stamped from the current task; for a foreign actor it
@@ -331,39 +342,20 @@ int tracepoint_task_newtask(struct trace_event_raw_task_newtask* ctx) {
331342 SUBMIT_EVENT_AS (child_pid , EVENT_TYPE_FORK , { e -> data .fork .parent_pid = parent_pid ; });
332343}
333344
334- /* Remove pid's ownership registration. The mm_owner value is verified against
335- * pid before deleting: a stale pid_mm entry (LRU eviction skew) could otherwise
336- * point at an mm since re-registered by another process, and deleting that
337- * would silence a live owner's foreign attribution. */
338- static __always_inline void drop_mm_ownership (__u32 pid ) {
339- __u64 * mm = bpf_map_lookup_elem (& pid_mm , & pid );
340- if (mm ) {
341- __u32 * owner = bpf_map_lookup_elem (& mm_owner , mm );
342- if (owner && * owner == pid ) {
343- bpf_map_delete_elem (& mm_owner , mm );
344- }
345- }
346- bpf_map_delete_elem (& pid_mm , & pid );
347- }
348-
349345SEC ("tracepoint/sched/sched_process_exec" )
350346int tracepoint_sched_process_exec (void * ctx ) {
351347 __u32 pid = bpf_get_current_pid_tgid () >> 32 ;
352348 if (!is_tracked (pid )) {
353349 return 0 ;
354350 }
355351
356- /* Maintain ownership before submitting (SUBMIT_EVENT_AS returns from the
357- * function). Exec frees the old mm long before group death, so the stale
358- * pointer must be dropped here or a reused mm_struct would be misattributed. */
359- drop_mm_ownership (pid );
360-
352+ /* SUBMIT_EVENT_AS returns, so the rebind must precede it. */
361353 struct task_struct * task = bpf_get_current_task_btf ();
362354 __u64 new_mm = (__u64 )BPF_CORE_READ (task , mm );
363355 if (new_mm ) {
364- bpf_map_update_elem (& mm_owner , & new_mm , & pid , BPF_ANY );
365- bpf_map_update_elem (& pid_mm , & pid , & new_mm , BPF_ANY );
356+ mm_owner_take (new_mm , pid );
366357 }
358+ set_pid_mm (pid , new_mm );
367359
368360 SUBMIT_EVENT_AS (pid , EVENT_TYPE_EXEC , {});
369361}
@@ -398,7 +390,7 @@ int tracepoint_sched_process_exit(void* ctx) {
398390
399391 /* Drop the ownership mapping so foreign actors stop attributing to a pid
400392 * the kernel may reuse. */
401- drop_mm_ownership (pid );
393+ set_pid_mm (pid , 0 );
402394
403395 SUBMIT_EVENT_AS (pid , EVENT_TYPE_EXIT , {});
404396}
0 commit comments