diff --git a/docs/disk_failure.md b/docs/disk_failure.md index 19c7c0829a..5737eb7c83 100644 --- a/docs/disk_failure.md +++ b/docs/disk_failure.md @@ -23,6 +23,10 @@ The idea, is to simulate this behavior by catching this signal before the kernel The `diskFailure` disruption runs an eBPF program used to intercept system calls and inject errors. It is used to prevent certain processes from accessing certain files. It defines a target process and a filter path, and if the process is trying to open a file that matches the filter path, an -ENOENT error will be injected, preventing the process from opening the file. +**Process targeting** is done by matching the target container's PID namespace inode (`/proc//ns/pid`). Every process in a container shares the same PID namespace inode, so the entire process tree is targeted regardless of depth — not just the root process and its direct children. At node level, the PID namespace filter is disabled and all processes on the node are targeted. + +**Path targeting** intercepts both absolute paths (e.g. `/data/config`) and relative paths resolved via `AT_FDCWD` (e.g. `open("config.yaml", ...)`). Relative paths are resolved to their absolute equivalent by walking the task's working directory dentries, so the same prefix filter applies to both. + The Linux kernel provides an eBPF framework that allows users to load and run custom programs within the kernel of the operating system. That means it can extend or even modify the way the kernel behaves. It is useful for observability, security, chaos, etc... With eBPF it is possible to catch openat syscall and override the result with a `-ENOENT` error code. @@ -97,7 +101,7 @@ spec: probability: 100% ``` -* **Pod**: Intercept all `openat` system calls of the main process of the containers and its children. Allow to filter by container name too: +* **Pod**: Intercept all `openat` system calls for the entire process tree of the targeted containers (matched via PID namespace inode — grandchildren are targeted too). Containers that share the host PID namespace are rejected at injection time. Filtering by container name is supported: > Disrupt all containers @@ -206,29 +210,21 @@ To know more about exit codes you can refer to this [page](https://linux.die.net * The source code of the eBPF disk failure program is [here](../ebpf/disk-failure) * Tested with Ubuntu 22.10, kernel 5.15, go 1.19 * To know how to create an eBPF disruption you can refer to the following [documentation](ebpf_disruption.md) -* :warning: It does not support linux kernel greater than 5.15.95 -* :warning: Be sure to have a kernel build with eBPF: +* :warning: Be sure to have a kernel built with eBPF and `fmod_ret` support: ```shell +# Core eBPF (required by all eBPF disruptions) CONFIG_BPF=y -CONFIG_HAVE_EBPF_JIT=y -CONFIG_ARCH_WANT_DEFAULT_BPF_JIT=y CONFIG_BPF_SYSCALL=y CONFIG_BPF_JIT=y -CONFIG_BPF_JIT_ALWAYS_ON=y -CONFIG_BPF_JIT_DEFAULT_ON=y -CONFIG_BPF_UNPRIV_DEFAULT_OFF=y -CONFIG_BPF_LSM=y -CONFIG_CGROUP_BPF=y -CONFIG_IPV6_SEG6_BPF=y -CONFIG_NETFILTER_XT_MATCH_BPF=m -CONFIG_BPFILTER=y -CONFIG_BPFILTER_UMH=m -CONFIG_NET_CLS_BPF=m -CONFIG_NET_ACT_BPF=m -CONFIG_BPF_STREAM_PARSER=y -CONFIG_LWTUNNEL_BPF=y -CONFIG_BPF_EVENTS=y -CONFIG_BPF_KPROBE_OVERRIDE=y -CONFIG_TEST_BPF=m +CONFIG_HAVE_EBPF_JIT=y +CONFIG_NET_CLS_ACT=y + +# Required specifically for disk failure (fmod_ret hook) +# The eBPF program uses fmod_ret to intercept openat — this requires +# error-injectable function support in the kernel. +# CONFIG_BPF_KPROBE_OVERRIDE is no longer needed. +CONFIG_FUNCTION_ERROR_INJECTION=y ``` + +> **Note:** `CONFIG_FUNCTION_ERROR_INJECTION` replaces the previously documented `CONFIG_BPF_KPROBE_OVERRIDE` requirement. The disk failure injector now uses `fmod_ret` (a BTF-based return-value override hook) instead of the older `kprobe + bpf_override_return` approach. Kernels that satisfy the network disruption eBPF requirements but lack `CONFIG_FUNCTION_ERROR_INJECTION` will pass the common eBPF check but be rejected specifically when a disk failure injection is attempted. diff --git a/ebpf/config_informer_mock.go b/ebpf/config_informer_mock.go index 86daccfbf0..ff7134baa2 100644 --- a/ebpf/config_informer_mock.go +++ b/ebpf/config_informer_mock.go @@ -214,6 +214,51 @@ func (_c *ConfigInformerMock_IsKernelConfigAvailable_Call) RunAndReturn(run func return _c } +// ValidateDiskFailureRequiredConfig provides a mock function with no fields +func (_m *ConfigInformerMock) ValidateDiskFailureRequiredConfig() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for ValidateDiskFailureRequiredConfig") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ConfigInformerMock_ValidateDiskFailureRequiredConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ValidateDiskFailureRequiredConfig' +type ConfigInformerMock_ValidateDiskFailureRequiredConfig_Call struct { + *mock.Call +} + +// ValidateDiskFailureRequiredConfig is a helper method to define mock.On call +func (_e *ConfigInformerMock_Expecter) ValidateDiskFailureRequiredConfig() *ConfigInformerMock_ValidateDiskFailureRequiredConfig_Call { + return &ConfigInformerMock_ValidateDiskFailureRequiredConfig_Call{Call: _e.mock.On("ValidateDiskFailureRequiredConfig")} +} + +func (_c *ConfigInformerMock_ValidateDiskFailureRequiredConfig_Call) Run(run func()) *ConfigInformerMock_ValidateDiskFailureRequiredConfig_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *ConfigInformerMock_ValidateDiskFailureRequiredConfig_Call) Return(_a0 error) *ConfigInformerMock_ValidateDiskFailureRequiredConfig_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *ConfigInformerMock_ValidateDiskFailureRequiredConfig_Call) RunAndReturn(run func() error) *ConfigInformerMock_ValidateDiskFailureRequiredConfig_Call { + _c.Call.Return(run) + return _c +} + // ValidateRequiredSystemConfig provides a mock function with no fields func (_m *ConfigInformerMock) ValidateRequiredSystemConfig() error { ret := _m.Called() diff --git a/ebpf/disk-failure/injection.bpf.c b/ebpf/disk-failure/injection.bpf.c index 3b41431f71..fba96389b1 100644 --- a/ebpf/disk-failure/injection.bpf.c +++ b/ebpf/disk-failure/injection.bpf.c @@ -6,7 +6,7 @@ // +build ignore #include "injection.bpf.h" -const volatile pid_t target_pid = 0; +const volatile unsigned int target_pid_ns_inum = 0; const volatile pid_t exclude_pid; const volatile char filter_path[61]; const volatile pid_t exit_code = ENOENT; @@ -16,9 +16,8 @@ unsigned int hits = 0; unsigned int disruptedHits = 0; struct data_t { - u32 ppid; u32 pid; - u32 tid; + u32 tid; u32 id; char comm[100]; }; @@ -30,59 +29,318 @@ struct { __type(value, u32); } events SEC(".maps"); -SEC("kprobe/sys_openat") -int injection_disk_failure(struct pt_regs *ctx) +// openat() dirfd value meaning "relative to the current working directory". +#define AT_FDCWD -100 + +#ifndef offsetof +#define offsetof(TYPE, MEMBER) __builtin_offsetof(TYPE, MEMBER) +#endif +// container_of recovers the enclosing struct mount from its embedded vfsmount. +#ifndef container_of +#define container_of(ptr, type, member) \ + ((type *)((void *)(ptr) - offsetof(type, member))) +#endif + +// Maximum number of path components walked when resolving the cwd, and the +// per-component name cap. filter_path is at most 60 chars. The depth is kept +// small on purpose: container working directories are shallow and a larger value +// makes the verifier explore too many states (the cost is super-linear in depth). +// CWD_CHAIN_SIZE must be a power of two >= CWD_MAX_DEPTH: the chain array is +// indexed with (idx & (CWD_CHAIN_SIZE-1)) to give the verifier a mask-based +// bounds proof. Keeping CWD_MAX_DEPTH=10 (loop bound) separate from +// CWD_CHAIN_SIZE=16 (array/mask bound) avoids both index corruption and the +// verifier E2BIG that results from increasing the loop iteration count. +#define CWD_MAX_DEPTH 10 +#define CWD_CHAIN_SIZE 16 +#define CWD_NAME_BUF 64 + +// Resolved-path buffer. PATH_MASK keeps the running write offset provably in +// bounds (offset & PATH_MASK) so the verifier does not have to track it +// precisely; PATH_BUF leaves room for one full CWD_NAME_BUF write at the highest +// masked offset. The offset wraps past PATH_MASK, but the filter is at most 60 +// chars so only the first bytes of the path are ever compared. +#define PATH_BUF 320 +#define PATH_MASK 255 + +// Scratch space for cwd resolution. Kept in a per-CPU array map instead of on +// the stack: the dentry chain and the path buffer exceed the 512 byte BPF stack +// limit. +#if defined(__TARGET_ARCH_arm64) || defined(__TARGET_ARCH_x86) +struct cwd_scratch { + struct dentry *chain[CWD_CHAIN_SIZE]; + char filter[62]; + char path[PATH_BUF]; +}; + +struct { + __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY); + __uint(max_entries, 1); + __type(key, u32); + __type(value, struct cwd_scratch); +} cwd_scratch_map SEC(".maps"); +#endif + +// abs_path_matches_filter applies the prefix filter to an absolute path. +#if defined(__TARGET_ARCH_arm64) || defined(__TARGET_ARCH_x86) +static __always_inline int abs_path_matches_filter(const char *p) +{ + for (int i = 0; i < 61; i++) { + char fc = filter_path[i]; + if (fc == '\0') + break; + if (p[i] != fc) + return 0; + } + return 1; +} + +// rel_path_matches_filter resolves a relative open against the process current +// working directory and applies the prefix filter to the resulting absolute +// path. The cwd is reconstructed by walking the dentry chain up to the global +// root, crossing mount points (jumping to the mountpoint dentry in the parent +// mount) like d_path does. The components are then streamed root->leaf, followed +// by "/" + relpath, and compared char by char against filter_path. Path +// components like "." and ".." are not normalised. +static __always_inline int rel_path_matches_filter(struct dentry *start_dentry, struct vfsmount *start_mnt, const char *relpath) +{ + u32 zero = 0; + struct cwd_scratch *scratch = bpf_map_lookup_elem(&cwd_scratch_map, &zero); + if (scratch == NULL) + return 0; + + // Copy the filter out of .rodata into a runtime buffer so the verifier treats + // its bytes as unknown scalars (see match_filter_char): this keeps the nested + // cwd loops prunable instead of forking a state per known filter byte. + bpf_probe_read_kernel(scratch->filter, sizeof(scratch->filter), (const void *)filter_path); + + int filter_len = 0; + for (int i = 0; i < 61; i++) { + if (scratch->filter[i] == '\0') + break; + filter_len = i + 1; + } + if (filter_len == 0) + return 1; // empty filter matches everything + + // Collect the path component dentries leaf->root, crossing mount boundaries. + struct dentry *dentry = start_dentry; + struct vfsmount *vfsmnt = start_mnt; + struct mount *mnt = container_of(vfsmnt, struct mount, mnt); + int n = 0; + int depth_truncated = 0; + // Track whether dentry walk reached the filesystem root. If the loop + // exhausts its iteration budget without reaching the root (e.g. more than + // six nested mount crossings), reached_root stays 0 and we treat it as a + // truncation: building a path from only the collected suffix would produce + // a root-relative prefix that may falsely match the filter. + int reached_root = 0; + + // A few extra iterations beyond CWD_MAX_DEPTH absorb mount crossings (which do + // not add a component). Kept tight: more iterations explode verifier state. + for (int i = 0; i < CWD_MAX_DEPTH + 6; i++) { + struct dentry *mnt_root = BPF_CORE_READ(vfsmnt, mnt_root); + struct dentry *parent = BPF_CORE_READ(dentry, d_parent); + + if (dentry == mnt_root) { + struct mount *mnt_parent = BPF_CORE_READ(mnt, mnt_parent); + if (mnt != mnt_parent) { + // Cross into the parent mount at this mount's mountpoint. + dentry = BPF_CORE_READ(mnt, mnt_mountpoint); + mnt = mnt_parent; + vfsmnt = &mnt->mnt; + continue; + } + reached_root = 1; + break; // reached the global root + } + if (dentry == parent) { + reached_root = 1; + break; // root without a mount crossing + } + + if (n >= CWD_MAX_DEPTH) { + // Path is deeper than CWD_MAX_DEPTH. The chain only holds the + // deepest components; building a path from it would produce a + // root-relative prefix that may match the filter even though + // the real resolved path does not. Fail closed. + depth_truncated = 1; + break; + } + scratch->chain[n] = dentry; + n++; + dentry = parent; + } + + if (depth_truncated || !reached_root) + return 0; // cannot safely resolve; do not disrupt + + // Build the absolute path "/" into scratch->path. Each component + // name is copied in a single bpf_probe_read_kernel_str call (not char by char): + // the running offset is only ever used masked (pos & PATH_MASK), so the verifier + // does not track it precisely and the loop does not explode into a state per + // possible string length (which processed >1M insns and was rejected E2BIG). + int pos = 0; + + // cwd components root->leaf (chain[0] is the leaf, chain[n-1] the topmost); + // each contributes "/". + for (int k = 0; k < CWD_MAX_DEPTH; k++) { + int idx = n - 1 - k; + if (idx < 0) + break; + // Stop once we have more than 61 bytes — the prefix check only reads + // scratch->path[0..60] (filter_path is at most 60 chars). Continuing + // past this point risks pos wrapping past PATH_MASK(255) and overwriting + // the bytes the comparison reads, producing false matches or misses. + if (pos >= 62) + break; + idx &= (CWD_CHAIN_SIZE - 1); // mask to CWD_CHAIN_SIZE (power of 2) for verifier bounds proof + + scratch->path[pos & PATH_MASK] = '/'; + pos++; + + // Load the kernel dentry pointer from the map value with a plain access, + // then apply CO-RE only to the kernel struct (cwd_scratch is not in vmlinux + // BTF, so wrapping the whole chain in BPF_CORE_READ breaks relocation). + struct dentry *comp = scratch->chain[idx]; + const char *dname = (const char *)BPF_CORE_READ(comp, d_name.name); + int len = bpf_probe_read_kernel_str(&scratch->path[pos & PATH_MASK], CWD_NAME_BUF, dname); + if (len > 1) + pos += len - 1; // advance over the name, dropping the trailing NUL + } + + // Append "/" + relpath — only if we still need more bytes for the comparison. + if (pos < 62) { + scratch->path[pos & PATH_MASK] = '/'; + pos++; + int rlen = bpf_probe_read_kernel_str(&scratch->path[pos & PATH_MASK], 62, relpath); + if (rlen > 1) + pos += rlen - 1; + } + + // Prefix match: the path matches when the whole filter is a prefix of it. + for (int i = 0; i < 61; i++) { + char fc = scratch->filter[i]; + if (fc == '\0') + return 1; // entire filter matched: disrupt + if (i >= pos) + return 0; // path shorter than the filter + if (scratch->path[i] != fc) + return 0; + } + return 1; +} +#endif + +#if defined(__TARGET_ARCH_arm64) +SEC("fmod_ret/__arm64_sys_openat") +#else +SEC("fmod_ret/__x64_sys_openat") +#endif +// fmod_ret programs receive the accumulated return value from earlier programs in +// the chain via `ret`. Pass-through branches must return `ret` (not 0) so a prior +// program's -exit_code is not cleared when multiple disk-failure programs run +// concurrently (e.g. one per spec.Paths entry or per target container). +int BPF_PROG(injection_disk_failure, struct pt_regs *real_regs, long ret) { struct data_t data = {}; // Get data of the current process - u32 ppid = 0; u32 pid = bpf_get_current_pid_tgid(); if (pid == exclude_pid) { - return 0; + return ret; } u32 tid = bpf_get_current_pid_tgid() >> 32; u32 gid = bpf_get_current_uid_gid(); - if (pid != 1) { - // Get parent pid - struct task_struct *task; - struct task_struct *real_parent; - task = (struct task_struct *)bpf_get_current_task(); - bpf_probe_read(&real_parent, sizeof(real_parent), &task->real_parent); - bpf_probe_read(&ppid, sizeof(ppid), &real_parent->tgid); - - // Allow only children and parent process. - if (target_pid != 0 && ppid != target_pid && pid != target_pid) { - return 0; - } + if (target_pid_ns_inum != 0) { + struct task_struct *task = (struct task_struct *)bpf_get_current_task(); + // Use the task's active PID namespace (thread_pid->numbers[level].ns) + // rather than nsproxy->pid_ns_for_children. pid_ns_for_children is + // updated by unshare(CLONE_NEWPID)/setns before the process forks, so + // a host process preparing to launch a container child would be matched, + // while a container process that called unshare would be missed. + // thread_pid->numbers[level].ns is the namespace the task's PID is + // actually registered in — the correct active namespace. + struct pid *thread_pid = BPF_CORE_READ(task, thread_pid); + unsigned int level = BPF_CORE_READ(thread_pid, level); + struct pid_namespace *active_ns = NULL; + bpf_probe_read_kernel(&active_ns, sizeof(active_ns), + &thread_pid->numbers[level].ns); + unsigned int ns_inum = 0; + if (active_ns) + ns_inum = BPF_CORE_READ(active_ns, ns.inum); + if (ns_inum != target_pid_ns_inum) + return ret; } - if (ppid == exclude_pid || tid == exclude_pid) { - return 0; + if (tid == exclude_pid) { + return ret; } // Exclude this part of code if the following variables are not defined. // It allows the go program to compile without error. #if defined(__TARGET_ARCH_arm64) || defined(__TARGET_ARCH_x86) - // Allow only file with the desired prefix. - struct pt_regs *real_regs = (struct pt_regs *)PT_REGS_PARM1(ctx); + int dirfd = (int)PT_REGS_PARM1_CORE(real_regs); char *path = (char *)PT_REGS_PARM2_CORE(real_regs); char cmp_path_name[62]; bpf_probe_read(&cmp_path_name, sizeof(cmp_path_name), path); - char cmp_expected_path[62]; - bpf_probe_read(cmp_expected_path, sizeof(cmp_expected_path), (const void *)filter_path); - int filter_len = (int) (sizeof(filter_path) / sizeof(filter_path[0])) - 1; - - if (filter_len > 62) { - return 0; - } - for (int i = 0; i < filter_len; ++i) { - if (cmp_expected_path[i] == NULL) - break; - if (cmp_path_name[i] != cmp_expected_path[i]) - return 0; + if (cmp_path_name[0] == '/') { + // Absolute path: apply the prefix filter directly. + if (!abs_path_matches_filter(cmp_path_name)) + return ret; + } else { + // When the filter is "/" it matches everything — any relative open + // (regardless of dirfd) resolves under root. Check this first so that + // dirfd-relative opens are not passed through before reaching the root + // shortcut. For non-root filters, only AT_FDCWD-relative opens can be + // resolved to an absolute path in BPF. + int filter_is_root = (filter_path[0] == '/' && filter_path[1] == '\0'); + + if (!filter_is_root) { + if (dirfd != AT_FDCWD) + return ret; + + // If the path was truncated at 62 bytes (no NUL in the buffer), a + // ".." component may appear past the cutoff. Reject conservatively: + // we cannot guarantee the visible prefix is free of ".." escapes. + int relpath_truncated = 1; + for (int ti = 0; ti < 62; ti++) { + if (cmp_path_name[ti] == '\0') { + relpath_truncated = 0; + break; + } + } + if (relpath_truncated) + return ret; + + // Reject relative paths that contain ".." components. BPF cannot + // normalize them: openat(AT_FDCWD, "../outside") from a cwd under + // the filter prefix would produce "/filtered/../outside", which + // passes the prefix check but resolves outside the filter. Pass + // through conservatively rather than disrupting the wrong path. + if (cmp_path_name[0] == '.' && cmp_path_name[1] == '.' && + (cmp_path_name[2] == '\0' || cmp_path_name[2] == '/')) + return ret; + for (int di = 1; di < 60; di++) { + if (cmp_path_name[di] == '\0') + break; + if (cmp_path_name[di - 1] == '/' && cmp_path_name[di] == '.' && + cmp_path_name[di + 1] == '.' && + (cmp_path_name[di + 2] == '\0' || cmp_path_name[di + 2] == '/')) + return ret; + } + + struct task_struct *cwd_task = (struct task_struct *)bpf_get_current_task(); + struct dentry *cwd_dentry = BPF_CORE_READ(cwd_task, fs, pwd.dentry); + struct vfsmount *cwd_mnt = BPF_CORE_READ(cwd_task, fs, pwd.mnt); + if (cwd_dentry == NULL || cwd_mnt == NULL) + return ret; + + if (!rel_path_matches_filter(cwd_dentry, cwd_mnt, cmp_path_name)) + return ret; + } + // filter_is_root: fall through to disrupt } #endif @@ -93,7 +351,7 @@ int injection_disk_failure(struct pt_regs *ctx) if ((scaled_disruptedHits / scaled_hits) > probability) { hits++; - return 0; + return ret; } } @@ -101,7 +359,6 @@ int injection_disk_failure(struct pt_regs *ctx) disruptedHits++; } - data.ppid = ppid; data.pid = pid; data.tid = tid; data.id = gid; @@ -112,9 +369,6 @@ int injection_disk_failure(struct pt_regs *ctx) // Add the event to the ring buffer bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &data, 100); - // Override return of process with an -ENOENT error. - bpf_override_return(ctx, -exit_code); - - return 0; + return -(int)exit_code; } diff --git a/ebpf/disk-failure/main.go b/ebpf/disk-failure/main.go index 40defe5681..d19a07a001 100644 --- a/ebpf/disk-failure/main.go +++ b/ebpf/disk-failure/main.go @@ -16,14 +16,13 @@ import ( "os" "os/signal" - "github.com/DataDog/chaos-controller/ebpf" "github.com/DataDog/chaos-controller/log" bpf "github.com/aquasecurity/libbpfgo" "github.com/aquasecurity/libbpfgo/helpers" "go.uber.org/zap" ) -var nPid = flag.Uint64("process", 0, "Process to disrupt") +var nPidNsInum = flag.Uint64("pid-ns-inum", 0, "PID namespace inode to disrupt (0 = all namespaces)") var nPath = flag.String("path", "/", "Filter path") var nProbability = flag.Uint64("probability", 100, "Probability to disrupt") var nExitCode = flag.Uint64("exit-code", 1, "Exit code") @@ -72,8 +71,15 @@ func main() { prog, err := bpfModule.GetProgram("injection_disk_failure") must(err) - // Attach the kprope to catch sys openat syscall - _, err = prog.AttachKprobe(ebpf.SysOpenat) + // AttachGeneric attaches the fmod_ret program declared in the ELF section + // (fmod_ret/__x64_sys_openat or fmod_ret/__arm64_sys_openat). fmod_ret + // requires BPF trampoline support (Linux 5.7+). This intentionally + // replaces the previous kprobe + bpf_override_return approach: fmod_ret + // lets the program return a value directly without needing + // bpf_override_return, which is unavailable on kernels built without + // CONFIG_BPF_KPROBE_OVERRIDE. Nodes running kernels older than 5.7 are + // not supported by this injector and will fail at BPFLoadObject() above. + _, err = prog.AttachGeneric() must(err) // Create the ring buffer to store events @@ -96,12 +102,11 @@ func main() { } func printEvent(data []byte) { - ppid := int(binary.LittleEndian.Uint32(data[0:4])) - pid := int(binary.LittleEndian.Uint32(data[4:8])) - tid := int(binary.LittleEndian.Uint32(data[8:12])) - gid := int(binary.LittleEndian.Uint32(data[12:16])) - comm := string(bytes.TrimRight(data[16:], "\x00")) - logger.Infof("Disrupt Ppid %d, Pid %d, Tid: %d, Gid: %d, Command: %s", ppid, pid, tid, gid, comm) + pid := int(binary.LittleEndian.Uint32(data[0:4])) + tid := int(binary.LittleEndian.Uint32(data[4:8])) + gid := int(binary.LittleEndian.Uint32(data[8:12])) + comm := string(bytes.TrimRight(data[12:], "\x00")) + logger.Infof("Disrupt Pid %d, Tid: %d, Gid: %d, Command: %s", pid, tid, gid, comm) } // The global variables are shared against the userspace application and the BPF application (loaded into the kernel). @@ -109,10 +114,8 @@ func printEvent(data []byte) { func initGlobalVariables(bpfModule *bpf.Module) { flag.Parse() - // Set the PID - var pid uint32 - pid = uint32(*nPid) - if err := bpfModule.InitGlobalVariable("target_pid", pid); err != nil { + pidNsInum := uint32(*nPidNsInum) + if err := bpfModule.InitGlobalVariable("target_pid_ns_inum", pidNsInum); err != nil { must(err) } @@ -121,14 +124,12 @@ func initGlobalVariables(bpfModule *bpf.Module) { must(err) } - var exitCode uint32 - exitCode = uint32(*nExitCode) + exitCode := uint32(*nExitCode) if err := bpfModule.InitGlobalVariable("exit_code", exitCode); err != nil { must(err) } - var probability uint32 - probability = uint32(*nProbability) + probability := uint32(*nProbability) if err := bpfModule.InitGlobalVariable("probability", probability); err != nil { must(err) } diff --git a/ebpf/validator.go b/ebpf/validator.go index a5afe03ce4..9486c10f87 100644 --- a/ebpf/validator.go +++ b/ebpf/validator.go @@ -33,6 +33,10 @@ type ConfigInformer interface { // a 'KernelParams' struct. GetRequiredSystemConfig() KernelParams + // ValidateDiskFailureRequiredConfig validates kernel parameters specific to the + // disk failure injector (fmod_ret hook, CONFIG_FUNCTION_ERROR_INJECTION). + ValidateDiskFailureRequiredConfig() error + // GetMapTypes retrieves information about available map types and returns them as a 'MapTypes' struct. GetMapTypes() MapTypes @@ -269,10 +273,6 @@ func (v configInformer) GetRequiredSystemConfig() KernelParams { Description: coreInfraDescription, Enabled: config.ConfigHaveEbpfJit.Enabled(), }, - "CONFIG_BPF_KPROBE_OVERRIDE": KernelOption{ - Description: coreInfraDescription, - Enabled: config.ConfigBpfKprobeOverride.Enabled(), - }, "CONFIG_NET_CLS_ACT": KernelOption{ Description: coreInfraDescription, Enabled: config.ConfigNetClsAct.Enabled(), @@ -280,6 +280,22 @@ func (v configInformer) GetRequiredSystemConfig() KernelParams { } } +// ValidateDiskFailureRequiredConfig validates kernel parameters specific to the disk failure +// injector. It must be called in addition to ValidateRequiredSystemConfig when injecting disk +// failures, because fmod_ret programs require CONFIG_FUNCTION_ERROR_INJECTION which is not +// needed by other eBPF program types (e.g. TC classifiers used by network disruptions). +func (v configInformer) ValidateDiskFailureRequiredConfig() error { + if !v.IsKernelConfigAvailable() { + return fmt.Errorf("kernel config file not found") + } + + if !v.features.ConfigFunctionErrorInjection.Enabled() { + return fmt.Errorf("CONFIG_FUNCTION_ERROR_INJECTION kernel parameter is required (needed for: fmod_ret programs used by disk failure injector)") + } + + return nil +} + // GetMapTypes retrieves information about available map types from the system configuration features. // // Returns: diff --git a/ebpf/validator_test.go b/ebpf/validator_test.go index c1960839ca..10b7f7cede 100644 --- a/ebpf/validator_test.go +++ b/ebpf/validator_test.go @@ -360,10 +360,6 @@ var _ = Describe("ConfigInformer", func() { Description: "Essential eBPF infrastructure", Enabled: true, }, - "CONFIG_BPF_KPROBE_OVERRIDE": ebpf.KernelOption{ - Description: "Essential eBPF infrastructure", - Enabled: true, - }, "CONFIG_NET_CLS_ACT": ebpf.KernelOption{ Description: "Essential eBPF infrastructure", Enabled: true, @@ -377,7 +373,7 @@ var _ = Describe("ConfigInformer", func() { "CONFIG_BPF_SYSCALL": "n", "CONFIG_HAVE_EBPF_JIT": "n", "CONFIG_BPF_JIT": "n", - "CONFIG_BPF_KPROBE_OVERRIDE": "n", + "CONFIG_FUNCTION_ERROR_INJECTION": "n", "CONFIG_NET_CLS_ACT": "n" } } @@ -399,10 +395,6 @@ var _ = Describe("ConfigInformer", func() { Description: "Essential eBPF infrastructure", Enabled: false, }, - "CONFIG_BPF_KPROBE_OVERRIDE": ebpf.KernelOption{ - Description: "Essential eBPF infrastructure", - Enabled: false, - }, "CONFIG_NET_CLS_ACT": ebpf.KernelOption{ Description: "Essential eBPF infrastructure", Enabled: false, @@ -578,7 +570,7 @@ var _ = Describe("ConfigInformer", func() { "CONFIG_BPF_SYSCALL": "y", "CONFIG_HAVE_EBPF_JIT": "y", "CONFIG_BPF_JIT": "y", - "CONFIG_BPF_KPROBE_OVERRIDE": "y", + "CONFIG_FUNCTION_ERROR_INJECTION": "y", "CONFIG_NET_CLS_ACT": "n" } } @@ -595,7 +587,7 @@ var _ = Describe("ConfigInformer", func() { "CONFIG_BPF_SYSCALL": "n", "CONFIG_HAVE_EBPF_JIT": "n", "CONFIG_BPF_JIT": "n", - "CONFIG_BPF_KPROBE_OVERRIDE": "n", + "CONFIG_FUNCTION_ERROR_INJECTION": "n", "CONFIG_NET_CLS_ACT": "n" } } @@ -605,7 +597,6 @@ var _ = Describe("ConfigInformer", func() { "CONFIG_BPF_SYSCALL", "CONFIG_HAVE_EBPF_JIT", "CONFIG_BPF_JIT", - "CONFIG_BPF_KPROBE_OVERRIDE", "CONFIG_NET_CLS_ACT", }, ), @@ -614,6 +605,75 @@ var _ = Describe("ConfigInformer", func() { }) }) + When("ValidateDiskFailureRequiredConfig method is called", func() { + JustBeforeEach(func() { + // Action + err = configInformer.ValidateDiskFailureRequiredConfig() + }) + + Describe("success cases", func() { + Context("with CONFIG_FUNCTION_ERROR_INJECTION enabled", func() { + BeforeEach(func() { + // Arrange + bpftoolExecutorMock.EXPECT().Run([]string{"-j", "feature", "probe"}).Return(0, validKernelConfig, nil) + }) + + It("should not return an error", func() { + // Assert + Expect(err).ShouldNot(HaveOccurred()) + }) + }) + }) + + Describe("error cases", func() { + Context("when the kernel configuration is not available", func() { + BeforeEach(func() { + // Arrange + unameFuncMock = func() (unix.Utsname, error) { + return unix.Utsname{}, fmt.Errorf("an error happened") + } + }) + + It("should return an error", func() { + // Assert + Expect(err).Should(HaveOccurred()) + Expect(err).To(MatchError("kernel config file not found")) + }) + }) + + Context("with CONFIG_FUNCTION_ERROR_INJECTION disabled", func() { + It("should return an error", func() { + // Arrange — fresh configInformer so the disabled config is not shadowed + // by the outer BeforeEach's .Maybe() expectation. + localBpftool := ebpf.NewExecutorMock(GinkgoT()) + localBpftool.EXPECT().Run([]string{"-j", "feature", "probe"}).Return(0, ` +{ + "system_config": { + "CONFIG_BPF": "y", + "CONFIG_BPF_SYSCALL": "y", + "CONFIG_HAVE_EBPF_JIT": "y", + "CONFIG_BPF_JIT": "y", + "CONFIG_FUNCTION_ERROR_INJECTION": "n", + "CONFIG_NET_CLS_ACT": "y" + } +} +`, nil).Once() + + localStatFS := mocks.NewStatFSMock(GinkgoT()) + localStatFS.EXPECT().Stat(mock.Anything).Return(nil, nil).Once() + + localConfigInformer, newErr := ebpf.NewConfigInformer(log, false, localBpftool, localStatFS, func() (unix.Utsname, error) { + return unix.Utsname{}, nil + }) + Expect(newErr).ShouldNot(HaveOccurred()) + + // Action && Assert + Expect(localConfigInformer.ValidateDiskFailureRequiredConfig()).To(MatchError(ContainSubstring("CONFIG_FUNCTION_ERROR_INJECTION kernel parameter is required"))) + }) + }) + }) + }) + When("IsKernelConfigAvailable method is called", func() { var ( unameAssertCalledCount int diff --git a/injector/disk_failure.go b/injector/disk_failure.go index c2b076594f..b8d92741cb 100644 --- a/injector/disk_failure.go +++ b/injector/disk_failure.go @@ -8,12 +8,15 @@ package injector import ( "context" "fmt" + "os" "strconv" "strings" + "syscall" "github.com/DataDog/chaos-controller/api/v1beta1" "github.com/DataDog/chaos-controller/command" "github.com/DataDog/chaos-controller/ebpf" + "github.com/DataDog/chaos-controller/env" //nolint:depguard "github.com/DataDog/chaos-controller/process" "github.com/DataDog/chaos-controller/types" ) @@ -29,6 +32,12 @@ type DiskFailureInjectorConfig struct { CmdFactory command.Factory ProcessManager process.Manager BPFConfigInformer ebpf.ConfigInformer + PidNsInumReader func(pid int) (uint64, error) + // PidNsSharedChecker reports whether the given PID namespace inode is shared + // by processes belonging to different containers (shareProcessNamespace: true). + // The BPF filter scopes by namespace inode, so a shared namespace would + // disrupt ALL containers in the pod, not just the targeted one. + PidNsSharedChecker func(targetPID int, nsInum uint64) (bool, error) } const EBPFDiskFailureCmd = "bpf-disk-failure" @@ -71,13 +80,75 @@ func (i *DiskFailureInjector) Inject() error { return fmt.Errorf("the disk failure needs a kernel supporting eBPF programs: %w", err) } + if err := i.config.BPFConfigInformer.ValidateDiskFailureRequiredConfig(); err != nil { + return fmt.Errorf("the disk failure injector requires fmod_ret kernel support: %w", err) + } + if !i.config.BPFConfigInformer.GetMapTypes().HavePerfEventArrayMapType { return fmt.Errorf("the disk failure needs the perf event array map type, but the current kernel does not support this type of map") } - pid := 0 + pidNsInum := uint64(0) + if i.config.Disruption.Level == types.DisruptionLevelPod { - pid = int(i.config.TargetContainer.PID()) + if i.config.PidNsInumReader == nil { + mountProc, ok := os.LookupEnv(env.InjectorMountProc) + if !ok { + return fmt.Errorf("environment variable %s doesn't exist", env.InjectorMountProc) + } + + i.config.PidNsInumReader = func(pid int) (uint64, error) { + var stat syscall.Stat_t + + path := fmt.Sprintf("%s%d/ns/pid", mountProc, pid) + if err := syscall.Stat(path, &stat); err != nil { + return 0, fmt.Errorf("cannot read PID namespace inode for pid %d from %s: %w", pid, path, err) + } + + return stat.Ino, nil + } + + // Set default shared-namespace detector alongside the inode reader. + // Both are derived from the same mountProc, so they are initialized together. + if i.config.PidNsSharedChecker == nil { + i.config.PidNsSharedChecker = makePidNsSharedChecker(mountProc, i.config.PidNsInumReader) + } + } + + pid := int(i.config.TargetContainer.PID()) + + var err error + + pidNsInum, err = i.config.PidNsInumReader(pid) + if err != nil { + return fmt.Errorf("unable to resolve PID namespace inode: %w", err) + } + + // Refuse pod-level disk failure when the container shares the host PID + // namespace (hostPID: true). In that case pidNsInum equals the host's PID + // namespace inode, so the eBPF filter would match every process on the node. + hostInum, err := i.config.PidNsInumReader(1) + if err != nil { + return fmt.Errorf("unable to resolve host PID namespace inode: %w", err) + } + + if pidNsInum == hostInum { + return fmt.Errorf("pod-level disk failure is not supported for containers sharing the host PID namespace (hostPID: true)") + } + + // Refuse when the namespace is shared between containers (shareProcessNamespace: true). + // The BPF filter matches by PID namespace inode, so it would disrupt all containers + // in the shared namespace, not just the targeted one. + if i.config.PidNsSharedChecker != nil { + shared, err := i.config.PidNsSharedChecker(pid, pidNsInum) + if err != nil { + return fmt.Errorf("unable to check for shared PID namespace: %w", err) + } + + if shared { + return fmt.Errorf("pod-level disk failure is not supported for containers using shareProcessNamespace") + } + } } exitCode := 0 @@ -87,7 +158,7 @@ func (i *DiskFailureInjector) Inject() error { } for _, path := range i.spec.Paths { - args := []string{"-process", strconv.Itoa(pid)} + args := []string{"-pid-ns-inum", strconv.FormatUint(pidNsInum, 10)} if path != "" { args = append(args, "-path", path) @@ -117,3 +188,112 @@ func (i *DiskFailureInjector) UpdateConfig(config Config) { func (i *DiskFailureInjector) Clean() error { return nil } + +// makePidNsSharedChecker returns a checker that detects shareProcessNamespace by +// scanning /proc for processes sharing the same PID namespace inode that belong +// to a different container (identified by a different container cgroup scope). +func makePidNsSharedChecker(mountProc string, inumReader func(int) (uint64, error)) func(int, uint64) (bool, error) { + return func(targetPID int, targetInum uint64) (bool, error) { + targetCgroup, err := readCgroupPath(mountProc, targetPID) + if err != nil { + return false, fmt.Errorf("cannot read cgroup for pid %d: %w", targetPID, err) + } + + entries, err := os.ReadDir(mountProc) + if err != nil { + return false, fmt.Errorf("cannot read %s: %w", mountProc, err) + } + + for _, e := range entries { + pid, err := strconv.Atoi(e.Name()) + if err != nil || pid == targetPID { + continue + } + + inum, err := inumReader(pid) + if err != nil || inum != targetInum { + continue + } + + cgroup, err := readCgroupPath(mountProc, pid) + if err != nil { + continue + } + + // Two processes are in the same container if any of their cgroup + // hierarchy paths is equal or one is a subdirectory of the other + // (delegated/nested cgroups). This handles both the systemd cgroup + // driver (.scope suffix) and the cgroupfs driver (/kubepods/.../ + // directories) without needing to parse driver-specific formats. + if !sameContainerCgroup(cgroup, targetCgroup) { + return true, nil + } + } + + return false, nil + } +} + +// sameContainerCgroup reports whether two /proc//cgroup file contents +// belong to the same container. Two processes are in the same container when +// any of their cgroup hierarchy paths is equal or one is a subdirectory of +// the other (delegated/nested cgroups inside the container). +// +// This handles both the systemd cgroup driver (paths ending with .scope) and +// the cgroupfs driver (/kubepods/.../ directories) without +// requiring driver-specific parsing. +func sameContainerCgroup(cgroupA, cgroupB string) bool { + pathsA := extractCgroupPaths(cgroupA) + pathsB := extractCgroupPaths(cgroupB) + + // If no non-root paths were found in either file (e.g. all controllers + // at "/" on an unusual cgroup v1 layout), we cannot distinguish containers + // via cgroup paths. Treat conservatively as the same container to avoid + // falsely detecting shareProcessNamespace and blocking valid injections. + if len(pathsA) == 0 || len(pathsB) == 0 { + return true + } + + for _, a := range pathsA { + for _, b := range pathsB { + if a == b || strings.HasPrefix(a, b+"/") || strings.HasPrefix(b, a+"/") { + return true + } + } + } + + return false +} + +func extractCgroupPaths(content string) []string { + var paths []string + + for _, line := range strings.Split(strings.TrimSpace(content), "\n") { + parts := strings.SplitN(line, ":", 3) + if len(parts) != 3 { + continue + } + + path := parts[2] + // Skip root-only paths ("/"). On cgroup v1 nodes, unused controllers + // (e.g. rdma) set every process's path to "/". Including these would + // cause sameContainerCgroup to match unrelated containers via the common + // "/" prefix before ever reaching container-specific hierarchies. + if path == "/" { + continue + } + + paths = append(paths, path) + } + + return paths +} + +func readCgroupPath(mountProc string, pid int) (string, error) { + data, err := os.ReadFile(fmt.Sprintf("%s%d/cgroup", mountProc, pid)) + if err != nil { + return "", err + } + + return string(data), nil +} diff --git a/injector/disk_failure_test.go b/injector/disk_failure_test.go index 52df8a794a..62d1068fcf 100644 --- a/injector/disk_failure_test.go +++ b/injector/disk_failure_test.go @@ -7,7 +7,6 @@ package injector_test import ( "fmt" - "os" "strconv" "github.com/DataDog/chaos-controller/api" @@ -27,7 +26,6 @@ var _ = Describe("Disk Failure", func() { config DiskFailureInjectorConfig err error level types.DisruptionLevel - proc *os.Process inj Injector spec v1beta1.DiskFailureSpec cmdFactoryMock *command.FactoryMock @@ -35,15 +33,18 @@ var _ = Describe("Disk Failure", func() { BPFConfigInformerMock *ebpf.ConfigInformerMock ) - const PID = 1 + const ( + PID = 42 + PidNsInum = uint64(12345) + HostPidNsInum = uint64(99999) + ) BeforeEach(func() { - proc = &os.Process{Pid: PID} - containerMock = container.NewContainerMock(GinkgoT()) BPFConfigInformerMock = ebpf.NewConfigInformerMock(GinkgoT()) BPFConfigInformerMock.EXPECT().ValidateRequiredSystemConfig().Return(nil).Maybe() + BPFConfigInformerMock.EXPECT().ValidateDiskFailureRequiredConfig().Return(nil).Maybe() BPFConfigInformerMock.EXPECT().GetMapTypes().Return(ebpf.MapTypes{HavePerfEventArrayMapType: true}).Maybe() cmd := command.NewCmdMock(GinkgoT()) @@ -58,6 +59,19 @@ var _ = Describe("Disk Failure", func() { config = DiskFailureInjectorConfig{ BPFConfigInformer: BPFConfigInformerMock, CmdFactory: cmdFactoryMock, + // Returns a distinct inode for the host (pid 1) vs the container, + // so tests can distinguish normal pod-level from host-namespace sharing. + PidNsInumReader: func(pid int) (uint64, error) { + if pid == 1 { + return HostPidNsInum, nil + } + + return PidNsInum, nil + }, + // Default: container is in a dedicated namespace (no sharing). + PidNsSharedChecker: func(_ int, _ uint64) (bool, error) { + return false, nil + }, Config: Config{ Log: log, MetricsSink: ms, @@ -97,10 +111,25 @@ var _ = Describe("Disk Failure", func() { }) }) + When("the ValidateDiskFailureRequiredConfig method of the eBPF config informer returns an error", func() { + BeforeEach(func() { + BPFConfigInformerMock = ebpf.NewConfigInformerMock(GinkgoT()) + BPFConfigInformerMock.EXPECT().ValidateRequiredSystemConfig().Return(nil).Once() + BPFConfigInformerMock.EXPECT().ValidateDiskFailureRequiredConfig().Return(fmt.Errorf("CONFIG_FUNCTION_ERROR_INJECTION kernel parameter is required")).Once() + config.BPFConfigInformer = BPFConfigInformerMock + }) + + It("should return an error", func() { + Expect(err).Should(HaveOccurred()) + Expect(err).To(MatchError("the disk failure injector requires fmod_ret kernel support: CONFIG_FUNCTION_ERROR_INJECTION kernel parameter is required")) + }) + }) + When("the bpf map type perf event array is not supported", func() { BeforeEach(func() { BPFConfigInformerMock = ebpf.NewConfigInformerMock(GinkgoT()) BPFConfigInformerMock.EXPECT().ValidateRequiredSystemConfig().Return(nil).Once() + BPFConfigInformerMock.EXPECT().ValidateDiskFailureRequiredConfig().Return(nil).Once() BPFConfigInformerMock.EXPECT().GetMapTypes().Return(ebpf.MapTypes{ HaveHashMapType: true, HaveArrayMapType: true, @@ -134,6 +163,55 @@ var _ = Describe("Disk Failure", func() { Expect(err).To(MatchError("the disk failure needs the perf event array map type, but the current kernel does not support this type of map")) }) }) + + When("the PID namespace inode reader returns an error", func() { + BeforeEach(func() { + config.Disruption.Level = types.DisruptionLevelPod + containerMock.EXPECT().PID().Return(PID).Once() + config.PidNsInumReader = func(pid int) (uint64, error) { + return 0, fmt.Errorf("stat failed") + } + }) + + It("should return an error", func() { + Expect(err).Should(HaveOccurred()) + Expect(err).To(MatchError(ContainSubstring("unable to resolve PID namespace inode"))) + }) + }) + + When("the container shares the host PID namespace", func() { + BeforeEach(func() { + config.Disruption.Level = types.DisruptionLevelPod + containerMock.EXPECT().PID().Return(PID).Once() + // Both container and host resolve to the same inode, simulating hostPID: true. + config.PidNsInumReader = func(pid int) (uint64, error) { + return HostPidNsInum, nil + } + }) + + It("should return an error", func() { + Expect(err).Should(HaveOccurred()) + Expect(err).To(MatchError(ContainSubstring("pod-level disk failure is not supported for containers sharing the host PID namespace"))) + }) + }) + + When("the pod uses shareProcessNamespace", func() { + BeforeEach(func() { + config.Disruption.Level = types.DisruptionLevelPod + containerMock.EXPECT().PID().Return(PID).Once() + // Simulate shareProcessNamespace: true — namespace differs from the host + // but is shared by multiple containers. The BPF filter would match all + // processes in the shared namespace, not just the targeted container. + config.PidNsSharedChecker = func(_ int, _ uint64) (bool, error) { + return true, nil + } + }) + + It("should return an error", func() { + Expect(err).Should(HaveOccurred()) + Expect(err).To(MatchError(ContainSubstring("pod-level disk failure is not supported for containers using shareProcessNamespace"))) + }) + }) }) Describe("success cases", func() { @@ -148,7 +226,7 @@ var _ = Describe("Disk Failure", func() { Expect(err).ShouldNot(HaveOccurred()) cmdFactoryMock.AssertCalled(GinkgoT(), "NewCmd", mock.Anything, EBPFDiskFailureCmd, []string{ - "-process", strconv.Itoa(proc.Pid), + "-pid-ns-inum", strconv.FormatUint(PidNsInum, 10), "-path", "/", "-probability", "100", }) @@ -163,12 +241,12 @@ var _ = Describe("Disk Failure", func() { Expect(err).ShouldNot(HaveOccurred()) cmdFactoryMock.AssertCalled(GinkgoT(), "NewCmd", mock.Anything, EBPFDiskFailureCmd, []string{ - "-process", strconv.Itoa(proc.Pid), + "-pid-ns-inum", strconv.FormatUint(PidNsInum, 10), "-path", "/test", "-probability", "100", }) cmdFactoryMock.AssertCalled(GinkgoT(), "NewCmd", mock.Anything, EBPFDiskFailureCmd, []string{ - "-process", strconv.Itoa(proc.Pid), + "-pid-ns-inum", strconv.FormatUint(PidNsInum, 10), "-path", "/toto", "-probability", "100", }) @@ -184,7 +262,7 @@ var _ = Describe("Disk Failure", func() { Expect(err).ShouldNot(HaveOccurred()) cmdFactoryMock.AssertCalled(GinkgoT(), "NewCmd", mock.Anything, EBPFDiskFailureCmd, []string{ - "-process", strconv.Itoa(proc.Pid), + "-pid-ns-inum", strconv.FormatUint(PidNsInum, 10), "-path", "/", "-exit-code", "13", "-probability", "100", @@ -201,7 +279,7 @@ var _ = Describe("Disk Failure", func() { Expect(err).ShouldNot(HaveOccurred()) cmdFactoryMock.AssertCalled(GinkgoT(), "NewCmd", mock.Anything, EBPFDiskFailureCmd, []string{ - "-process", strconv.Itoa(proc.Pid), + "-pid-ns-inum", strconv.FormatUint(PidNsInum, 10), "-path", "/", "-probability", "100", }) @@ -217,7 +295,7 @@ var _ = Describe("Disk Failure", func() { Expect(err).ShouldNot(HaveOccurred()) cmdFactoryMock.AssertCalled(GinkgoT(), "NewCmd", mock.Anything, EBPFDiskFailureCmd, []string{ - "-process", strconv.Itoa(proc.Pid), + "-pid-ns-inum", strconv.FormatUint(PidNsInum, 10), "-path", "/", "-probability", "50", }) @@ -235,7 +313,7 @@ var _ = Describe("Disk Failure", func() { containerMock.AssertNumberOfCalls(GinkgoT(), "PID", 0) cmdFactoryMock.AssertCalled(GinkgoT(), "NewCmd", mock.Anything, EBPFDiskFailureCmd, []string{ - "-process", strconv.Itoa(0), + "-pid-ns-inum", "0", "-path", "/", "-probability", "100", }) @@ -250,7 +328,7 @@ var _ = Describe("Disk Failure", func() { Expect(err).ShouldNot(HaveOccurred()) cmdFactoryMock.AssertCalled(GinkgoT(), "NewCmd", mock.Anything, EBPFDiskFailureCmd, []string{ - "-process", strconv.Itoa(0), + "-pid-ns-inum", "0", "-path", "/", "-exit-code", "17", "-probability", "100", @@ -267,7 +345,7 @@ var _ = Describe("Disk Failure", func() { Expect(err).ShouldNot(HaveOccurred()) cmdFactoryMock.AssertCalled(GinkgoT(), "NewCmd", mock.Anything, EBPFDiskFailureCmd, []string{ - "-process", strconv.Itoa(0), + "-pid-ns-inum", "0", "-path", "/", "-probability", "100", }) @@ -284,11 +362,12 @@ var _ = Describe("Disk Failure", func() { // Verify that validation still occurs in dry run mode since bpftool probe is read-only BPFConfigInformerMock.AssertCalled(GinkgoT(), "ValidateRequiredSystemConfig") + BPFConfigInformerMock.AssertCalled(GinkgoT(), "ValidateDiskFailureRequiredConfig") BPFConfigInformerMock.AssertCalled(GinkgoT(), "GetMapTypes") // Verify that the command was still created cmdFactoryMock.AssertCalled(GinkgoT(), "NewCmd", mock.Anything, EBPFDiskFailureCmd, []string{ - "-process", strconv.Itoa(0), + "-pid-ns-inum", "0", "-path", "/", "-probability", "100", })