66// +build ignore
77#include "injection.bpf.h"
88
9- const volatile pid_t target_pid = 0 ;
9+ const volatile unsigned int target_pid_ns_inum = 0 ;
1010const volatile pid_t exclude_pid ;
1111const volatile char filter_path [61 ];
1212const volatile pid_t exit_code = ENOENT ;
@@ -16,9 +16,8 @@ unsigned int hits = 0;
1616unsigned int disruptedHits = 0 ;
1717
1818struct data_t {
19- u32 ppid ;
2019 u32 pid ;
21- u32 tid ;
20+ u32 tid ;
2221 u32 id ;
2322 char comm [100 ];
2423};
@@ -30,59 +29,232 @@ struct {
3029 __type (value , u32 );
3130} events SEC (".maps" );
3231
33- SEC ("kprobe/sys_openat" )
34- int injection_disk_failure (struct pt_regs * ctx )
32+ // openat() dirfd value meaning "relative to the current working directory".
33+ #define AT_FDCWD -100
34+
35+ #ifndef offsetof
36+ #define offsetof (TYPE , MEMBER ) __builtin_offsetof(TYPE, MEMBER)
37+ #endif
38+ // container_of recovers the enclosing struct mount from its embedded vfsmount.
39+ #ifndef container_of
40+ #define container_of (ptr , type , member ) \
41+ ((type *)((void *)(ptr) - offsetof(type, member)))
42+ #endif
43+
44+ // Maximum number of path components walked when resolving the cwd, and the
45+ // per-component name cap. filter_path is at most 60 chars. The depth is kept
46+ // small on purpose: container working directories are shallow and a larger value
47+ // makes the verifier explore too many states (the cost is super-linear in depth).
48+ #define CWD_MAX_DEPTH 10
49+ #define CWD_NAME_BUF 64
50+
51+ // Resolved-path buffer. PATH_MASK keeps the running write offset provably in
52+ // bounds (offset & PATH_MASK) so the verifier does not have to track it
53+ // precisely; PATH_BUF leaves room for one full CWD_NAME_BUF write at the highest
54+ // masked offset. The offset wraps past PATH_MASK, but the filter is at most 60
55+ // chars so only the first bytes of the path are ever compared.
56+ #define PATH_BUF 320
57+ #define PATH_MASK 255
58+
59+ // Scratch space for cwd resolution. Kept in a per-CPU array map instead of on
60+ // the stack: the dentry chain and the path buffer exceed the 512 byte BPF stack
61+ // limit.
62+ #if defined(__TARGET_ARCH_arm64 ) || defined(__TARGET_ARCH_x86 )
63+ struct cwd_scratch {
64+ struct dentry * chain [CWD_MAX_DEPTH ];
65+ char filter [62 ];
66+ char path [PATH_BUF ];
67+ };
68+
69+ struct {
70+ __uint (type , BPF_MAP_TYPE_PERCPU_ARRAY );
71+ __uint (max_entries , 1 );
72+ __type (key , u32 );
73+ __type (value , struct cwd_scratch );
74+ } cwd_scratch_map SEC (".maps" );
75+ #endif
76+
77+ // abs_path_matches_filter applies the prefix filter to an absolute path.
78+ #if defined(__TARGET_ARCH_arm64 ) || defined(__TARGET_ARCH_x86 )
79+ static __always_inline int abs_path_matches_filter (const char * p )
80+ {
81+ for (int i = 0 ; i < 61 ; i ++ ) {
82+ char fc = filter_path [i ];
83+ if (fc == '\0' )
84+ break ;
85+ if (p [i ] != fc )
86+ return 0 ;
87+ }
88+ return 1 ;
89+ }
90+
91+ // rel_path_matches_filter resolves a relative open against the process current
92+ // working directory and applies the prefix filter to the resulting absolute
93+ // path. The cwd is reconstructed by walking the dentry chain up to the global
94+ // root, crossing mount points (jumping to the mountpoint dentry in the parent
95+ // mount) like d_path does. The components are then streamed root->leaf, followed
96+ // by "/" + relpath, and compared char by char against filter_path. Path
97+ // components like "." and ".." are not normalised.
98+ static __always_inline int rel_path_matches_filter (struct dentry * start_dentry , struct vfsmount * start_mnt , const char * relpath )
99+ {
100+ u32 zero = 0 ;
101+ struct cwd_scratch * scratch = bpf_map_lookup_elem (& cwd_scratch_map , & zero );
102+ if (scratch == NULL )
103+ return 0 ;
104+
105+ // Copy the filter out of .rodata into a runtime buffer so the verifier treats
106+ // its bytes as unknown scalars (see match_filter_char): this keeps the nested
107+ // cwd loops prunable instead of forking a state per known filter byte.
108+ bpf_probe_read_kernel (scratch -> filter , sizeof (scratch -> filter ), (const void * )filter_path );
109+
110+ int filter_len = 0 ;
111+ for (int i = 0 ; i < 61 ; i ++ ) {
112+ if (scratch -> filter [i ] == '\0' )
113+ break ;
114+ filter_len = i + 1 ;
115+ }
116+ if (filter_len == 0 )
117+ return 1 ; // empty filter matches everything
118+
119+ // Collect the path component dentries leaf->root, crossing mount boundaries.
120+ struct dentry * dentry = start_dentry ;
121+ struct vfsmount * vfsmnt = start_mnt ;
122+ struct mount * mnt = container_of (vfsmnt , struct mount , mnt );
123+ int n = 0 ;
124+
125+ // A few extra iterations beyond CWD_MAX_DEPTH absorb mount crossings (which do
126+ // not add a component). Kept tight: more iterations explode verifier state.
127+ for (int i = 0 ; i < CWD_MAX_DEPTH + 6 ; i ++ ) {
128+ struct dentry * mnt_root = BPF_CORE_READ (vfsmnt , mnt_root );
129+ struct dentry * parent = BPF_CORE_READ (dentry , d_parent );
130+
131+ if (dentry == mnt_root ) {
132+ struct mount * mnt_parent = BPF_CORE_READ (mnt , mnt_parent );
133+ if (mnt != mnt_parent ) {
134+ // Cross into the parent mount at this mount's mountpoint.
135+ dentry = BPF_CORE_READ (mnt , mnt_mountpoint );
136+ mnt = mnt_parent ;
137+ vfsmnt = & mnt -> mnt ;
138+ continue ;
139+ }
140+ break ; // reached the global root
141+ }
142+ if (dentry == parent )
143+ break ; // root without a mount crossing
144+
145+ if (n >= CWD_MAX_DEPTH )
146+ break ; // path too deep, stop collecting
147+ scratch -> chain [n ] = dentry ;
148+ n ++ ;
149+ dentry = parent ;
150+ }
151+
152+ // Build the absolute path "<cwd>/<relpath>" into scratch->path. Each component
153+ // name is copied in a single bpf_probe_read_kernel_str call (not char by char):
154+ // the running offset is only ever used masked (pos & PATH_MASK), so the verifier
155+ // does not track it precisely and the loop does not explode into a state per
156+ // possible string length (which processed >1M insns and was rejected E2BIG).
157+ int pos = 0 ;
158+
159+ // cwd components root->leaf (chain[0] is the leaf, chain[n-1] the topmost);
160+ // each contributes "/<name>".
161+ for (int k = 0 ; k < CWD_MAX_DEPTH ; k ++ ) {
162+ int idx = n - 1 - k ;
163+ if (idx < 0 )
164+ break ;
165+ idx &= (CWD_MAX_DEPTH - 1 ); // help the verifier bound the array access
166+
167+ scratch -> path [pos & PATH_MASK ] = '/' ;
168+ pos ++ ;
169+
170+ // Load the kernel dentry pointer from the map value with a plain access,
171+ // then apply CO-RE only to the kernel struct (cwd_scratch is not in vmlinux
172+ // BTF, so wrapping the whole chain in BPF_CORE_READ breaks relocation).
173+ struct dentry * comp = scratch -> chain [idx ];
174+ const char * dname = (const char * )BPF_CORE_READ (comp , d_name .name );
175+ int len = bpf_probe_read_kernel_str (& scratch -> path [pos & PATH_MASK ], CWD_NAME_BUF , dname );
176+ if (len > 1 )
177+ pos += len - 1 ; // advance over the name, dropping the trailing NUL
178+ }
179+
180+ // Append "/" + relpath.
181+ scratch -> path [pos & PATH_MASK ] = '/' ;
182+ pos ++ ;
183+ int rlen = bpf_probe_read_kernel_str (& scratch -> path [pos & PATH_MASK ], 62 , relpath );
184+ if (rlen > 1 )
185+ pos += rlen - 1 ;
186+
187+ // Prefix match: the path matches when the whole filter is a prefix of it.
188+ for (int i = 0 ; i < 61 ; i ++ ) {
189+ char fc = scratch -> filter [i ];
190+ if (fc == '\0' )
191+ return 1 ; // entire filter matched: disrupt
192+ if (i >= pos )
193+ return 0 ; // path shorter than the filter
194+ if (scratch -> path [i ] != fc )
195+ return 0 ;
196+ }
197+ return 1 ;
198+ }
199+ #endif
200+
201+ #if defined(__TARGET_ARCH_arm64 )
202+ SEC ("fmod_ret/__arm64_sys_openat" )
203+ #else
204+ SEC ("fmod_ret/__x64_sys_openat" )
205+ #endif
206+ int BPF_PROG (injection_disk_failure , struct pt_regs * real_regs )
35207{
36208 struct data_t data = {};
37209
38210 // Get data of the current process
39- u32 ppid = 0 ;
40211 u32 pid = bpf_get_current_pid_tgid ();
41212 if (pid == exclude_pid ) {
42213 return 0 ;
43214 }
44215 u32 tid = bpf_get_current_pid_tgid () >> 32 ;
45216 u32 gid = bpf_get_current_uid_gid ();
46217
47- if (pid != 1 ) {
48- // Get parent pid
49- struct task_struct * task ;
50- struct task_struct * real_parent ;
51- task = (struct task_struct * )bpf_get_current_task ();
52- bpf_probe_read (& real_parent , sizeof (real_parent ), & task -> real_parent );
53- bpf_probe_read (& ppid , sizeof (ppid ), & real_parent -> tgid );
54-
55- // Allow only children and parent process.
56- if (target_pid != 0 && ppid != target_pid && pid != target_pid ) {
57- return 0 ;
218+ if (target_pid_ns_inum != 0 ) {
219+ struct task_struct * task = (struct task_struct * )bpf_get_current_task ();
220+ unsigned int ns_inum = BPF_CORE_READ (task , nsproxy , pid_ns_for_children , ns .inum );
221+ if (ns_inum != target_pid_ns_inum ) {
222+ return 0 ;
58223 }
59224 }
60225
61- if (ppid == exclude_pid || tid == exclude_pid ) {
226+ if (tid == exclude_pid ) {
62227 return 0 ;
63228 }
64229
65230// Exclude this part of code if the following variables are not defined.
66231// It allows the go program to compile without error.
67232#if defined(__TARGET_ARCH_arm64 ) || defined(__TARGET_ARCH_x86 )
68- // Allow only file with the desired prefix.
69- struct pt_regs * real_regs = (struct pt_regs * )PT_REGS_PARM1 (ctx );
233+ int dirfd = (int )PT_REGS_PARM1_CORE (real_regs );
70234 char * path = (char * )PT_REGS_PARM2_CORE (real_regs );
71235 char cmp_path_name [62 ];
72236 bpf_probe_read (& cmp_path_name , sizeof (cmp_path_name ), path );
73- char cmp_expected_path [62 ];
74- bpf_probe_read (cmp_expected_path , sizeof (cmp_expected_path ), (const void * )filter_path );
75- int filter_len = (int ) (sizeof (filter_path ) / sizeof (filter_path [0 ])) - 1 ;
76-
77- if (filter_len > 62 ) {
78- return 0 ;
79- }
80237
81- for (int i = 0 ; i < filter_len ; ++ i ) {
82- if (cmp_expected_path [i ] == NULL )
83- break ;
84- if (cmp_path_name [i ] != cmp_expected_path [i ])
85- return 0 ;
238+ if (cmp_path_name [0 ] == '/' ) {
239+ // Absolute path: apply the prefix filter directly.
240+ if (!abs_path_matches_filter (cmp_path_name ))
241+ return 0 ;
242+ } else {
243+ // Relative path: resolve it against the process current working directory
244+ // and apply the filter to the resulting absolute path. Only AT_FDCWD-relative
245+ // opens can be resolved this way; opens relative to a real directory fd are
246+ // left untouched to avoid disrupting paths outside the filter.
247+ if (dirfd != AT_FDCWD )
248+ return 0 ;
249+
250+ struct task_struct * cwd_task = (struct task_struct * )bpf_get_current_task ();
251+ struct dentry * cwd_dentry = BPF_CORE_READ (cwd_task , fs , pwd .dentry );
252+ struct vfsmount * cwd_mnt = BPF_CORE_READ (cwd_task , fs , pwd .mnt );
253+ if (cwd_dentry == NULL || cwd_mnt == NULL )
254+ return 0 ;
255+
256+ if (!rel_path_matches_filter (cwd_dentry , cwd_mnt , cmp_path_name ))
257+ return 0 ;
86258 }
87259#endif
88260
@@ -101,7 +273,6 @@ int injection_disk_failure(struct pt_regs *ctx)
101273 disruptedHits ++ ;
102274 }
103275
104- data .ppid = ppid ;
105276 data .pid = pid ;
106277 data .tid = tid ;
107278 data .id = gid ;
@@ -112,9 +283,6 @@ int injection_disk_failure(struct pt_regs *ctx)
112283 // Add the event to the ring buffer
113284 bpf_perf_event_output (ctx , & events , BPF_F_CURRENT_CPU , & data , 100 );
114285
115- // Override return of process with an -ENOENT error.
116- bpf_override_return (ctx , - exit_code );
117-
118- return 0 ;
286+ return - (int )exit_code ;
119287}
120288
0 commit comments