88
99const volatile pid_t target_pid = 0 ;
1010const volatile pid_t exclude_pid ;
11+ const volatile int use_cgroup_filter = 0 ;
1112const volatile char filter_path [61 ];
13+ // Inode of filter_path's parent directory. When non-zero, enables filtering of
14+ // relative openat calls by comparing the process CWD inode against this value.
15+ // Works correctly inside containers because Kubernetes volumes are bind-mounted:
16+ // the host inode and the in-container inode are identical.
17+ const volatile u64 filter_dir_inode = 0 ;
18+ // Device ID paired with filter_dir_inode. Inodes are only unique within a device,
19+ // so checking both prevents false matches on bind-mounted or multi-filesystem targets.
20+ const volatile u32 filter_dir_dev = 0 ;
21+ // Second inode/device pair: set when filter_path is itself a directory. When the
22+ // CWD matches this inode/device, any relative open (except ".." escapes) is
23+ // in-scope. This handles "cd /mnt/data && cat file" alongside the parent+basename
24+ // case covered by filter_dir_inode (i.e. "cwd=/mnt && cat data/file").
25+ const volatile u64 filter_dir_inode2 = 0 ;
26+ const volatile u32 filter_dir_dev2 = 0 ;
27+
28+ // Populated from userspace with the container's cgroupv2 directory fd.
29+ // bpf_current_task_under_cgroup() matches the process itself AND any sub-cgroup
30+ // (e.g. containerd exec-<id> sub-cgroups created by kubectl exec).
31+ struct {
32+ __uint (type , BPF_MAP_TYPE_CGROUP_ARRAY );
33+ __uint (max_entries , 1 );
34+ __type (key , u32 );
35+ __type (value , u32 );
36+ } target_cgroup SEC (".maps" );
37+
1238const volatile pid_t exit_code = ENOENT ;
1339const volatile int probability = 100 ;
1440
@@ -18,7 +44,7 @@ unsigned int disruptedHits = 0;
1844struct data_t {
1945 u32 ppid ;
2046 u32 pid ;
21- u32 tid ;
47+ u32 tid ;
2248 u32 id ;
2349 char comm [100 ];
2450};
@@ -30,12 +56,111 @@ struct {
3056 __type (value , u32 );
3157} events SEC (".maps" );
3258
33- SEC ("kprobe/sys_openat" )
59+ // AT_FDCWD sentinel value; openat resolves relative paths against the CWD only
60+ // when this value is passed as dirfd.
61+ #ifndef AT_FDCWD
62+ #define AT_FDCWD -100
63+ #endif
64+
65+ // check_basename_prefix returns 1 if rel_buf starts with the basename suffix of
66+ // filter_path (the part after the last '/').
67+ static __always_inline int check_basename_prefix (const char * rel_buf )
68+ {
69+ int last_slash = 0 ;
70+ for (int i = 0 ; i < 60 ; i ++ ) {
71+ if (filter_path [i ] == '\0' ) break ;
72+ if (filter_path [i ] == '/' ) last_slash = i ;
73+ }
74+ for (int i = 0 ; i < 60 ; i ++ ) {
75+ int fi = last_slash + 1 + i ;
76+ if (fi >= 61 ) break ;
77+ if (filter_path [fi & 0x3f ] == '\0' ) break ;
78+ if (rel_buf [i ] != filter_path [fi & 0x3f ]) return 0 ;
79+ }
80+ return 1 ;
81+ }
82+
83+ // check_relative_path returns 1 if a relative openat call should be disrupted.
84+ // Two checks are attempted in order:
85+ // 1. CWD inode == filter_dir_inode (parent of filter_path) AND rel_path starts
86+ // with the basename — handles "cwd=/parent && openat(AT_FDCWD, "dir/file")".
87+ // 2. CWD inode == filter_dir_inode2 (filter_path itself, set when it is a dir)
88+ // AND rel_path does not start with ".." — handles "cwd=/dir && openat(AT_FDCWD, "file")".
89+ // Only called when dirfd == AT_FDCWD; other dirfds are not supported.
90+ // Using the CWD inode rather than walking the dentry chain works inside containers
91+ // because Kubernetes volumes are bind-mounted: host inode == in-container inode.
92+ static int check_relative_path (int dirfd , const char * rel_path )
93+ {
94+ if (dirfd != AT_FDCWD ) return 0 ;
95+ if (filter_dir_inode == 0 && filter_dir_inode2 == 0 ) return 0 ;
96+
97+ // Read CWD inode and device via task->fs->pwd.dentry->d_inode.
98+ struct task_struct * task = (struct task_struct * )bpf_get_current_task ();
99+ struct fs_struct * fs_ptr ;
100+ bpf_probe_read_kernel (& fs_ptr , sizeof (fs_ptr ), & task -> fs );
101+ struct path pwd ;
102+ bpf_probe_read_kernel (& pwd , sizeof (pwd ), & fs_ptr -> pwd );
103+ struct inode * inode_ptr ;
104+ bpf_probe_read_kernel (& inode_ptr , sizeof (inode_ptr ), & pwd .dentry -> d_inode );
105+ u64 ino = 0 ;
106+ bpf_probe_read_kernel (& ino , sizeof (ino ), & inode_ptr -> i_ino );
107+ struct super_block * sb_ptr = NULL ;
108+ bpf_probe_read_kernel (& sb_ptr , sizeof (sb_ptr ), & inode_ptr -> i_sb );
109+ u32 dev = 0 ;
110+ bpf_probe_read_kernel (& dev , sizeof (dev ), & sb_ptr -> s_dev );
111+
112+ char rel_buf [62 ] = {};
113+ bpf_probe_read (rel_buf , sizeof (rel_buf ) - 1 , rel_path );
114+
115+ // Check 1: parent inode + basename prefix.
116+ if (filter_dir_inode != 0 && ino == filter_dir_inode &&
117+ (filter_dir_dev == 0 || dev == filter_dir_dev ) &&
118+ check_basename_prefix (rel_buf )) return 1 ;
119+
120+ // Check 2: exact directory inode — match any file that does not escape via "..".
121+ if (filter_dir_inode2 != 0 && ino == filter_dir_inode2 &&
122+ (filter_dir_dev2 == 0 || dev == filter_dir_dev2 ) &&
123+ !(rel_buf [0 ] == '.' && rel_buf [1 ] == '.' )) return 1 ;
124+
125+ return 0 ;
126+ }
127+
128+ // do_filter_by_process returns 1 if the current process should be excluded (filtered out),
129+ // 0 if it should be disrupted.
130+ static __always_inline int do_filter_by_process (u32 pid , u32 ppid )
131+ {
132+ if (use_cgroup_filter ) {
133+ return bpf_current_task_under_cgroup (& target_cgroup , 0 ) != 1 ? 1 : 0 ;
134+ } else if (target_pid != 0 ) {
135+ return (ppid != target_pid && pid != target_pid ) ? 1 : 0 ;
136+ }
137+ return 0 ;
138+ }
139+
140+ // do_probability_check returns 1 if the event should be skipped due to probability sampling.
141+ static __always_inline int do_probability_check ()
142+ {
143+ if (probability == 100 ) return 0 ;
144+ if (hits != 0 ) {
145+ unsigned long long scaled = disruptedHits * 100 ;
146+ if ((scaled / hits ) > probability ) {
147+ hits ++ ;
148+ return 1 ;
149+ }
150+ }
151+ hits ++ ;
152+ disruptedHits ++ ;
153+ return 0 ;
154+ }
155+
156+ // Kprobe on do_sys_openat2 (not the arch-specific sys_openat wrapper) because
157+ // only do_sys_openat2 is tagged ALLOW_ERROR_INJECTION in fs/open.c, which is
158+ // required for bpf_override_return to actually modify the return value.
159+ SEC ("kprobe/do_sys_openat2" )
34160int injection_disk_failure (struct pt_regs * ctx )
35161{
36162 struct data_t data = {};
37163
38- // Get data of the current process
39164 u32 ppid = 0 ;
40165 u32 pid = bpf_get_current_pid_tgid ();
41166 if (pid == exclude_pid ) {
@@ -45,62 +170,44 @@ int injection_disk_failure(struct pt_regs *ctx)
45170 u32 gid = bpf_get_current_uid_gid ();
46171
47172 if (pid != 1 ) {
48- // Get parent pid
173+ // Get parent pid (needed for cgroupv1 PID filter and exclude_pid check below)
49174 struct task_struct * task ;
50175 struct task_struct * real_parent ;
51176 task = (struct task_struct * )bpf_get_current_task ();
52177 bpf_probe_read (& real_parent , sizeof (real_parent ), & task -> real_parent );
53178 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 ;
58- }
59179 }
60180
181+ if (do_filter_by_process (pid , ppid )) return 0 ;
182+
61183 if (ppid == exclude_pid || tid == exclude_pid ) {
62184 return 0 ;
63185 }
64186
65- // Exclude this part of code if the following variables are not defined.
66- // It allows the go program to compile without error.
67- #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 );
70- char * path = (char * )PT_REGS_PARM2_CORE (real_regs );
187+ // do_sys_openat2(int dfd, const char __user *filename, struct open_how *how)
188+ int dirfd = (int )PT_REGS_PARM1_CORE (ctx );
189+ char * path = (char * )PT_REGS_PARM2_CORE (ctx );
71190 char cmp_path_name [62 ];
72191 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- }
80-
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 ;
86- }
87- #endif
88192
89- if (probability != 100 ) {
90- if ( hits != 0 ) {
91- unsigned long long scaled_disruptedHits = disruptedHits * 100 ;
92- unsigned long long scaled_hits = hits ;
93-
94- if (( scaled_disruptedHits / scaled_hits ) > probability ) {
95- hits ++ ;
96- return 0 ;
97- }
193+ if (cmp_path_name [ 0 ] == '/' ) {
194+ // Absolute path: compare raw path argument against filter prefix directly.
195+ char cmp_expected_path [ 62 ] ;
196+ bpf_probe_read ( cmp_expected_path , sizeof ( cmp_expected_path ), ( const void * ) filter_path ) ;
197+ int filter_len = ( int )( sizeof ( filter_path ) / sizeof ( filter_path [ 0 ])) - 1 ;
198+ if (filter_len > 62 ) return 0 ;
199+ for ( int i = 0 ; i < filter_len ; ++ i ) {
200+ if ( cmp_expected_path [ i ] == NULL ) break ;
201+ if ( cmp_path_name [ i ] != cmp_expected_path [ i ]) return 0 ;
98202 }
99-
100- hits ++ ;
101- disruptedHits ++ ;
203+ } else {
204+ // Relative path: compare CWD inode against the filter. dirfd is passed so
205+ // that opens with a non-AT_FDCWD dirfd are skipped (CWD is irrelevant there).
206+ if (!check_relative_path (dirfd , path )) return 0 ;
102207 }
103208
209+ if (do_probability_check ()) return 0 ;
210+
104211 data .ppid = ppid ;
105212 data .pid = pid ;
106213 data .tid = tid ;
@@ -117,4 +224,3 @@ int injection_disk_failure(struct pt_regs *ctx)
117224
118225 return 0 ;
119226}
120-
0 commit comments