summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-30 21:47:29 +0300
committerPaul Buetow <paul@buetow.org>2026-05-30 21:47:29 +0300
commitb3e6798b340af13a1a2a4c670f5d55fae9f48a40 (patch)
treef945190920a06dd5f7104b5a9c6dfda328d3058a /internal
parentab2053c6c618ce01d7e18a5e3584cfafc6e58ab4 (diff)
test(kexec_file_load): lock in KindFd/Security/UNCLASSIFIED audit
Audit of kexec_file_load(2) against the man page confirmed the existing classification is already correct and consistent: KindFd capturing kernel_fd at args[0], FamilySecurity (matching its sibling kexec_load after task 6v), and an UNCLASSIFIED ret_event exit (returns 0/-1). The cmdline argument (args[3]) is a kernel command-line STRING, not a filesystem path, and is correctly never read as a path; the second fd initrd_fd (args[1]) is not captured, per the single-fd KindFd convention. Add a dedicated lock-in test plus real-kernel-format fixtures so future refactors cannot silently regress the fd wiring: assert ev->fd=args[0], no args[1] fd capture, no bpf_probe_read_user_str on the cmdline, and an UNCLASSIFIED (never READ/WRITE/TRANSFER) exit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat (limited to 'internal')
-rw-r--r--internal/generate/codegen_test.go46
-rw-r--r--internal/generate/testdata.go52
2 files changed, 98 insertions, 0 deletions
diff --git a/internal/generate/codegen_test.go b/internal/generate/codegen_test.go
index 3178b1c..da86b86 100644
--- a/internal/generate/codegen_test.go
+++ b/internal/generate/codegen_test.go
@@ -132,6 +132,52 @@ func TestGeneratePidfdGetfdHandlerUsesPidfdArgument(t *testing.T) {
requireContains(t, output, "ev->fd = (__s32)ctx->args[0];")
}
+// TestGenerateKexecFileLoadHandler locks in the generated BPF C for
+// kexec_file_load(2):
+//
+// long kexec_file_load(int kernel_fd, int initrd_fd,
+// unsigned long cmdline_len, const char *cmdline,
+// unsigned long flags)
+//
+// kexec_file_load loads a new kernel (and optional initrd) from open file
+// descriptors so it can later be booted by reboot(2); it returns 0 on success
+// or -1 on error. The leading kernel_fd (args[0]) makes the enter a KindFd
+// fd_event capturing ev->fd = args[0]. There are TWO fds (kernel_fd at args[0],
+// initrd_fd at args[1]); by the single-fd KindFd convention only the first
+// (kernel_fd) is captured, so the handler must NOT wire args[1]. Critically,
+// cmdline_ptr (args[3]) is a command-line STRING for the new kernel, NOT a
+// filesystem path, so it must NOT be read with bpf_probe_read_user_str. On exit
+// kexec_file_load returns 0/-1 — UNCLASSIFIED (a plain ret_event, no
+// read/write/transfer byte count). It shares FamilySecurity with its sibling
+// kexec_load(2) (asserted in family_test.go).
+func TestGenerateKexecFileLoadHandler(t *testing.T) {
+ output := generateFromPair(t, FormatKexecFileLoad, FormatExitKexecFileLoad)
+
+ // Enter: KindFd fd_event capturing kernel_fd from args[0].
+ requireContains(t, output, `SEC("tracepoint/syscalls/sys_enter_kexec_file_load")`)
+ requireContains(t, output, "struct fd_event *ev = bpf_ringbuf_reserve(&event_map, sizeof(struct fd_event), 0);")
+ requireContains(t, output, "ev->event_type = ENTER_FD_EVENT;")
+ requireContains(t, output, "ev->trace_id = SYS_ENTER_KEXEC_FILE_LOAD;")
+ requireContains(t, output, "ev->fd = (__s32)ctx->args[0];")
+
+ // Negative guards: only kernel_fd (args[0]) is captured — initrd_fd (args[1])
+ // must not be wired as a second fd, and cmdline_ptr (args[3]) is a kernel
+ // command-line string, never a filesystem path, so it must not be slurped via
+ // bpf_probe_read_user_str.
+ requireNotContains(t, output, "ev->fd = (__s32)ctx->args[1];")
+ requireNotContains(t, output, "bpf_probe_read_user_str")
+
+ // Exit: plain ret_event, UNCLASSIFIED (kexec_file_load returns 0/-1, no byte
+ // count).
+ requireContains(t, output, `SEC("tracepoint/syscalls/sys_exit_kexec_file_load")`)
+ requireContains(t, output, "struct ret_event *ev = bpf_ringbuf_reserve(&event_map, sizeof(struct ret_event), 0);")
+ requireContains(t, output, "ev->ret = ctx->ret;")
+ requireContains(t, output, "ev->ret_type = UNCLASSIFIED;")
+ requireNotContains(t, output, "ev->ret_type = READ_CLASSIFIED;")
+ requireNotContains(t, output, "ev->ret_type = WRITE_CLASSIFIED;")
+ requireNotContains(t, output, "ev->ret_type = TRANSFER_CLASSIFIED;")
+}
+
// TestGenerateProcessMadviseHandlerUsesFirstArgumentAsFd locks in the BPF
// handler wiring for process_madvise(2):
//
diff --git a/internal/generate/testdata.go b/internal/generate/testdata.go
index 7631d61..d9fb2c0 100644
--- a/internal/generate/testdata.go
+++ b/internal/generate/testdata.go
@@ -2296,3 +2296,55 @@ format:
print fmt: "0x%lx", REC->ret
`
+
+// FormatKexecFileLoad / FormatExitKexecFileLoad mirror the real kernel
+// tracepoint format for kexec_file_load(2):
+//
+// long kexec_file_load(int kernel_fd, int initrd_fd,
+// unsigned long cmdline_len, const char *cmdline,
+// unsigned long flags)
+//
+// kexec_file_load loads a new kernel (and optional initrd) from open file
+// descriptors so it can later be booted by reboot(2); it returns 0 on success
+// or -1 on error. The leading "kernel_fd" field (args[0]) makes the enter a
+// KindFd fd_event capturing ev->fd = args[0] — the FieldNumber("fd") lookup
+// finds no field literally named "fd" here, so generateExtraFd falls back to
+// args[0], which IS kernel_fd. There are TWO fds (kernel_fd at args[0],
+// initrd_fd at args[1]); by the single-fd KindFd convention only the first
+// (kernel_fd) is captured. Critically, cmdline_ptr (args[3]) is a command-line
+// STRING for the new kernel, NOT a filesystem path, so it must NOT be read with
+// bpf_probe_read_user_str. On exit kexec_file_load returns 0/-1, which is
+// UNCLASSIFIED (a plain ret_event, no read/write/transfer byte count). Field
+// names/offsets are copied verbatim from
+// /sys/kernel/tracing/events/syscalls/sys_enter_kexec_file_load.
+const FormatKexecFileLoad = `name: sys_enter_kexec_file_load
+ID: 508
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+ field:int kernel_fd; offset:16; size:8; signed:0;
+ field:int initrd_fd; offset:24; size:8; signed:0;
+ field:unsigned long cmdline_len; offset:32; size:8; signed:0;
+ field:const char * cmdline_ptr; offset:40; size:8; signed:0;
+ field:unsigned long flags; offset:48; size:8; signed:0;
+
+print fmt: "kernel_fd: 0x%08lx, initrd_fd: 0x%08lx, cmdline_len: 0x%08lx, cmdline_ptr: 0x%08lx, flags: 0x%08lx", ((unsigned long)(REC->kernel_fd)), ((unsigned long)(REC->initrd_fd)), ((unsigned long)(REC->cmdline_len)), ((unsigned long)(REC->cmdline_ptr)), ((unsigned long)(REC->flags))
+`
+
+const FormatExitKexecFileLoad = `name: sys_exit_kexec_file_load
+ID: 507
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+ field:long ret; offset:16; size:8; signed:1;
+
+print fmt: "0x%lx", REC->ret
+`