Skip to content

Commit 59a7ed6

Browse files
aymericDDclaude
andcommitted
fix(ebpf): correct process and path targeting in disk failure
The disk failure injector mis-targeted both processes and paths, causing it to either fail to disrupt intended opens or disrupt far more than the targeted path. Process targeting: PID-based filtering only matched the container root process and its direct children (ppid == target_pid), missing grandchildren deeper in the tree. Replace it with PID namespace inode filtering — every process in a container shares the same PID namespace inode, so matching on it catches the whole process tree regardless of depth. - BPF program: remove target_pid + parent traversal; add target_pid_ns_inum volatile read via BPF_CORE_READ chain task→nsproxy→pid_ns_for_children→ns.inum - eBPF binary: replace -process flag with -pid-ns-inum flag - Injector: resolve PID namespace inode via syscall.Stat at injection time; node-level passes inum=0 (match all) Path targeting: openat() accepts absolute and relative paths, but the program only matched the absolute filter_path prefix. Relative paths skipped the filter and were disrupted unconditionally, failing nearly every open of the targeted process. Resolve AT_FDCWD-relative paths to an absolute path by walking the task's pwd dentry chain across mount points (like d_path) into a per-CPU scratch buffer, then apply the same prefix filter as absolute paths. Verifier fixes: use the BPF_PROG macro for the fmod_ret program so the openat argument is read from BTF offset 0 instead of PT_REGS_PARM1 (off=112), which the verifier rejects for fmod_ret. Keep scratch buffers in a per-CPU array, read the filter into a runtime buffer, copy component names with bpf_probe_read_kernel_str at a masked offset, and cap cwd depth to keep verifier state from exploding (E2BIG). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent ce06beb commit 59a7ed6

4 files changed

Lines changed: 278 additions & 72 deletions

File tree

ebpf/disk-failure/injection.bpf.c

Lines changed: 205 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
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;
1010
const volatile pid_t exclude_pid;
1111
const volatile char filter_path[61];
1212
const volatile pid_t exit_code = ENOENT;
@@ -16,9 +16,8 @@ unsigned int hits = 0;
1616
unsigned int disruptedHits = 0;
1717

1818
struct 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

ebpf/disk-failure/main.go

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,13 @@ import (
1616
"os"
1717
"os/signal"
1818

19-
"github.com/DataDog/chaos-controller/ebpf"
2019
"github.com/DataDog/chaos-controller/log"
2120
bpf "github.com/aquasecurity/libbpfgo"
2221
"github.com/aquasecurity/libbpfgo/helpers"
2322
"go.uber.org/zap"
2423
)
2524

26-
var nPid = flag.Uint64("process", 0, "Process to disrupt")
25+
var nPidNsInum = flag.Uint64("pid-ns-inum", 0, "PID namespace inode to disrupt (0 = all namespaces)")
2726
var nPath = flag.String("path", "/", "Filter path")
2827
var nProbability = flag.Uint64("probability", 100, "Probability to disrupt")
2928
var nExitCode = flag.Uint64("exit-code", 1, "Exit code")
@@ -73,7 +72,7 @@ func main() {
7372
must(err)
7473

7574
// Attach the kprope to catch sys openat syscall
76-
_, err = prog.AttachKprobe(ebpf.SysOpenat)
75+
_, err = prog.AttachGeneric()
7776
must(err)
7877

7978
// Create the ring buffer to store events
@@ -96,23 +95,20 @@ func main() {
9695
}
9796

9897
func printEvent(data []byte) {
99-
ppid := int(binary.LittleEndian.Uint32(data[0:4]))
100-
pid := int(binary.LittleEndian.Uint32(data[4:8]))
101-
tid := int(binary.LittleEndian.Uint32(data[8:12]))
102-
gid := int(binary.LittleEndian.Uint32(data[12:16]))
103-
comm := string(bytes.TrimRight(data[16:], "\x00"))
104-
logger.Infof("Disrupt Ppid %d, Pid %d, Tid: %d, Gid: %d, Command: %s", ppid, pid, tid, gid, comm)
98+
pid := int(binary.LittleEndian.Uint32(data[0:4]))
99+
tid := int(binary.LittleEndian.Uint32(data[4:8]))
100+
gid := int(binary.LittleEndian.Uint32(data[8:12]))
101+
comm := string(bytes.TrimRight(data[12:], "\x00"))
102+
logger.Infof("Disrupt Pid %d, Tid: %d, Gid: %d, Command: %s", pid, tid, gid, comm)
105103
}
106104

107105
// The global variables are shared against the userspace application and the BPF application (loaded into the kernel).
108106
// This global variables allow the user application to parametrise the BPF application.
109107
func initGlobalVariables(bpfModule *bpf.Module) {
110108
flag.Parse()
111109

112-
// Set the PID
113-
var pid uint32
114-
pid = uint32(*nPid)
115-
if err := bpfModule.InitGlobalVariable("target_pid", pid); err != nil {
110+
pidNsInum := uint32(*nPidNsInum)
111+
if err := bpfModule.InitGlobalVariable("target_pid_ns_inum", pidNsInum); err != nil {
116112
must(err)
117113
}
118114

@@ -121,14 +117,12 @@ func initGlobalVariables(bpfModule *bpf.Module) {
121117
must(err)
122118
}
123119

124-
var exitCode uint32
125-
exitCode = uint32(*nExitCode)
120+
exitCode := uint32(*nExitCode)
126121
if err := bpfModule.InitGlobalVariable("exit_code", exitCode); err != nil {
127122
must(err)
128123
}
129124

130-
var probability uint32
131-
probability = uint32(*nProbability)
125+
probability := uint32(*nProbability)
132126
if err := bpfModule.InitGlobalVariable("probability", probability); err != nil {
133127
must(err)
134128
}

0 commit comments

Comments
 (0)