diff --git a/src/54-exec-image-inspector/README.md b/src/54-exec-image-inspector/README.md index aab86ca6..e4b4b23e 100644 --- a/src/54-exec-image-inspector/README.md +++ b/src/54-exec-image-inspector/README.md @@ -29,16 +29,16 @@ Linux 6.19 introduced **file dynptr**, which provides verifier-tracked access to Combining these features, the design becomes: 1. Attach an LSM hook to `bprm_committed_creds`, which fires after exec installs the new credentials -2. In the hook (non-sleepable), identify the target process and schedule a task work callback +2. In the hook (non-sleepable), create per-exec state and schedule a task work callback 3. The callback runs in a sleepable context, where it can access the installed executable, read content from any offset (including cold pages), and send results to user space -This separation (identify the target in a non-sleepable hook, do the heavy lifting in a sleepable callback) is the key insight. +This separation (schedule work in a non-sleepable hook, read the file in a sleepable callback) is the key insight. ## How BPF task work operates When you call `bpf_task_work_schedule_signal(task, work, map, callback)`, the kernel associates your callback with the specified task. The callback does not run immediately; it runs later, at a safe point before that task returns to user space. -The `struct bpf_task_work` is an opaque structure that the kernel uses to track the scheduled callback. Your BPF program allocates storage for it but does not interpret its contents. This tool uses a single-element ARRAY map to hold `struct exec_work`, which contains the `bpf_task_work` storage plus fields for timestamps and intermediate results. +The `struct bpf_task_work` is an opaque structure that the kernel uses to track the scheduled callback. Your BPF program allocates storage for it but does not interpret its contents. This tool uses a HASH map keyed by `pid_tgid`; each `struct exec_work` value contains the `bpf_task_work` storage plus fields for timestamps and intermediate results. Separate keys allow concurrent execs to remain independent. The callback signature is `int callback(struct bpf_map *map, void *key, void *value)`. The `value` parameter points to the map element containing your `bpf_task_work`, so you can pass data from the scheduling hook to the callback through surrounding fields. @@ -56,18 +56,11 @@ In a non-sleepable context, `bpf_dynptr_read` only succeeds if the target bytes ## Tool architecture -The user-space program forks a child process, but holds the child blocked on a pipe before it calls `execvp`. This gives the parent time to: +The user-space program loads and attaches the BPF program, creates a ring buffer reader, and prints `READY scope=system-wide`. It then remains active until SIGINT or SIGTERM while workloads run normally. -1. Note the child's PID (which becomes its TGID, thread group ID) -2. Open the BPF skeleton and write the target TGID into read-only data -3. Load and attach the BPF program -4. Create a ring buffer reader +For every successful exec after `READY`, the `lsm/bprm_committed_creds` hook inserts one `exec_work` value into the `pending` HASH map under the current `pid_tgid`, records a timestamp, and schedules a task work callback. Failed insertions or scheduling attempts are counted and clean up the map entry. -Only then does the parent release the child by writing to the pipe. This handshake eliminates a race condition: without it, a fast-exiting command might finish before the BPF program attaches. - -When the child calls `execvp`, the `lsm/bprm_committed_creds` hook fires. The BPF program compares the current TGID with the configured target; if they match, it records a timestamp and schedules a task work callback. - -The callback (`inspect_executable`) runs in the child's sleepable context. It: +The callback (`inspect_executable`) runs later in the execing task's sleepable context. It: 1. Calls `bpf_get_task_exe_file` to get the installed executable (returning a referenced `struct file` that must be released with `bpf_put_file`) 2. Resolves the path with `bpf_path_d_path` @@ -75,7 +68,7 @@ The callback (`inspect_executable`) runs in the child's sleepable context. It: 4. Parses ELF fields: magic number, class (32/64-bit), data (endianness), type (executable vs shared object), and machine (architecture) 5. Sends an event through the ring buffer -User space polls the ring buffer while also checking whether the child has exited. A single child might exec multiple times (for example, `/bin/sh -c 'exec /bin/true'` first execs the shell, then execs `/bin/true`), so after the child exits, the tool drains any remaining events from the ring buffer. +User space polls the ring buffer until a signal arrives. On shutdown it detaches the LSM program, waits until `completed >= scheduled`, drains remaining events, prints the counters, and then destroys the skeleton. ![Exec image inspector data flow](https://github.com/eunomia-bpf/bpf-developer-tutorial/raw/main/src/54-exec-image-inspector/exec-image-flow.png) @@ -94,7 +87,6 @@ The implementation spans four files: a shared header, a compatibility header for #define EXEC_COMM_LEN 16 #define EXEC_PATH_LEN 256 -#define EXEC_PROBE_LEN 8 struct exec_event { unsigned int pid; @@ -107,13 +99,9 @@ struct exec_event { unsigned short elf_machine; int header_error; int path_error; - int direct_probe_error; - int deferred_probe_error; unsigned long long latency_ns; - unsigned long long probe_offset; char comm[EXEC_COMM_LEN]; char path[EXEC_PATH_LEN]; - unsigned char probe_bytes[EXEC_PROBE_LEN]; }; struct inspector_stats { @@ -121,13 +109,11 @@ struct inspector_stats { unsigned long long scheduled; unsigned long long schedule_errors; unsigned long long callbacks; + unsigned long long completed; unsigned long long header_errors; unsigned long long path_errors; - unsigned long long direct_probes; - unsigned long long direct_probe_errors; - unsigned long long deferred_probes; - unsigned long long deferred_probe_errors; unsigned long long dropped; + unsigned long long cleanup_errors; }; #endif /* __EXEC_IMAGE_INSPECTOR_H */ @@ -196,9 +182,6 @@ char LICENSE[] SEC("license") = "GPL"; #define ELFDATA2LSB 1 #define ELFDATA2MSB 2 -const volatile __u32 target_tgid; -const volatile __u32 probe_offset; - struct inspector_stats stats; struct { @@ -208,14 +191,14 @@ struct { struct exec_work { __u64 scheduled_ns; - int direct_probe_error; struct bpf_task_work work; }; struct { - __uint(type, BPF_MAP_TYPE_ARRAY); - __uint(max_entries, 1); - __type(key, __u32); + __uint(type, BPF_MAP_TYPE_HASH); + __uint(map_flags, BPF_F_NO_PREALLOC); + __uint(max_entries, 4096); + __type(key, __u64); __type(value, struct exec_work); } pending SEC(".maps"); @@ -226,36 +209,6 @@ static __u16 read_elf_u16(const unsigned char *header, int offset, __u8 data) return header[offset] | ((__u16)header[offset + 1] << 8); } -static int probe_file_without_sleep(struct file *file) -{ - unsigned char sample[EXEC_PROBE_LEN]; - struct bpf_dynptr dynptr; - int err; - - if (!probe_offset) - return 0; - - __sync_fetch_and_add(&stats.direct_probes, 1); - if (!file) { - err = -ENOENT; - goto record; - } - - err = bpf_dynptr_from_file(file, 0, &dynptr); - if (err) { - bpf_dynptr_file_discard(&dynptr); - goto record; - } - - err = bpf_dynptr_read(sample, sizeof(sample), &dynptr, probe_offset, 0); - bpf_dynptr_file_discard(&dynptr); - -record: - if (err) - __sync_fetch_and_add(&stats.direct_probe_errors, 1); - return err; -} - static int inspect_executable(struct bpf_map *map, void *key, void *value) { unsigned char header[64] = {}; @@ -267,16 +220,11 @@ static int inspect_executable(struct bpf_map *map, void *key, void *value) __u64 pid_tgid; int err; - (void)map; - (void)key; __sync_fetch_and_add(&stats.callbacks, 1); - pid_tgid = bpf_get_current_pid_tgid(); event.pid = (__u32)pid_tgid; event.tgid = pid_tgid >> 32; event.latency_ns = bpf_ktime_get_ns() - work->scheduled_ns; - event.direct_probe_error = work->direct_probe_error; - event.probe_offset = probe_offset; bpf_get_current_comm(event.comm, sizeof(event.comm)); task = bpf_get_current_task_btf(); @@ -284,11 +232,6 @@ static int inspect_executable(struct bpf_map *map, void *key, void *value) if (!file) { event.header_error = -ENOENT; __sync_fetch_and_add(&stats.header_errors, 1); - if (probe_offset) { - event.deferred_probe_error = -ENOENT; - __sync_fetch_and_add(&stats.deferred_probes, 1); - __sync_fetch_and_add(&stats.deferred_probe_errors, 1); - } goto emit; } @@ -303,11 +246,6 @@ static int inspect_executable(struct bpf_map *map, void *key, void *value) bpf_dynptr_file_discard(&dynptr); event.header_error = err; __sync_fetch_and_add(&stats.header_errors, 1); - if (probe_offset) { - event.deferred_probe_error = err; - __sync_fetch_and_add(&stats.deferred_probes, 1); - __sync_fetch_and_add(&stats.deferred_probe_errors, 1); - } goto put_file; } @@ -316,15 +254,6 @@ static int inspect_executable(struct bpf_map *map, void *key, void *value) event.header_error = err; __sync_fetch_and_add(&stats.header_errors, 1); } - - if (probe_offset) { - __sync_fetch_and_add(&stats.deferred_probes, 1); - err = bpf_dynptr_read(event.probe_bytes, sizeof(event.probe_bytes), - &dynptr, probe_offset, 0); - event.deferred_probe_error = err; - if (err) - __sync_fetch_and_add(&stats.deferred_probe_errors, 1); - } bpf_dynptr_file_discard(&dynptr); if (!event.header_error && header[0] == 0x7f && header[1] == 'E' && @@ -341,6 +270,9 @@ put_file: emit: if (bpf_ringbuf_output(&events, &event, sizeof(event), 0)) __sync_fetch_and_add(&stats.dropped, 1); + if (bpf_map_delete_elem(map, key)) + __sync_fetch_and_add(&stats.cleanup_errors, 1); + __sync_fetch_and_add(&stats.completed, 1); return 0; } @@ -348,598 +280,99 @@ SEC("lsm/bprm_committed_creds") void BPF_PROG(schedule_exec_inspection, struct linux_binprm *bprm) { struct task_struct *task; + struct exec_work empty_work = {}; struct exec_work *work; __u64 pid_tgid; - __u32 key = 0, tgid; + __u64 key; int err; + (void)bprm; pid_tgid = bpf_get_current_pid_tgid(); - tgid = pid_tgid >> 32; - if (target_tgid && tgid != target_tgid) - return; - + key = pid_tgid; __sync_fetch_and_add(&stats.matched, 1); + err = bpf_map_update_elem(&pending, &key, &empty_work, BPF_NOEXIST); + if (err) { + __sync_fetch_and_add(&stats.schedule_errors, 1); + return; + } work = bpf_map_lookup_elem(&pending, &key); if (!work) { __sync_fetch_and_add(&stats.schedule_errors, 1); + if (bpf_map_delete_elem(&pending, &key)) + __sync_fetch_and_add(&stats.cleanup_errors, 1); return; } work->scheduled_ns = bpf_ktime_get_ns(); - work->direct_probe_error = probe_file_without_sleep(bprm->file); task = bpf_get_current_task_btf(); err = bpf_task_work_schedule_signal(task, &work->work, &pending, inspect_executable); if (err) { __sync_fetch_and_add(&stats.schedule_errors, 1); + if (bpf_map_delete_elem(&pending, &key)) + __sync_fetch_and_add(&stats.cleanup_errors, 1); return; } __sync_fetch_and_add(&stats.scheduled, 1); } ``` -The entry point is `schedule_exec_inspection`, declared with `SEC("lsm/bprm_committed_creds")`. This LSM hook fires after the new executable's credentials have been installed, so `bprm->file` points to the file being executed. The hook itself is non-sleepable, but it can identify the target and schedule the deferred work. - -The two `const volatile` variables (`target_tgid` and `probe_offset`) live in the `.rodata` section. User space writes values between `open()` and `load()`, and the verifier treats them as compile-time constants for optimization. +The entry point is `schedule_exec_inspection`, declared with `SEC("lsm/bprm_committed_creds")`. This LSM hook fires after the new executable's credentials have been installed. The hook itself is non-sleepable, so it creates the per-exec state and schedules the deferred work. -When the target matches, the program looks up `struct exec_work` from the single-element `pending` ARRAY map. This structure holds the `bpf_task_work` storage plus a timestamp and optional direct-probe result. A single slot suffices because this tool observes one child process per invocation. +For each exec, the program inserts a zeroed `struct exec_work` into the `pending` HASH map with `BPF_NOEXIST`, keyed by `pid_tgid`, then records the timestamp. The callback deletes that exact key. This supports concurrent execs without sharing one task-work slot. -The optional `--probe-offset` flag makes the tool attempt a direct read in the non-sleepable hook via `probe_file_without_sleep`. The test suite uses this to verify that reading a cold page fails with `-EFAULT` in the non-sleepable context but succeeds in the sleepable callback. - -After saving the timestamp and direct-probe result, the hook calls `bpf_task_work_schedule_signal`. The kernel holds the references needed to execute the callback later. +The hook calls `bpf_task_work_schedule_signal` after saving the timestamp. The kernel holds the references needed to execute the callback later. Every insert, lookup, or scheduling failure is counted, and every path that created pending state deletes it. The callback `inspect_executable` calculates latency for diagnostics, then acquires the executable file with `bpf_get_task_exe_file`. This returns a referenced `struct file` that must be released with `bpf_put_file`. The callback resolves the path, creates a file dynptr, reads the 64-byte ELF header, and parses it. The `read_elf_u16` helper handles endianness: ELF files declare their byte order in the header, and multi-byte fields must be read accordingly. -Every path that creates a dynptr, success or failure, must call `bpf_dynptr_file_discard`. Finally, `bpf_ringbuf_output` sends the event to user space. +Every path that creates a dynptr, success or failure, must call `bpf_dynptr_file_discard`. Finally, `bpf_ringbuf_output` sends the event to user space, the callback deletes its pending entry, and `completed` is incremented so shutdown can wait for finished work rather than merely scheduled work. ### User-space loader -`exec_image_inspector.c` coordinates the child process, loads BPF, receives events, and reports results: +The complete loader is linked at the beginning of this tutorial. Its main lifecycle is: ```c -// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "exec_image_inspector.h" -#include "exec_image_inspector.skel.h" - -struct environment { - unsigned long long probe_offset; - unsigned int timeout_ms; - bool verbose; - char **command; -}; - -struct child_process { - pid_t pid; - int release_fd; - bool released; - bool reaped; - int status; -}; - -struct event_context { - unsigned int seen; -}; - -static struct environment env = { - .timeout_ms = 5000, -}; - -static int libbpf_print_fn(enum libbpf_print_level level, const char *format, - va_list args) -{ - if (level == LIBBPF_DEBUG && !env.verbose) - return 0; - return vfprintf(stderr, format, args); -} - -static void usage(const char *program) -{ - fprintf(stderr, - "Usage: %s [--probe-offset BYTES] [--timeout-ms MS] [--verbose] " - "-- COMMAND [ARG...]\n\n" - "Inspect the executable image installed by one command.\n\n" - "Options:\n" - " -p, --probe-offset BYTES also compare direct/deferred file reads\n" - " -t, --timeout-ms MS bound the command, 100-60000 " - "(default: 5000)\n" - " -v, --verbose print libbpf diagnostics\n" - " -h, --help show this help\n", - program); -} - -static int parse_u64(const char *value, unsigned long long maximum, - unsigned long long *result) -{ - char *end = NULL; - unsigned long long parsed; - - errno = 0; - parsed = strtoull(value, &end, 10); - if (errno || end == value || *end || parsed > maximum) - return -EINVAL; - *result = parsed; - return 0; -} - -static int parse_probe_offset(const char *value) -{ - unsigned long long parsed; - - if (parse_u64(value, UINT_MAX - EXEC_PROBE_LEN, &parsed)) { - fprintf(stderr, "invalid probe offset: %s\n", value); - return -EINVAL; - } - env.probe_offset = parsed; - return 0; -} - -static int parse_timeout(const char *value) -{ - unsigned long long parsed; - - if (parse_u64(value, 60000, &parsed) || parsed < 100) { - fprintf(stderr, "invalid timeout in milliseconds: %s\n", value); - return -EINVAL; - } - env.timeout_ms = parsed; - return 0; -} - -static int parse_option(int option, const char *program) -{ - switch (option) { - case 'p': - return parse_probe_offset(optarg); - case 't': - return parse_timeout(optarg); - case 'v': - env.verbose = true; - return 0; - case 'h': - usage(program); - exit(0); - default: - return -EINVAL; - } -} - -static int parse_args(int argc, char **argv) -{ - static const struct option options[] = { - { "probe-offset", required_argument, NULL, 'p' }, - { "timeout-ms", required_argument, NULL, 't' }, - { "verbose", no_argument, NULL, 'v' }, - { "help", no_argument, NULL, 'h' }, - {}, - }; - int error, option; - - while ((option = getopt_long(argc, argv, "+p:t:vh", options, NULL)) != -1) { - error = parse_option(option, argv[0]); - if (error) - return error; - } - - if (optind == argc) { - fprintf(stderr, "COMMAND is required\n"); - return -EINVAL; - } - env.command = &argv[optind]; - return 0; -} - -static long long monotonic_milliseconds(void) -{ - struct timespec timestamp; - - if (clock_gettime(CLOCK_MONOTONIC, ×tamp)) - return -errno; - return timestamp.tv_sec * 1000LL + timestamp.tv_nsec / 1000000; -} - -static int start_blocked_child(struct child_process *child) -{ - int pipe_fds[2]; - pid_t pid; - - if (pipe(pipe_fds)) - return -errno; - - pid = fork(); - if (pid < 0) { - int error = -errno; - - close(pipe_fds[0]); - close(pipe_fds[1]); - return error; - } - - if (pid == 0) { - char release; - ssize_t count; - - close(pipe_fds[1]); - do { - count = read(pipe_fds[0], &release, sizeof(release)); - } while (count < 0 && errno == EINTR); - close(pipe_fds[0]); - if (count != sizeof(release)) - _exit(126); - - /* Intentional argv execution; no shell parses the supplied arguments. */ - execvp(env.command[0], env.command); /* Flawfinder: ignore */ - fprintf(stderr, "failed to execute %s: %s\n", env.command[0], - strerror(errno)); - _exit(127); - } - - close(pipe_fds[0]); - child->pid = pid; - child->release_fd = pipe_fds[1]; - return 0; -} - -static int release_child(struct child_process *child) -{ - char release = 1; - ssize_t count; - - do { - count = write(child->release_fd, &release, sizeof(release)); - } while (count < 0 && errno == EINTR); - close(child->release_fd); - child->release_fd = -1; - if (count != sizeof(release)) - return count < 0 ? -errno : -EIO; - child->released = true; - return 0; -} - -static int child_exit_code(int status) -{ - if (WIFEXITED(status)) - return WEXITSTATUS(status); - if (WIFSIGNALED(status)) - return 128 + WTERMSIG(status); - return 125; -} - -static int reap_child(struct child_process *child, int options) -{ - pid_t result; - - if (child->reaped) - return 1; - do { - result = waitpid(child->pid, &child->status, options); - } while (result < 0 && errno == EINTR); - if (result < 0) - return -errno; - if (result == 0) - return 0; - child->reaped = true; - return 1; -} - -static int drain_events(struct ring_buffer *ring_buffer) -{ - int error; - - for (;;) { - error = ring_buffer__poll(ring_buffer, 0); - if (error == -EINTR) - continue; - if (error < 0) { - fprintf(stderr, "ring-buffer drain failed: %s\n", - strerror(-error)); - return error; - } - if (!error) - return 0; - } -} - -static const char *elf_class_name(unsigned char value) -{ - switch (value) { - case 1: - return "ELF32"; - case 2: - return "ELF64"; - default: - return "UNKNOWN"; - } -} - -static const char *elf_data_name(unsigned char value) -{ - switch (value) { - case 1: - return "LSB"; - case 2: - return "MSB"; - default: - return "UNKNOWN"; - } -} - -static const char *elf_type_name(unsigned short value) -{ - switch (value) { - case 2: - return "ET_EXEC"; - case 3: - return "ET_DYN"; - default: - return "OTHER"; - } -} - -static const char *elf_machine_name(unsigned short value) -{ - switch (value) { - case 3: - return "EM_386"; - case 62: - return "EM_X86_64"; - case 183: - return "EM_AARCH64"; - default: - return "OTHER"; - } -} - -static int handle_event(void *context, void *data, size_t size) -{ - const struct exec_event *event = data; - struct event_context *events = context; - unsigned int index; - - if (size < sizeof(*event)) { - fprintf(stderr, "short ring-buffer event: %zu bytes\n", size); - return 0; - } - - events->seen++; - printf("EXEC pid=%u tgid=%u comm=%.*s path=%.*s is_elf=%u " - "class=%s endian=%s type=%s(%u) machine=%s(%u) " - "header_error=%d path_error=%d latency_us=%llu\n", - event->pid, event->tgid, EXEC_COMM_LEN, event->comm, - EXEC_PATH_LEN, event->path, event->is_elf, - elf_class_name(event->elf_class), elf_data_name(event->elf_data), - elf_type_name(event->elf_type), event->elf_type, - elf_machine_name(event->elf_machine), event->elf_machine, - event->header_error, event->path_error, - event->latency_ns / 1000); - - if (event->probe_offset) { - printf("PROBE offset=%llu direct_error=%d deferred_error=%d bytes=", - event->probe_offset, event->direct_probe_error, - event->deferred_probe_error); - for (index = 0; index < EXEC_PROBE_LEN; index++) - printf("%02x", event->probe_bytes[index]); - putchar('\n'); - } - fflush(stdout); - return 0; -} - -static void stop_child(struct child_process *child) -{ - if (child->reaped || child->pid <= 0) - return; - if (!child->released && child->release_fd >= 0) { - close(child->release_fd); - child->release_fd = -1; - } else { - kill(child->pid, SIGKILL); - } - (void)reap_child(child, 0); -} - -static int setup_inspector(const struct child_process *child, - struct event_context *events, - struct exec_image_inspector_bpf **skeleton, - struct ring_buffer **ring_buffer) -{ - struct exec_image_inspector_bpf *skel; - struct ring_buffer *ring; - int error; - - skel = exec_image_inspector_bpf__open(); - if (!skel) { - fprintf(stderr, "failed to open BPF skeleton\n"); - return -ENOMEM; - } - *skeleton = skel; - skel->rodata->target_tgid = child->pid; - skel->rodata->probe_offset = env.probe_offset; - - error = exec_image_inspector_bpf__load(skel); - if (error) { - fprintf(stderr, "failed to load BPF object: %s\n", strerror(-error)); - return error; - } - error = exec_image_inspector_bpf__attach(skel); - if (error) { - fprintf(stderr, "failed to attach bprm_committed_creds LSM hook: %s\n", - strerror(-error)); - return error; - } - - ring = ring_buffer__new(bpf_map__fd(skel->maps.events), handle_event, - events, NULL); - if (!ring) { - fprintf(stderr, "failed to create ring buffer: %s\n", strerror(errno)); - return errno ? -errno : -ENOMEM; - } - *ring_buffer = ring; - return 0; -} - -static int reap_timed_out_child(struct child_process *child) -{ - int error; - - if (child->reaped) - return 0; - - fprintf(stderr, "command exceeded timeout; sending SIGKILL\n"); - kill(child->pid, SIGKILL); - error = reap_child(child, 0); - if (error < 0) { - fprintf(stderr, "waitpid after timeout failed: %s\n", - strerror(-error)); - return error; - } - return 0; -} - -static int wait_for_command(struct ring_buffer *ring_buffer, - struct child_process *child, - const struct event_context *events) -{ - long long deadline, now; - int error; - - printf("READY target_tgid=%d probe_offset=%llu timeout_ms=%u command=%s\n", - child->pid, env.probe_offset, env.timeout_ms, env.command[0]); - fflush(stdout); - error = release_child(child); - if (error) { - fprintf(stderr, "failed to release command process: %s\n", - strerror(-error)); - return error; - } - - now = monotonic_milliseconds(); - if (now < 0) { - fprintf(stderr, "failed to read monotonic clock: %s\n", - strerror((int)-now)); - return (int)now; - } - deadline = now + env.timeout_ms; - - for (;;) { - error = ring_buffer__poll(ring_buffer, 50); - if (error == -EINTR) - continue; - if (error < 0) { - fprintf(stderr, "ring-buffer poll failed: %s\n", strerror(-error)); - return error; - } - - error = reap_child(child, WNOHANG); - if (error < 0) { - fprintf(stderr, "waitpid failed: %s\n", strerror(-error)); - return error; - } - if (child->reaped && events->seen) - break; - - now = monotonic_milliseconds(); - if (now < 0) - return (int)now; - if (now >= deadline) - break; - if (child->reaped && !events->seen) - continue; - } - - error = reap_timed_out_child(child); - if (error) - return error; - error = drain_events(ring_buffer); - if (error) - return error; - return child_exit_code(child->status); -} - -static int report_result(const struct exec_image_inspector_bpf *skel, - const struct event_context *events, int command_exit) -{ - struct inspector_stats final_stats = skel->bss->stats; - - printf("SUMMARY matched=%llu scheduled=%llu schedule_errors=%llu " - "callbacks=%llu header_errors=%llu path_errors=%llu " - "direct_probes=%llu direct_probe_errors=%llu " - "deferred_probes=%llu deferred_probe_errors=%llu dropped=%llu " - "events=%u command_exit=%d\n", - final_stats.matched, final_stats.scheduled, - final_stats.schedule_errors, final_stats.callbacks, - final_stats.header_errors, final_stats.path_errors, - final_stats.direct_probes, final_stats.direct_probe_errors, - final_stats.deferred_probes, final_stats.deferred_probe_errors, - final_stats.dropped, events->seen, command_exit); - - if (!events->seen) { - fprintf(stderr, "no executable image event was observed\n"); - return 1; - } - if (command_exit) { - fprintf(stderr, "command exited with status %d\n", command_exit); - return command_exit; - } - return 0; -} - int main(int argc, char **argv) { struct exec_image_inspector_bpf *skel = NULL; - struct child_process child = { .release_fd = -1 }; struct event_context events = {}; struct ring_buffer *ring_buffer = NULL; - int command_exit, error, result = 1; + int error, result = 1; error = parse_args(argc, argv); if (error) { - usage(argv[0]); + usage(stderr, argv[0]); return 2; } - - libbpf_set_print(libbpf_print_fn); - error = start_blocked_child(&child); + error = install_signal_handlers(); if (error) { - fprintf(stderr, "failed to create command process: %s\n", + fprintf(stderr, "failed to install signal handlers: %s\n", strerror(-error)); return 1; } - error = setup_inspector(&child, &events, &skel, &ring_buffer); + libbpf_set_print(libbpf_print_fn); + error = setup_inspector(&events, &skel, &ring_buffer); if (error) goto cleanup; - command_exit = wait_for_command(ring_buffer, &child, &events); - if (command_exit < 0) - goto cleanup; - result = report_result(skel, &events, command_exit); + error = monitor_execs(ring_buffer); + exec_image_inspector_bpf__detach(skel); + if (!error) + error = drain_pending_events(ring_buffer, skel); + report_result(skel, &events); + if (!error) + result = 0; cleanup: - stop_child(&child); ring_buffer__free(ring_buffer); exec_image_inspector_bpf__destroy(skel); return result; } ``` -The core technique is the blocked-child handshake. `start_blocked_child` forks before BPF setup, but the child blocks on a pipe read. The parent notes the child's PID, opens the skeleton, writes `target_tgid`, loads and attaches, creates the ring buffer, and only then calls `release_child` to write to the pipe and let the child proceed to `execvp`. - -`wait_for_command` is the main loop. It prints a `READY` line, releases the child, then alternates between polling the ring buffer and checking whether the child has exited. The loop ends when the child is reaped and at least one event has arrived, or when the timeout expires. After timeout, `reap_timed_out_child` sends SIGKILL and waits. +`setup_inspector` opens, loads, and attaches the skeleton before creating the ring buffer. `monitor_execs` prints `READY` and polls until SIGINT or SIGTERM. -A single exec might trigger multiple events if the command itself execs (for example, `/bin/sh -c 'exec /bin/true'`). After the child exits, `drain_events` does a zero-timeout poll to collect any remaining events. +On shutdown, `main` detaches first. `drain_pending_events` then waits in bounded 100 ms polls for `completed` to catch up with `scheduled`, drains the ring buffer, and reports all counters before resources are destroyed. `handle_event` formats the output, translating numeric ELF values to readable names while preserving the raw values for scripting. @@ -953,7 +386,7 @@ make -C src/54-exec-image-inspector clean make -C src/54-exec-image-inspector -j2 ``` -The test suite requires Linux 6.19 or newer with the BPF LSM active. Check that `bpf` appears in the LSM list: +Before running, check that `bpf` appears in the active LSM list: ```bash cat /sys/kernel/security/lsm @@ -961,52 +394,20 @@ cat /sys/kernel/security/lsm If `bpf` is missing, add it to the kernel command line: change `lsm=` to `lsm=,bpf` in your bootloader configuration. -Run the tests: +Start the monitor: ```bash -cd src/54-exec-image-inspector -sudo make test +sudo ./src/54-exec-image-inspector/exec_image_inspector ``` -The repository CI only compiles this lesson. Runtime behavior was functionally tested on x86_64 with kernel `7.0.0-rc2+`. Sample output: - -```text -TEST-MISSING matched=0 events=0 command_exit=127 -TEST-TIMEOUT matched=1 callbacks=1 events=1 command_exit=137 -TEST-REEXEC matched=2 callbacks=2 events=2 command_exit=0 final_path=/usr/bin/true -READY target_tgid=1265 probe_offset=4214784 timeout_ms=3000 command=/tmp/exec-image-inspector-sxm3lumw/exec_fixture_image -EXEC pid=1265 tgid=1265 comm=exec_fixture_im path=/tmp/exec-image-inspector-sxm3lumw/exec_fixture_image is_elf=1 class=ELF64 endian=LSB type=ET_DYN(3) machine=EM_X86_64(62) header_error=0 path_error=0 latency_us=37 -PROBE offset=4214784 direct_error=-14 deferred_error=0 bytes=454950524f424521 -exec fixture completed -SUMMARY matched=1 scheduled=1 schedule_errors=0 callbacks=1 header_errors=0 path_errors=0 direct_probes=1 direct_probe_errors=1 deferred_probes=1 deferred_probe_errors=0 dropped=0 events=1 command_exit=0 -PASS: missing-command, timeout cleanup, re-exec drain, ELF decode, and deferred file read succeeded -``` - -The first three test lines cover boundary conditions: - -- **TEST-MISSING**: A nonexistent command exits with status 127. The LSM hook never fires because exec fails before credentials are committed, so `matched=0` and `events=0`. -- **TEST-TIMEOUT**: The command runs too long, gets SIGKILL, and is reaped with status 137 (128 + 9). One exec event was observed. -- **TEST-REEXEC**: A shell command that uses `exec` internally produces two events, and the final path is `/usr/bin/true`. +Once the program prints `READY scope=system-wide`, it reports each successful exec with an `EXEC` line containing the process IDs, command name, resolved executable path, ELF metadata, and callback latency. Press Ctrl-C to stop; the final `SUMMARY` shows scheduling, callback, error, drop, and event counts. -The `EXEC` line shows the installed image and parsed ELF fields. The `PROBE` line demonstrates the sleepable-context difference: the test writes a marker (`EIPROBE!`) to a page, flushes and evicts it from the page cache, then execs. The direct read in the non-sleepable hook returns `-EFAULT` (`-14`) because the page is cold. The deferred read in the sleepable callback succeeds, returning the marker bytes as hex (`454950524f424521`). - -To inspect a simple command: +Use `--verbose` when libbpf diagnostics are needed: ```bash -sudo ./exec_image_inspector --timeout-ms 3000 -- /bin/true +sudo ./src/54-exec-image-inspector/exec_image_inspector --verbose ``` -Command-line format: - -```text -exec_image_inspector [--probe-offset BYTES] [--timeout-ms MS] [--verbose] -- COMMAND [ARG...] -``` - -- `--timeout-ms`: 100 to 60000 milliseconds (default 5000). The tool kills and reaps the command after this deadline. -- `--probe-offset`: Read 8 bytes at this offset both in the hook (direct) and in the callback (deferred), to verify the cold-page difference. -- `--verbose`: Print libbpf diagnostic messages. -- `--`: Separates inspector options from the command to run. - ### Requirements | Requirement | Details | @@ -1019,13 +420,13 @@ exec_image_inspector [--probe-offset BYTES] [--timeout-ms MS] [--verbose] -- COM ## Limitations and extensions -This tool observes one direct child per invocation, using a single map slot. For concurrent services, you would allocate per-task state, implement admission limits, and handle callback reclamation. Timeout cleanup sends SIGKILL to the child only; callers needing process-group management or external signal handling would add those features. +This tool observes successful execs system-wide after `READY`. The pending HASH map supports up to 4096 concurrent `pid_tgid` keys; insertion or scheduling pressure is visible in `schedule_errors`. Shutdown waits for callbacks for about one second before returning an error. ## Summary -This tutorial demonstrates how to combine BPF task work and file dynptr to inspect the executable image actually installed by exec. The key insight is separating two moments: identifying the target (in a non-sleepable LSM hook) and reading file content (in a sleepable task work callback). This lets eBPF programs read file data reliably, even when the target bytes are not in the page cache. +This tutorial demonstrates how to combine BPF task work and file dynptr to inspect the executable image actually installed by exec. The LSM hook schedules work for each exec, and the task work callback reads the file in a sleepable context. This lets eBPF programs read file data reliably, even when the target bytes are not in the page cache. -The blocked-child handshake eliminates attach races, bounded execution ensures cleanup, and the final drain captures all events. Together, these techniques produce a reproducible single-command tool while leaving room for extension. +The persistent monitor exposes a natural `READY` boundary for independent workloads. Detach-before-drain shutdown, per-exec pending state, and completed-work accounting keep concurrent callbacks safe while preserving the original task-work and file-dynptr lesson. > To learn more about eBPF, visit our tutorial repository at or . diff --git a/src/54-exec-image-inspector/README.zh.md b/src/54-exec-image-inspector/README.zh.md index f3cd433e..88ad7229 100644 --- a/src/54-exec-image-inspector/README.zh.md +++ b/src/54-exec-image-inspector/README.zh.md @@ -29,16 +29,16 @@ Linux 6.19 引入了 **file dynptr**,提供验证器跟踪的文件数据访 结合这两个特性,设计变成: 1. 在 `bprm_committed_creds` 挂载 LSM 钩子,它在 exec 安装新凭据后触发 -2. 在钩子中(不可睡眠)识别目标进程并安排 task work 回调 +2. 在钩子中(不可睡眠)创建每次 exec 独立的状态并安排 task work 回调 3. 回调在可睡眠上下文中执行,可以访问已安装的可执行文件、读取任意偏移的内容(包括冷页)、然后把结果发送给用户态 -这种分离(在不可睡眠的钩子中识别目标,在可睡眠的回调中完成重活)是关键思路。 +这种分离(在不可睡眠钩子中安排工作,在可睡眠回调中读取文件)是关键思路。 ## BPF task work 的工作机制 调用 `bpf_task_work_schedule_signal(task, work, map, callback)` 时,内核把你的回调关联到指定的任务。回调不会立即执行;它会稍后在该任务返回用户态之前的某个安全点执行。 -`struct bpf_task_work` 是一个不透明结构,内核用它来追踪已安排的回调。BPF 程序只需为它分配存储空间,不解释其内容。本工具使用单元素 ARRAY map 保存 `struct exec_work`,其中包含 `bpf_task_work` 存储以及时间戳和中间结果字段。 +`struct bpf_task_work` 是一个不透明结构,内核用它来追踪已安排的回调。BPF 程序只需为它分配存储空间,不解释其内容。本工具使用以 `pid_tgid` 为键的 HASH map;每个 `struct exec_work` 值包含 `bpf_task_work` 存储以及时间戳和中间结果字段。不同 key 让并发 exec 互不干扰。 回调签名是 `int callback(struct bpf_map *map, void *key, void *value)`。`value` 参数指向包含你的 `bpf_task_work` 的 map 元素,所以你可以通过周围的字段从调度钩子向回调传递数据。 @@ -56,18 +56,11 @@ dynptr 用边界信息包装指针,BPF 验证器可以追踪这些边界。对 ## 工具架构 -用户态程序 fork 出一个子进程,但在子进程调用 `execvp` 之前用管道阻塞它。这给父进程时间去: +用户态程序加载并挂载 BPF 程序,创建 ring buffer reader,然后打印 `READY scope=system-wide`。之后它会持续运行到收到 SIGINT 或 SIGTERM,期间 workload 可以正常运行。 -1. 记录子进程的 PID(它会成为 TGID,线程组 ID) -2. 打开 BPF skeleton 并把目标 TGID 写入只读数据段 -3. 加载并挂载 BPF 程序 -4. 创建 ring buffer reader +`READY` 之后每次成功 exec 都会触发 `lsm/bprm_committed_creds`。钩子以当前 `pid_tgid` 为键向 `pending` HASH map 插入一个 `exec_work`,记录时间戳并安排 task work 回调。插入或调度失败都会被计数并清理对应 map 条目。 -只有这些都完成后,父进程才通过写管道来释放子进程。这个握手消除了竞态条件:没有它的话,快速退出的命令可能在 BPF 程序挂载之前就结束了。 - -当子进程调用 `execvp` 时,`lsm/bprm_committed_creds` 钩子触发。BPF 程序比较当前 TGID 与配置的目标;如果匹配,它记录时间戳并安排 task work 回调。 - -回调 `inspect_executable` 在子进程的可睡眠上下文中执行。它: +回调 `inspect_executable` 稍后在执行 exec 的任务的可睡眠上下文中运行。它: 1. 调用 `bpf_get_task_exe_file` 获取已安装的可执行文件(返回一个带引用的 `struct file`,必须用 `bpf_put_file` 释放) 2. 用 `bpf_path_d_path` 解析路径 @@ -75,7 +68,7 @@ dynptr 用边界信息包装指针,BPF 验证器可以追踪这些边界。对 4. 解析 ELF 字段:魔数、class(32/64 位)、data(字节序)、type(可执行文件 vs 共享对象)、machine(架构) 5. 通过 ring buffer 发送事件 -用户态一边轮询 ring buffer 一边检查子进程是否已退出。单个子进程可能多次 exec(例如 `/bin/sh -c 'exec /bin/true'` 先 exec shell,然后 exec `/bin/true`),所以在子进程退出后,工具会取尽 ring buffer 中的剩余事件。 +用户态轮询 ring buffer,直到收到信号。关闭时先解除 LSM 挂载,再等待 `completed >= scheduled`,排空剩余事件并打印计数,最后销毁 skeleton。 ![exec 镜像检查器数据流](https://github.com/eunomia-bpf/bpf-developer-tutorial/raw/main/src/54-exec-image-inspector/exec-image-flow.png) @@ -94,7 +87,6 @@ dynptr 用边界信息包装指针,BPF 验证器可以追踪这些边界。对 #define EXEC_COMM_LEN 16 #define EXEC_PATH_LEN 256 -#define EXEC_PROBE_LEN 8 struct exec_event { unsigned int pid; @@ -107,13 +99,9 @@ struct exec_event { unsigned short elf_machine; int header_error; int path_error; - int direct_probe_error; - int deferred_probe_error; unsigned long long latency_ns; - unsigned long long probe_offset; char comm[EXEC_COMM_LEN]; char path[EXEC_PATH_LEN]; - unsigned char probe_bytes[EXEC_PROBE_LEN]; }; struct inspector_stats { @@ -121,13 +109,11 @@ struct inspector_stats { unsigned long long scheduled; unsigned long long schedule_errors; unsigned long long callbacks; + unsigned long long completed; unsigned long long header_errors; unsigned long long path_errors; - unsigned long long direct_probes; - unsigned long long direct_probe_errors; - unsigned long long deferred_probes; - unsigned long long deferred_probe_errors; unsigned long long dropped; + unsigned long long cleanup_errors; }; #endif /* __EXEC_IMAGE_INSPECTOR_H */ @@ -196,9 +182,6 @@ char LICENSE[] SEC("license") = "GPL"; #define ELFDATA2LSB 1 #define ELFDATA2MSB 2 -const volatile __u32 target_tgid; -const volatile __u32 probe_offset; - struct inspector_stats stats; struct { @@ -208,14 +191,14 @@ struct { struct exec_work { __u64 scheduled_ns; - int direct_probe_error; struct bpf_task_work work; }; struct { - __uint(type, BPF_MAP_TYPE_ARRAY); - __uint(max_entries, 1); - __type(key, __u32); + __uint(type, BPF_MAP_TYPE_HASH); + __uint(map_flags, BPF_F_NO_PREALLOC); + __uint(max_entries, 4096); + __type(key, __u64); __type(value, struct exec_work); } pending SEC(".maps"); @@ -226,36 +209,6 @@ static __u16 read_elf_u16(const unsigned char *header, int offset, __u8 data) return header[offset] | ((__u16)header[offset + 1] << 8); } -static int probe_file_without_sleep(struct file *file) -{ - unsigned char sample[EXEC_PROBE_LEN]; - struct bpf_dynptr dynptr; - int err; - - if (!probe_offset) - return 0; - - __sync_fetch_and_add(&stats.direct_probes, 1); - if (!file) { - err = -ENOENT; - goto record; - } - - err = bpf_dynptr_from_file(file, 0, &dynptr); - if (err) { - bpf_dynptr_file_discard(&dynptr); - goto record; - } - - err = bpf_dynptr_read(sample, sizeof(sample), &dynptr, probe_offset, 0); - bpf_dynptr_file_discard(&dynptr); - -record: - if (err) - __sync_fetch_and_add(&stats.direct_probe_errors, 1); - return err; -} - static int inspect_executable(struct bpf_map *map, void *key, void *value) { unsigned char header[64] = {}; @@ -267,16 +220,11 @@ static int inspect_executable(struct bpf_map *map, void *key, void *value) __u64 pid_tgid; int err; - (void)map; - (void)key; __sync_fetch_and_add(&stats.callbacks, 1); - pid_tgid = bpf_get_current_pid_tgid(); event.pid = (__u32)pid_tgid; event.tgid = pid_tgid >> 32; event.latency_ns = bpf_ktime_get_ns() - work->scheduled_ns; - event.direct_probe_error = work->direct_probe_error; - event.probe_offset = probe_offset; bpf_get_current_comm(event.comm, sizeof(event.comm)); task = bpf_get_current_task_btf(); @@ -284,11 +232,6 @@ static int inspect_executable(struct bpf_map *map, void *key, void *value) if (!file) { event.header_error = -ENOENT; __sync_fetch_and_add(&stats.header_errors, 1); - if (probe_offset) { - event.deferred_probe_error = -ENOENT; - __sync_fetch_and_add(&stats.deferred_probes, 1); - __sync_fetch_and_add(&stats.deferred_probe_errors, 1); - } goto emit; } @@ -303,11 +246,6 @@ static int inspect_executable(struct bpf_map *map, void *key, void *value) bpf_dynptr_file_discard(&dynptr); event.header_error = err; __sync_fetch_and_add(&stats.header_errors, 1); - if (probe_offset) { - event.deferred_probe_error = err; - __sync_fetch_and_add(&stats.deferred_probes, 1); - __sync_fetch_and_add(&stats.deferred_probe_errors, 1); - } goto put_file; } @@ -316,15 +254,6 @@ static int inspect_executable(struct bpf_map *map, void *key, void *value) event.header_error = err; __sync_fetch_and_add(&stats.header_errors, 1); } - - if (probe_offset) { - __sync_fetch_and_add(&stats.deferred_probes, 1); - err = bpf_dynptr_read(event.probe_bytes, sizeof(event.probe_bytes), - &dynptr, probe_offset, 0); - event.deferred_probe_error = err; - if (err) - __sync_fetch_and_add(&stats.deferred_probe_errors, 1); - } bpf_dynptr_file_discard(&dynptr); if (!event.header_error && header[0] == 0x7f && header[1] == 'E' && @@ -341,6 +270,9 @@ put_file: emit: if (bpf_ringbuf_output(&events, &event, sizeof(event), 0)) __sync_fetch_and_add(&stats.dropped, 1); + if (bpf_map_delete_elem(map, key)) + __sync_fetch_and_add(&stats.cleanup_errors, 1); + __sync_fetch_and_add(&stats.completed, 1); return 0; } @@ -348,598 +280,99 @@ SEC("lsm/bprm_committed_creds") void BPF_PROG(schedule_exec_inspection, struct linux_binprm *bprm) { struct task_struct *task; + struct exec_work empty_work = {}; struct exec_work *work; __u64 pid_tgid; - __u32 key = 0, tgid; + __u64 key; int err; + (void)bprm; pid_tgid = bpf_get_current_pid_tgid(); - tgid = pid_tgid >> 32; - if (target_tgid && tgid != target_tgid) - return; - + key = pid_tgid; __sync_fetch_and_add(&stats.matched, 1); + err = bpf_map_update_elem(&pending, &key, &empty_work, BPF_NOEXIST); + if (err) { + __sync_fetch_and_add(&stats.schedule_errors, 1); + return; + } work = bpf_map_lookup_elem(&pending, &key); if (!work) { __sync_fetch_and_add(&stats.schedule_errors, 1); + if (bpf_map_delete_elem(&pending, &key)) + __sync_fetch_and_add(&stats.cleanup_errors, 1); return; } work->scheduled_ns = bpf_ktime_get_ns(); - work->direct_probe_error = probe_file_without_sleep(bprm->file); task = bpf_get_current_task_btf(); err = bpf_task_work_schedule_signal(task, &work->work, &pending, inspect_executable); if (err) { __sync_fetch_and_add(&stats.schedule_errors, 1); + if (bpf_map_delete_elem(&pending, &key)) + __sync_fetch_and_add(&stats.cleanup_errors, 1); return; } __sync_fetch_and_add(&stats.scheduled, 1); } ``` -入口点是用 `SEC("lsm/bprm_committed_creds")` 声明的 `schedule_exec_inspection`。这个 LSM 钩子在新可执行文件的凭据安装后触发,此时 `bprm->file` 指向正在被执行的文件。钩子本身不可睡眠,但可以识别目标并安排延迟工作。 - -两个 `const volatile` 变量(`target_tgid` 和 `probe_offset`)位于 `.rodata` 段。用户态在 `open()` 和 `load()` 之间写入值,验证器把它们当作编译期常量来优化。 +入口点是用 `SEC("lsm/bprm_committed_creds")` 声明的 `schedule_exec_inspection`。这个 LSM 钩子在新可执行文件的凭据安装后触发。钩子本身不可睡眠,因此它创建每次 exec 独立的状态并安排延迟工作。 -当目标匹配时,程序从单元素 `pending` ARRAY map 查找 `struct exec_work`。这个结构保存 `bpf_task_work` 存储以及时间戳和可选的直接探测结果。单个槽位就够了,因为本工具每次调用只观察一个子进程。 +每次 exec 都会用 `BPF_NOEXIST` 向 `pending` HASH map 插入清零的 `struct exec_work`,key 是 `pid_tgid`,然后记录时间戳。回调删除同一个 key,因此并发 exec 不会共享一个 task-work 槽位。 -可选的 `--probe-offset` 标志让工具通过 `probe_file_without_sleep` 在不可睡眠的钩子中尝试直接读取。测试套件用这个来验证:读取冷页在不可睡眠上下文中以 `-EFAULT` 失败,但在可睡眠回调中成功。 - -保存时间戳和直接探测结果后,钩子调用 `bpf_task_work_schedule_signal`。内核持有稍后执行回调所需的引用。 +保存时间戳后,钩子调用 `bpf_task_work_schedule_signal`。内核持有稍后执行回调所需的引用。插入、查找或调度失败都会被计数;任何已经创建 pending 状态的失败路径都会删除它。 回调 `inspect_executable` 计算延迟用于诊断,然后用 `bpf_get_task_exe_file` 获取可执行文件。这返回一个带引用的 `struct file`,必须用 `bpf_put_file` 释放。回调解析路径、创建 file dynptr、读取 64 字节 ELF 头部并解析它。`read_elf_u16` 辅助函数处理字节序:ELF 文件在头部声明其字节序,多字节字段必须相应地读取。 -每条创建 dynptr 的路径(无论成功还是失败)都必须调用 `bpf_dynptr_file_discard`。最后,`bpf_ringbuf_output` 把事件发送给用户态。 +每条创建 dynptr 的路径(无论成功还是失败)都必须调用 `bpf_dynptr_file_discard`。最后,`bpf_ringbuf_output` 把事件发送给用户态,回调删除 pending 条目并增加 `completed`,使关闭流程等待真正完成的工作,而不只是已调度的工作。 ### 用户态加载器 -`exec_image_inspector.c` 协调子进程、加载 BPF、接收事件、报告结果: +完整加载器可从教程开头的源码链接查看。它的主生命周期如下: ```c -// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "exec_image_inspector.h" -#include "exec_image_inspector.skel.h" - -struct environment { - unsigned long long probe_offset; - unsigned int timeout_ms; - bool verbose; - char **command; -}; - -struct child_process { - pid_t pid; - int release_fd; - bool released; - bool reaped; - int status; -}; - -struct event_context { - unsigned int seen; -}; - -static struct environment env = { - .timeout_ms = 5000, -}; - -static int libbpf_print_fn(enum libbpf_print_level level, const char *format, - va_list args) -{ - if (level == LIBBPF_DEBUG && !env.verbose) - return 0; - return vfprintf(stderr, format, args); -} - -static void usage(const char *program) -{ - fprintf(stderr, - "Usage: %s [--probe-offset BYTES] [--timeout-ms MS] [--verbose] " - "-- COMMAND [ARG...]\n\n" - "Inspect the executable image installed by one command.\n\n" - "Options:\n" - " -p, --probe-offset BYTES also compare direct/deferred file reads\n" - " -t, --timeout-ms MS bound the command, 100-60000 " - "(default: 5000)\n" - " -v, --verbose print libbpf diagnostics\n" - " -h, --help show this help\n", - program); -} - -static int parse_u64(const char *value, unsigned long long maximum, - unsigned long long *result) -{ - char *end = NULL; - unsigned long long parsed; - - errno = 0; - parsed = strtoull(value, &end, 10); - if (errno || end == value || *end || parsed > maximum) - return -EINVAL; - *result = parsed; - return 0; -} - -static int parse_probe_offset(const char *value) -{ - unsigned long long parsed; - - if (parse_u64(value, UINT_MAX - EXEC_PROBE_LEN, &parsed)) { - fprintf(stderr, "invalid probe offset: %s\n", value); - return -EINVAL; - } - env.probe_offset = parsed; - return 0; -} - -static int parse_timeout(const char *value) -{ - unsigned long long parsed; - - if (parse_u64(value, 60000, &parsed) || parsed < 100) { - fprintf(stderr, "invalid timeout in milliseconds: %s\n", value); - return -EINVAL; - } - env.timeout_ms = parsed; - return 0; -} - -static int parse_option(int option, const char *program) -{ - switch (option) { - case 'p': - return parse_probe_offset(optarg); - case 't': - return parse_timeout(optarg); - case 'v': - env.verbose = true; - return 0; - case 'h': - usage(program); - exit(0); - default: - return -EINVAL; - } -} - -static int parse_args(int argc, char **argv) -{ - static const struct option options[] = { - { "probe-offset", required_argument, NULL, 'p' }, - { "timeout-ms", required_argument, NULL, 't' }, - { "verbose", no_argument, NULL, 'v' }, - { "help", no_argument, NULL, 'h' }, - {}, - }; - int error, option; - - while ((option = getopt_long(argc, argv, "+p:t:vh", options, NULL)) != -1) { - error = parse_option(option, argv[0]); - if (error) - return error; - } - - if (optind == argc) { - fprintf(stderr, "COMMAND is required\n"); - return -EINVAL; - } - env.command = &argv[optind]; - return 0; -} - -static long long monotonic_milliseconds(void) -{ - struct timespec timestamp; - - if (clock_gettime(CLOCK_MONOTONIC, ×tamp)) - return -errno; - return timestamp.tv_sec * 1000LL + timestamp.tv_nsec / 1000000; -} - -static int start_blocked_child(struct child_process *child) -{ - int pipe_fds[2]; - pid_t pid; - - if (pipe(pipe_fds)) - return -errno; - - pid = fork(); - if (pid < 0) { - int error = -errno; - - close(pipe_fds[0]); - close(pipe_fds[1]); - return error; - } - - if (pid == 0) { - char release; - ssize_t count; - - close(pipe_fds[1]); - do { - count = read(pipe_fds[0], &release, sizeof(release)); - } while (count < 0 && errno == EINTR); - close(pipe_fds[0]); - if (count != sizeof(release)) - _exit(126); - - /* Intentional argv execution; no shell parses the supplied arguments. */ - execvp(env.command[0], env.command); /* Flawfinder: ignore */ - fprintf(stderr, "failed to execute %s: %s\n", env.command[0], - strerror(errno)); - _exit(127); - } - - close(pipe_fds[0]); - child->pid = pid; - child->release_fd = pipe_fds[1]; - return 0; -} - -static int release_child(struct child_process *child) -{ - char release = 1; - ssize_t count; - - do { - count = write(child->release_fd, &release, sizeof(release)); - } while (count < 0 && errno == EINTR); - close(child->release_fd); - child->release_fd = -1; - if (count != sizeof(release)) - return count < 0 ? -errno : -EIO; - child->released = true; - return 0; -} - -static int child_exit_code(int status) -{ - if (WIFEXITED(status)) - return WEXITSTATUS(status); - if (WIFSIGNALED(status)) - return 128 + WTERMSIG(status); - return 125; -} - -static int reap_child(struct child_process *child, int options) -{ - pid_t result; - - if (child->reaped) - return 1; - do { - result = waitpid(child->pid, &child->status, options); - } while (result < 0 && errno == EINTR); - if (result < 0) - return -errno; - if (result == 0) - return 0; - child->reaped = true; - return 1; -} - -static int drain_events(struct ring_buffer *ring_buffer) -{ - int error; - - for (;;) { - error = ring_buffer__poll(ring_buffer, 0); - if (error == -EINTR) - continue; - if (error < 0) { - fprintf(stderr, "ring-buffer drain failed: %s\n", - strerror(-error)); - return error; - } - if (!error) - return 0; - } -} - -static const char *elf_class_name(unsigned char value) -{ - switch (value) { - case 1: - return "ELF32"; - case 2: - return "ELF64"; - default: - return "UNKNOWN"; - } -} - -static const char *elf_data_name(unsigned char value) -{ - switch (value) { - case 1: - return "LSB"; - case 2: - return "MSB"; - default: - return "UNKNOWN"; - } -} - -static const char *elf_type_name(unsigned short value) -{ - switch (value) { - case 2: - return "ET_EXEC"; - case 3: - return "ET_DYN"; - default: - return "OTHER"; - } -} - -static const char *elf_machine_name(unsigned short value) -{ - switch (value) { - case 3: - return "EM_386"; - case 62: - return "EM_X86_64"; - case 183: - return "EM_AARCH64"; - default: - return "OTHER"; - } -} - -static int handle_event(void *context, void *data, size_t size) -{ - const struct exec_event *event = data; - struct event_context *events = context; - unsigned int index; - - if (size < sizeof(*event)) { - fprintf(stderr, "short ring-buffer event: %zu bytes\n", size); - return 0; - } - - events->seen++; - printf("EXEC pid=%u tgid=%u comm=%.*s path=%.*s is_elf=%u " - "class=%s endian=%s type=%s(%u) machine=%s(%u) " - "header_error=%d path_error=%d latency_us=%llu\n", - event->pid, event->tgid, EXEC_COMM_LEN, event->comm, - EXEC_PATH_LEN, event->path, event->is_elf, - elf_class_name(event->elf_class), elf_data_name(event->elf_data), - elf_type_name(event->elf_type), event->elf_type, - elf_machine_name(event->elf_machine), event->elf_machine, - event->header_error, event->path_error, - event->latency_ns / 1000); - - if (event->probe_offset) { - printf("PROBE offset=%llu direct_error=%d deferred_error=%d bytes=", - event->probe_offset, event->direct_probe_error, - event->deferred_probe_error); - for (index = 0; index < EXEC_PROBE_LEN; index++) - printf("%02x", event->probe_bytes[index]); - putchar('\n'); - } - fflush(stdout); - return 0; -} - -static void stop_child(struct child_process *child) -{ - if (child->reaped || child->pid <= 0) - return; - if (!child->released && child->release_fd >= 0) { - close(child->release_fd); - child->release_fd = -1; - } else { - kill(child->pid, SIGKILL); - } - (void)reap_child(child, 0); -} - -static int setup_inspector(const struct child_process *child, - struct event_context *events, - struct exec_image_inspector_bpf **skeleton, - struct ring_buffer **ring_buffer) -{ - struct exec_image_inspector_bpf *skel; - struct ring_buffer *ring; - int error; - - skel = exec_image_inspector_bpf__open(); - if (!skel) { - fprintf(stderr, "failed to open BPF skeleton\n"); - return -ENOMEM; - } - *skeleton = skel; - skel->rodata->target_tgid = child->pid; - skel->rodata->probe_offset = env.probe_offset; - - error = exec_image_inspector_bpf__load(skel); - if (error) { - fprintf(stderr, "failed to load BPF object: %s\n", strerror(-error)); - return error; - } - error = exec_image_inspector_bpf__attach(skel); - if (error) { - fprintf(stderr, "failed to attach bprm_committed_creds LSM hook: %s\n", - strerror(-error)); - return error; - } - - ring = ring_buffer__new(bpf_map__fd(skel->maps.events), handle_event, - events, NULL); - if (!ring) { - fprintf(stderr, "failed to create ring buffer: %s\n", strerror(errno)); - return errno ? -errno : -ENOMEM; - } - *ring_buffer = ring; - return 0; -} - -static int reap_timed_out_child(struct child_process *child) -{ - int error; - - if (child->reaped) - return 0; - - fprintf(stderr, "command exceeded timeout; sending SIGKILL\n"); - kill(child->pid, SIGKILL); - error = reap_child(child, 0); - if (error < 0) { - fprintf(stderr, "waitpid after timeout failed: %s\n", - strerror(-error)); - return error; - } - return 0; -} - -static int wait_for_command(struct ring_buffer *ring_buffer, - struct child_process *child, - const struct event_context *events) -{ - long long deadline, now; - int error; - - printf("READY target_tgid=%d probe_offset=%llu timeout_ms=%u command=%s\n", - child->pid, env.probe_offset, env.timeout_ms, env.command[0]); - fflush(stdout); - error = release_child(child); - if (error) { - fprintf(stderr, "failed to release command process: %s\n", - strerror(-error)); - return error; - } - - now = monotonic_milliseconds(); - if (now < 0) { - fprintf(stderr, "failed to read monotonic clock: %s\n", - strerror((int)-now)); - return (int)now; - } - deadline = now + env.timeout_ms; - - for (;;) { - error = ring_buffer__poll(ring_buffer, 50); - if (error == -EINTR) - continue; - if (error < 0) { - fprintf(stderr, "ring-buffer poll failed: %s\n", strerror(-error)); - return error; - } - - error = reap_child(child, WNOHANG); - if (error < 0) { - fprintf(stderr, "waitpid failed: %s\n", strerror(-error)); - return error; - } - if (child->reaped && events->seen) - break; - - now = monotonic_milliseconds(); - if (now < 0) - return (int)now; - if (now >= deadline) - break; - if (child->reaped && !events->seen) - continue; - } - - error = reap_timed_out_child(child); - if (error) - return error; - error = drain_events(ring_buffer); - if (error) - return error; - return child_exit_code(child->status); -} - -static int report_result(const struct exec_image_inspector_bpf *skel, - const struct event_context *events, int command_exit) -{ - struct inspector_stats final_stats = skel->bss->stats; - - printf("SUMMARY matched=%llu scheduled=%llu schedule_errors=%llu " - "callbacks=%llu header_errors=%llu path_errors=%llu " - "direct_probes=%llu direct_probe_errors=%llu " - "deferred_probes=%llu deferred_probe_errors=%llu dropped=%llu " - "events=%u command_exit=%d\n", - final_stats.matched, final_stats.scheduled, - final_stats.schedule_errors, final_stats.callbacks, - final_stats.header_errors, final_stats.path_errors, - final_stats.direct_probes, final_stats.direct_probe_errors, - final_stats.deferred_probes, final_stats.deferred_probe_errors, - final_stats.dropped, events->seen, command_exit); - - if (!events->seen) { - fprintf(stderr, "no executable image event was observed\n"); - return 1; - } - if (command_exit) { - fprintf(stderr, "command exited with status %d\n", command_exit); - return command_exit; - } - return 0; -} - int main(int argc, char **argv) { struct exec_image_inspector_bpf *skel = NULL; - struct child_process child = { .release_fd = -1 }; struct event_context events = {}; struct ring_buffer *ring_buffer = NULL; - int command_exit, error, result = 1; + int error, result = 1; error = parse_args(argc, argv); if (error) { - usage(argv[0]); + usage(stderr, argv[0]); return 2; } - - libbpf_set_print(libbpf_print_fn); - error = start_blocked_child(&child); + error = install_signal_handlers(); if (error) { - fprintf(stderr, "failed to create command process: %s\n", + fprintf(stderr, "failed to install signal handlers: %s\n", strerror(-error)); return 1; } - error = setup_inspector(&child, &events, &skel, &ring_buffer); + libbpf_set_print(libbpf_print_fn); + error = setup_inspector(&events, &skel, &ring_buffer); if (error) goto cleanup; - command_exit = wait_for_command(ring_buffer, &child, &events); - if (command_exit < 0) - goto cleanup; - result = report_result(skel, &events, command_exit); + error = monitor_execs(ring_buffer); + exec_image_inspector_bpf__detach(skel); + if (!error) + error = drain_pending_events(ring_buffer, skel); + report_result(skel, &events); + if (!error) + result = 0; cleanup: - stop_child(&child); ring_buffer__free(ring_buffer); exec_image_inspector_bpf__destroy(skel); return result; } ``` -核心技术是阻塞子进程握手。`start_blocked_child` 在 BPF 设置前 fork,但子进程阻塞在 pipe 读取上。父进程记录子进程 PID、打开 skeleton、写入 `target_tgid`、加载并挂载、创建 ring buffer,然后才调用 `release_child` 写管道让子进程继续执行 `execvp`。 - -`wait_for_command` 是主循环。它打印 `READY` 行、释放子进程,然后交替轮询 ring buffer 和检查子进程是否退出。当子进程被回收且至少收到一条事件时循环结束,或者超时时结束。超时后,`reap_timed_out_child` 发送 SIGKILL 并等待。 +`setup_inspector` 打开、加载并挂载 skeleton,然后创建 ring buffer。`monitor_execs` 打印 `READY`,并轮询到 SIGINT 或 SIGTERM。 -单次 exec 可能触发多条事件,如果命令本身也调用 exec(例如 `/bin/sh -c 'exec /bin/true'`)。子进程退出后,`drain_events` 用零超时 poll 取尽剩余事件。 +关闭时 `main` 先 detach。`drain_pending_events` 再用有界的 100 ms poll 等待 `completed` 追上 `scheduled`,排空 ring buffer,并在销毁资源前报告全部计数。 `handle_event` 格式化输出,把数字 ELF 值转换为可读名称,同时保留原始值供脚本使用。 @@ -953,7 +386,7 @@ make -C src/54-exec-image-inspector clean make -C src/54-exec-image-inspector -j2 ``` -测试套件要求 Linux 6.19 或更新版本,且 BPF LSM 处于活动状态。检查 `bpf` 是否出现在 LSM 列表中: +运行前,检查 `bpf` 是否出现在活动 LSM 列表中: ```bash cat /sys/kernel/security/lsm @@ -961,52 +394,20 @@ cat /sys/kernel/security/lsm 如果缺少 `bpf`,在内核命令行中添加它:把引导加载器配置中的 `lsm=` 改为 `lsm=,bpf`。 -运行测试: +启动监控器: ```bash -cd src/54-exec-image-inspector -sudo make test +sudo ./src/54-exec-image-inspector/exec_image_inspector ``` -仓库 CI 只编译本课。运行时行为在 x86_64 上通过功能测试,内核版本 `7.0.0-rc2+`。示例输出: - -```text -TEST-MISSING matched=0 events=0 command_exit=127 -TEST-TIMEOUT matched=1 callbacks=1 events=1 command_exit=137 -TEST-REEXEC matched=2 callbacks=2 events=2 command_exit=0 final_path=/usr/bin/true -READY target_tgid=1265 probe_offset=4214784 timeout_ms=3000 command=/tmp/exec-image-inspector-sxm3lumw/exec_fixture_image -EXEC pid=1265 tgid=1265 comm=exec_fixture_im path=/tmp/exec-image-inspector-sxm3lumw/exec_fixture_image is_elf=1 class=ELF64 endian=LSB type=ET_DYN(3) machine=EM_X86_64(62) header_error=0 path_error=0 latency_us=37 -PROBE offset=4214784 direct_error=-14 deferred_error=0 bytes=454950524f424521 -exec fixture completed -SUMMARY matched=1 scheduled=1 schedule_errors=0 callbacks=1 header_errors=0 path_errors=0 direct_probes=1 direct_probe_errors=1 deferred_probes=1 deferred_probe_errors=0 dropped=0 events=1 command_exit=0 -PASS: missing-command, timeout cleanup, re-exec drain, ELF decode, and deferred file read succeeded -``` - -前三行测试覆盖边界条件: - -- **TEST-MISSING**:不存在的命令以状态 127 退出。LSM 钩子从未触发,因为 exec 在凭据提交前就失败了,所以 `matched=0` 且 `events=0`。 -- **TEST-TIMEOUT**:命令运行太久,被 SIGKILL 终止,以状态 137(128 + 9)回收。观察到一条 exec 事件。 -- **TEST-REEXEC**:内部使用 `exec` 的 shell 命令产生两条事件,最终路径是 `/usr/bin/true`。 +程序打印 `READY scope=system-wide` 后,会为每次成功 exec 输出一行 `EXEC`,其中包含进程 ID、命令名、解析后的可执行文件路径、ELF 元数据和回调延迟。按 Ctrl-C 停止;最后的 `SUMMARY` 会显示调度、回调、错误、丢弃和事件计数。 -`EXEC` 行显示已安装的镜像和解析的 ELF 字段。`PROBE` 行展示了可睡眠上下文的差异:测试向一个页面写入标记(`EIPROBE!`),刷新并从页缓存驱逐它,然后 exec。不可睡眠钩子中的直接读取返回 `-EFAULT`(`-14`),因为页是冷的。可睡眠回调中的延迟读取成功,返回标记字节的十六进制表示(`454950524f424521`)。 - -检查简单命令: +需要查看 libbpf 诊断信息时使用 `--verbose`: ```bash -sudo ./exec_image_inspector --timeout-ms 3000 -- /bin/true +sudo ./src/54-exec-image-inspector/exec_image_inspector --verbose ``` -命令行格式: - -```text -exec_image_inspector [--probe-offset BYTES] [--timeout-ms MS] [--verbose] -- COMMAND [ARG...] -``` - -- `--timeout-ms`:100 到 60000 毫秒(默认 5000)。超过此期限后工具会终止并回收命令。 -- `--probe-offset`:在钩子中(直接)和回调中(延迟)分别读取此偏移处的 8 字节,以验证冷页差异。 -- `--verbose`:打印 libbpf 诊断信息。 -- `--`:分隔 inspector 选项和要运行的命令。 - ### 环境要求 | 要求 | 详情 | @@ -1019,13 +420,13 @@ exec_image_inspector [--probe-offset BYTES] [--timeout-ms MS] [--verbose] -- COM ## 局限性与扩展 -本工具每次调用观察一个直接子进程,使用单个 map 槽位。对于并发服务场景,需要按任务分配状态、实现准入限制、处理回调回收。超时清理只向子进程发送 SIGKILL;需要进程组管理或外部信号处理的调用方需自行添加这些功能。 +本工具在 `READY` 后观察系统级成功 exec。pending HASH map 最多支持 4096 个并发 `pid_tgid` key,插入或调度压力会反映在 `schedule_errors`。关闭时会等待回调约一秒,超时则返回错误。 ## 总结 -本教程展示了如何结合 BPF task work 和 file dynptr 来检查 exec 实际安装的可执行镜像。关键思路是分离两个时刻:识别目标(在不可睡眠的 LSM 钩子中)和读取文件内容(在可睡眠的 task work 回调中)。这让 eBPF 程序能可靠地读取文件数据,即使目标字节不在页缓存中也能完成。 +本教程展示了如何结合 BPF task work 和 file dynptr 来检查 exec 实际安装的可执行镜像。LSM 钩子为每次 exec 安排工作,task work 回调则在可睡眠上下文中读取文件。这让 eBPF 程序能可靠地读取文件数据,即使目标字节不在页缓存中也能完成。 -阻塞子进程握手消除了 attach 竞态,有界执行确保清理完整,最终 drain 捕获所有事件。这些技术组合产生一个可复现的单命令工具,同时为扩展留出空间。 +持续监控器为独立 workload 提供自然的 `READY` 边界。先 detach 再 drain 的关闭顺序、每次 exec 独立的 pending 状态和完成计数,在保留原有 task-work 与 file-dynptr 教学内容的同时保证并发回调安全。 > 要深入了解 eBPF,请访问我们的教程仓库 。 diff --git a/src/54-exec-image-inspector/exec-image-flow.dot b/src/54-exec-image-inspector/exec-image-flow.dot index 1c63eff6..6b93b883 100644 --- a/src/54-exec-image-inspector/exec-image-flow.dot +++ b/src/54-exec-image-inspector/exec-image-flow.dot @@ -29,41 +29,28 @@ digraph exec_image_flow { arrowsize=0.75 ]; - fork [ - label="User space\nfork child\nchild waits on pipe", + start [ + label="User space\nstart system-wide monitor", fillcolor="#EBF8FF", color="#3182CE" ]; prepare [ - label="target_tgid + probe_offset\nload LSM + ring buffer", + label="load LSM + ring buffer", fillcolor="#EBF8FF", color="#3182CE" ]; exec [ - label="release pipe\nchild calls execvp", + label="any process calls exec", fillcolor="#EBF8FF", color="#3182CE" ]; hook [ - label="bprm_committed_creds\nmatch target_tgid", - fillcolor="#FFFAF0", - color="#DD6B20" - ]; - probe [ - label="probe_offset set?", - shape=diamond, - style="filled", - fillcolor="#FFFAF0", - color="#DD6B20", - margin="0.10,0.05" - ]; - direct [ - label="non-sleepable direct probe\nstore result", + label="bprm_committed_creds\nobserve every committed exec", fillcolor="#FFFAF0", color="#DD6B20" ]; pending [ - label="pending[0]\nscheduled_ns + direct result", + label="pending[pid_tgid]\nscheduled_ns + task work", shape=cylinder, fillcolor="#FFF5F5", color="#C05621" @@ -84,38 +71,35 @@ digraph exec_image_flow { color="#805AD5" ]; read [ - label="file-backed dynptr\nread ELF header + marker\ndiscard dynptr + put file", + label="file-backed dynptr\nread ELF header\ndiscard dynptr + put file", fillcolor="#FAF5FF", color="#805AD5" ]; event [ - label="ring buffer event\npath + ELF + probe + latency", + label="ring buffer event\npath + ELF + latency\ndelete pending entry", fillcolor="#F0FFF4", color="#38A169" ]; collect [ - label="User space\npoll + waitpid\nfinal drain + stats", + label="User space\nstream events until SIGINT/SIGTERM\nfinal drain + stats", fillcolor="#EBF8FF", color="#3182CE" ]; - { rank=same; fork; prepare; exec; } - { rank=same; hook; probe; direct; } - { rank=same; pending; schedule; } + { rank=same; start; prepare; exec; } + { rank=same; hook; pending; schedule; } { rank=same; callback; file; read; } { rank=same; event; collect; } - fork -> prepare [label="parent knows child TGID"]; - prepare -> exec [label="attach completes"]; + start -> prepare; + prepare -> exec [label="READY"]; exec -> hook [label="each committed exec"]; - hook -> probe; - probe -> direct [label="yes"]; - probe -> pending [label="no"]; - direct -> pending; + hook -> pending; pending -> schedule; schedule -> callback [label="task work runs"]; callback -> file; file -> read; read -> event; event -> collect; + collect -> exec [label="keep monitoring", style=dashed]; } diff --git a/src/54-exec-image-inspector/exec-image-flow.png b/src/54-exec-image-inspector/exec-image-flow.png index 9f21ac28..2c43f1a9 100644 Binary files a/src/54-exec-image-inspector/exec-image-flow.png and b/src/54-exec-image-inspector/exec-image-flow.png differ diff --git a/src/54-exec-image-inspector/exec_image_inspector.bpf.c b/src/54-exec-image-inspector/exec_image_inspector.bpf.c index 01027894..7fedf2d9 100644 --- a/src/54-exec-image-inspector/exec_image_inspector.bpf.c +++ b/src/54-exec-image-inspector/exec_image_inspector.bpf.c @@ -16,9 +16,6 @@ char LICENSE[] SEC("license") = "GPL"; #define ELFDATA2LSB 1 #define ELFDATA2MSB 2 -const volatile __u32 target_tgid; -const volatile __u32 probe_offset; - struct inspector_stats stats; struct { @@ -28,14 +25,14 @@ struct { struct exec_work { __u64 scheduled_ns; - int direct_probe_error; struct bpf_task_work work; }; struct { - __uint(type, BPF_MAP_TYPE_ARRAY); - __uint(max_entries, 1); - __type(key, __u32); + __uint(type, BPF_MAP_TYPE_HASH); + __uint(map_flags, BPF_F_NO_PREALLOC); + __uint(max_entries, 4096); + __type(key, __u64); __type(value, struct exec_work); } pending SEC(".maps"); @@ -46,36 +43,6 @@ static __u16 read_elf_u16(const unsigned char *header, int offset, __u8 data) return header[offset] | ((__u16)header[offset + 1] << 8); } -static int probe_file_without_sleep(struct file *file) -{ - unsigned char sample[EXEC_PROBE_LEN]; - struct bpf_dynptr dynptr; - int err; - - if (!probe_offset) - return 0; - - __sync_fetch_and_add(&stats.direct_probes, 1); - if (!file) { - err = -ENOENT; - goto record; - } - - err = bpf_dynptr_from_file(file, 0, &dynptr); - if (err) { - bpf_dynptr_file_discard(&dynptr); - goto record; - } - - err = bpf_dynptr_read(sample, sizeof(sample), &dynptr, probe_offset, 0); - bpf_dynptr_file_discard(&dynptr); - -record: - if (err) - __sync_fetch_and_add(&stats.direct_probe_errors, 1); - return err; -} - static int inspect_executable(struct bpf_map *map, void *key, void *value) { unsigned char header[64] = {}; @@ -87,16 +54,11 @@ static int inspect_executable(struct bpf_map *map, void *key, void *value) __u64 pid_tgid; int err; - (void)map; - (void)key; __sync_fetch_and_add(&stats.callbacks, 1); - pid_tgid = bpf_get_current_pid_tgid(); event.pid = (__u32)pid_tgid; event.tgid = pid_tgid >> 32; event.latency_ns = bpf_ktime_get_ns() - work->scheduled_ns; - event.direct_probe_error = work->direct_probe_error; - event.probe_offset = probe_offset; bpf_get_current_comm(event.comm, sizeof(event.comm)); task = bpf_get_current_task_btf(); @@ -104,11 +66,6 @@ static int inspect_executable(struct bpf_map *map, void *key, void *value) if (!file) { event.header_error = -ENOENT; __sync_fetch_and_add(&stats.header_errors, 1); - if (probe_offset) { - event.deferred_probe_error = -ENOENT; - __sync_fetch_and_add(&stats.deferred_probes, 1); - __sync_fetch_and_add(&stats.deferred_probe_errors, 1); - } goto emit; } @@ -123,11 +80,6 @@ static int inspect_executable(struct bpf_map *map, void *key, void *value) bpf_dynptr_file_discard(&dynptr); event.header_error = err; __sync_fetch_and_add(&stats.header_errors, 1); - if (probe_offset) { - event.deferred_probe_error = err; - __sync_fetch_and_add(&stats.deferred_probes, 1); - __sync_fetch_and_add(&stats.deferred_probe_errors, 1); - } goto put_file; } @@ -136,15 +88,6 @@ static int inspect_executable(struct bpf_map *map, void *key, void *value) event.header_error = err; __sync_fetch_and_add(&stats.header_errors, 1); } - - if (probe_offset) { - __sync_fetch_and_add(&stats.deferred_probes, 1); - err = bpf_dynptr_read(event.probe_bytes, sizeof(event.probe_bytes), - &dynptr, probe_offset, 0); - event.deferred_probe_error = err; - if (err) - __sync_fetch_and_add(&stats.deferred_probe_errors, 1); - } bpf_dynptr_file_discard(&dynptr); if (!event.header_error && header[0] == 0x7f && header[1] == 'E' && @@ -161,6 +104,9 @@ static int inspect_executable(struct bpf_map *map, void *key, void *value) emit: if (bpf_ringbuf_output(&events, &event, sizeof(event), 0)) __sync_fetch_and_add(&stats.dropped, 1); + if (bpf_map_delete_elem(map, key)) + __sync_fetch_and_add(&stats.cleanup_errors, 1); + __sync_fetch_and_add(&stats.completed, 1); return 0; } @@ -168,30 +114,37 @@ SEC("lsm/bprm_committed_creds") void BPF_PROG(schedule_exec_inspection, struct linux_binprm *bprm) { struct task_struct *task; + struct exec_work empty_work = {}; struct exec_work *work; __u64 pid_tgid; - __u32 key = 0, tgid; + __u64 key; int err; + (void)bprm; pid_tgid = bpf_get_current_pid_tgid(); - tgid = pid_tgid >> 32; - if (target_tgid && tgid != target_tgid) - return; - + key = pid_tgid; __sync_fetch_and_add(&stats.matched, 1); + err = bpf_map_update_elem(&pending, &key, &empty_work, BPF_NOEXIST); + if (err) { + __sync_fetch_and_add(&stats.schedule_errors, 1); + return; + } work = bpf_map_lookup_elem(&pending, &key); if (!work) { __sync_fetch_and_add(&stats.schedule_errors, 1); + if (bpf_map_delete_elem(&pending, &key)) + __sync_fetch_and_add(&stats.cleanup_errors, 1); return; } work->scheduled_ns = bpf_ktime_get_ns(); - work->direct_probe_error = probe_file_without_sleep(bprm->file); task = bpf_get_current_task_btf(); err = bpf_task_work_schedule_signal(task, &work->work, &pending, inspect_executable); if (err) { __sync_fetch_and_add(&stats.schedule_errors, 1); + if (bpf_map_delete_elem(&pending, &key)) + __sync_fetch_and_add(&stats.cleanup_errors, 1); return; } __sync_fetch_and_add(&stats.scheduled, 1); diff --git a/src/54-exec-image-inspector/exec_image_inspector.c b/src/54-exec-image-inspector/exec_image_inspector.c index ce087e3a..c26aed0a 100644 --- a/src/54-exec-image-inspector/exec_image_inspector.c +++ b/src/54-exec-image-inspector/exec_image_inspector.c @@ -1,241 +1,90 @@ // SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) #include #include -#include #include #include -#include #include #include #include -#include -#include -#include #include #include #include "exec_image_inspector.h" #include "exec_image_inspector.skel.h" -struct environment { - unsigned long long probe_offset; - unsigned int timeout_ms; - bool verbose; - char **command; -}; - -struct child_process { - pid_t pid; - int release_fd; - bool released; - bool reaped; - int status; -}; - struct event_context { - unsigned int seen; + unsigned long long seen; }; -static struct environment env = { - .timeout_ms = 5000, -}; +static bool verbose; +static volatile sig_atomic_t exiting; static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args) { - if (level == LIBBPF_DEBUG && !env.verbose) + if (level == LIBBPF_DEBUG && !verbose) return 0; return vfprintf(stderr, format, args); } -static void usage(const char *program) +static void usage(FILE *stream, const char *program) { - fprintf(stderr, - "Usage: %s [--probe-offset BYTES] [--timeout-ms MS] [--verbose] " - "-- COMMAND [ARG...]\n\n" - "Inspect the executable image installed by one command.\n\n" + fprintf(stream, + "Usage: %s [--verbose]\n\n" + "Continuously inspect executable images installed by exec.\n" + "Press Ctrl-C to stop and print a summary.\n\n" "Options:\n" - " -p, --probe-offset BYTES also compare direct/deferred file reads\n" - " -t, --timeout-ms MS bound the command, 100-60000 " - "(default: 5000)\n" " -v, --verbose print libbpf diagnostics\n" " -h, --help show this help\n", program); } -static int parse_u64(const char *value, unsigned long long maximum, - unsigned long long *result) -{ - char *end = NULL; - unsigned long long parsed; - - errno = 0; - parsed = strtoull(value, &end, 10); - if (errno || end == value || *end || parsed > maximum) - return -EINVAL; - *result = parsed; - return 0; -} - -static int parse_probe_offset(const char *value) -{ - unsigned long long parsed; - - if (parse_u64(value, UINT_MAX - EXEC_PROBE_LEN, &parsed)) { - fprintf(stderr, "invalid probe offset: %s\n", value); - return -EINVAL; - } - env.probe_offset = parsed; - return 0; -} - -static int parse_timeout(const char *value) -{ - unsigned long long parsed; - - if (parse_u64(value, 60000, &parsed) || parsed < 100) { - fprintf(stderr, "invalid timeout in milliseconds: %s\n", value); - return -EINVAL; - } - env.timeout_ms = parsed; - return 0; -} - -static int parse_option(int option, const char *program) -{ - switch (option) { - case 'p': - return parse_probe_offset(optarg); - case 't': - return parse_timeout(optarg); - case 'v': - env.verbose = true; - return 0; - case 'h': - usage(program); - exit(0); - default: - return -EINVAL; - } -} - static int parse_args(int argc, char **argv) { static const struct option options[] = { - { "probe-offset", required_argument, NULL, 'p' }, - { "timeout-ms", required_argument, NULL, 't' }, { "verbose", no_argument, NULL, 'v' }, { "help", no_argument, NULL, 'h' }, {}, }; - int error, option; + int option; - while ((option = getopt_long(argc, argv, "+p:t:vh", options, NULL)) != -1) { - error = parse_option(option, argv[0]); - if (error) - return error; + while ((option = getopt_long(argc, argv, "+vh", options, NULL)) != -1) { + switch (option) { + case 'v': + verbose = true; + break; + case 'h': + usage(stdout, argv[0]); + exit(0); + default: + return -EINVAL; + } } - if (optind == argc) { - fprintf(stderr, "COMMAND is required\n"); + if (optind != argc) { + fprintf(stderr, "unexpected argument: %s\n", argv[optind]); return -EINVAL; } - env.command = &argv[optind]; return 0; } -static long long monotonic_milliseconds(void) +static void handle_signal(int signal_number) { - struct timespec timestamp; - - if (clock_gettime(CLOCK_MONOTONIC, ×tamp)) - return -errno; - return timestamp.tv_sec * 1000LL + timestamp.tv_nsec / 1000000; + (void)signal_number; + exiting = 1; } -static int start_blocked_child(struct child_process *child) +static int install_signal_handlers(void) { - int pipe_fds[2]; - pid_t pid; + struct sigaction action = { + .sa_handler = handle_signal, + }; - if (pipe(pipe_fds)) + sigemptyset(&action.sa_mask); + if (sigaction(SIGINT, &action, NULL) || sigaction(SIGTERM, &action, NULL)) return -errno; - - pid = fork(); - if (pid < 0) { - int error = -errno; - - close(pipe_fds[0]); - close(pipe_fds[1]); - return error; - } - - if (pid == 0) { - char release; - ssize_t count; - - close(pipe_fds[1]); - do { - count = read(pipe_fds[0], &release, sizeof(release)); - } while (count < 0 && errno == EINTR); - close(pipe_fds[0]); - if (count != sizeof(release)) - _exit(126); - - /* Intentional argv execution; no shell parses the supplied arguments. */ - execvp(env.command[0], env.command); /* Flawfinder: ignore */ - fprintf(stderr, "failed to execute %s: %s\n", env.command[0], - strerror(errno)); - _exit(127); - } - - close(pipe_fds[0]); - child->pid = pid; - child->release_fd = pipe_fds[1]; - return 0; -} - -static int release_child(struct child_process *child) -{ - char release = 1; - ssize_t count; - - do { - count = write(child->release_fd, &release, sizeof(release)); - } while (count < 0 && errno == EINTR); - close(child->release_fd); - child->release_fd = -1; - if (count != sizeof(release)) - return count < 0 ? -errno : -EIO; - child->released = true; return 0; } -static int child_exit_code(int status) -{ - if (WIFEXITED(status)) - return WEXITSTATUS(status); - if (WIFSIGNALED(status)) - return 128 + WTERMSIG(status); - return 125; -} - -static int reap_child(struct child_process *child, int options) -{ - pid_t result; - - if (child->reaped) - return 1; - do { - result = waitpid(child->pid, &child->status, options); - } while (result < 0 && errno == EINTR); - if (result < 0) - return -errno; - if (result == 0) - return 0; - child->reaped = true; - return 1; -} - static int drain_events(struct ring_buffer *ring_buffer) { int error; @@ -308,7 +157,6 @@ static int handle_event(void *context, void *data, size_t size) { const struct exec_event *event = data; struct event_context *events = context; - unsigned int index; if (size < sizeof(*event)) { fprintf(stderr, "short ring-buffer event: %zu bytes\n", size); @@ -327,33 +175,11 @@ static int handle_event(void *context, void *data, size_t size) event->header_error, event->path_error, event->latency_ns / 1000); - if (event->probe_offset) { - printf("PROBE offset=%llu direct_error=%d deferred_error=%d bytes=", - event->probe_offset, event->direct_probe_error, - event->deferred_probe_error); - for (index = 0; index < EXEC_PROBE_LEN; index++) - printf("%02x", event->probe_bytes[index]); - putchar('\n'); - } fflush(stdout); return 0; } -static void stop_child(struct child_process *child) -{ - if (child->reaped || child->pid <= 0) - return; - if (!child->released && child->release_fd >= 0) { - close(child->release_fd); - child->release_fd = -1; - } else { - kill(child->pid, SIGKILL); - } - (void)reap_child(child, 0); -} - -static int setup_inspector(const struct child_process *child, - struct event_context *events, +static int setup_inspector(struct event_context *events, struct exec_image_inspector_bpf **skeleton, struct ring_buffer **ring_buffer) { @@ -367,12 +193,15 @@ static int setup_inspector(const struct child_process *child, return -ENOMEM; } *skeleton = skel; - skel->rodata->target_tgid = child->pid; - skel->rodata->probe_offset = env.probe_offset; error = exec_image_inspector_bpf__load(skel); if (error) { - fprintf(stderr, "failed to load BPF object: %s\n", strerror(-error)); + fprintf(stderr, + "failed to load BPF object: %s\n" + "This monitor requires Linux 6.19+, BTF, BPF JIT, " + "CONFIG_BPF_LSM=y, and an active bpf LSM.\n" + "Check the active list with: cat /sys/kernel/security/lsm\n", + strerror(-error)); return error; } error = exec_image_inspector_bpf__attach(skel); @@ -392,144 +221,94 @@ static int setup_inspector(const struct child_process *child, return 0; } -static int reap_timed_out_child(struct child_process *child) +static int monitor_execs(struct ring_buffer *ring_buffer) { int error; - if (child->reaped) - return 0; - - fprintf(stderr, "command exceeded timeout; sending SIGKILL\n"); - kill(child->pid, SIGKILL); - error = reap_child(child, 0); - if (error < 0) { - fprintf(stderr, "waitpid after timeout failed: %s\n", - strerror(-error)); - return error; - } - return 0; -} - -static int wait_for_command(struct ring_buffer *ring_buffer, - struct child_process *child, - const struct event_context *events) -{ - long long deadline, now; - int error; - - printf("READY target_tgid=%d probe_offset=%llu timeout_ms=%u command=%s\n", - child->pid, env.probe_offset, env.timeout_ms, env.command[0]); + printf("READY scope=system-wide\n"); fflush(stdout); - error = release_child(child); - if (error) { - fprintf(stderr, "failed to release command process: %s\n", - strerror(-error)); - return error; - } - - now = monotonic_milliseconds(); - if (now < 0) { - fprintf(stderr, "failed to read monotonic clock: %s\n", - strerror((int)-now)); - return (int)now; - } - deadline = now + env.timeout_ms; - - for (;;) { - error = ring_buffer__poll(ring_buffer, 50); + while (!exiting) { + error = ring_buffer__poll(ring_buffer, 100); if (error == -EINTR) continue; if (error < 0) { fprintf(stderr, "ring-buffer poll failed: %s\n", strerror(-error)); return error; } + } + return 0; +} + +static int drain_pending_events(struct ring_buffer *ring_buffer, + const struct exec_image_inspector_bpf *skel) +{ + int attempts, error; - error = reap_child(child, WNOHANG); + for (attempts = 0; attempts < 10; attempts++) { + if (skel->bss->stats.completed >= skel->bss->stats.scheduled) + return drain_events(ring_buffer); + error = ring_buffer__poll(ring_buffer, 100); + if (error == -EINTR) + continue; if (error < 0) { - fprintf(stderr, "waitpid failed: %s\n", strerror(-error)); + fprintf(stderr, "ring-buffer shutdown poll failed: %s\n", + strerror(-error)); return error; } - if (child->reaped && events->seen) - break; - - now = monotonic_milliseconds(); - if (now < 0) - return (int)now; - if (now >= deadline) - break; - if (child->reaped && !events->seen) - continue; } - error = reap_timed_out_child(child); - if (error) - return error; - error = drain_events(ring_buffer); - if (error) - return error; - return child_exit_code(child->status); + fprintf(stderr, "timed out waiting for %llu scheduled callbacks\n", + skel->bss->stats.scheduled - skel->bss->stats.completed); + return -ETIMEDOUT; } -static int report_result(const struct exec_image_inspector_bpf *skel, - const struct event_context *events, int command_exit) +static void report_result(const struct exec_image_inspector_bpf *skel, + const struct event_context *events) { struct inspector_stats final_stats = skel->bss->stats; printf("SUMMARY matched=%llu scheduled=%llu schedule_errors=%llu " - "callbacks=%llu header_errors=%llu path_errors=%llu " - "direct_probes=%llu direct_probe_errors=%llu " - "deferred_probes=%llu deferred_probe_errors=%llu dropped=%llu " - "events=%u command_exit=%d\n", + "callbacks=%llu completed=%llu header_errors=%llu path_errors=%llu " + "dropped=%llu cleanup_errors=%llu events=%llu\n", final_stats.matched, final_stats.scheduled, final_stats.schedule_errors, final_stats.callbacks, - final_stats.header_errors, final_stats.path_errors, - final_stats.direct_probes, final_stats.direct_probe_errors, - final_stats.deferred_probes, final_stats.deferred_probe_errors, - final_stats.dropped, events->seen, command_exit); - - if (!events->seen) { - fprintf(stderr, "no executable image event was observed\n"); - return 1; - } - if (command_exit) { - fprintf(stderr, "command exited with status %d\n", command_exit); - return command_exit; - } - return 0; + final_stats.completed, final_stats.header_errors, + final_stats.path_errors, + final_stats.dropped, final_stats.cleanup_errors, events->seen); } int main(int argc, char **argv) { struct exec_image_inspector_bpf *skel = NULL; - struct child_process child = { .release_fd = -1 }; struct event_context events = {}; struct ring_buffer *ring_buffer = NULL; - int command_exit, error, result = 1; + int error, result = 1; error = parse_args(argc, argv); if (error) { - usage(argv[0]); + usage(stderr, argv[0]); return 2; } - - libbpf_set_print(libbpf_print_fn); - error = start_blocked_child(&child); + error = install_signal_handlers(); if (error) { - fprintf(stderr, "failed to create command process: %s\n", + fprintf(stderr, "failed to install signal handlers: %s\n", strerror(-error)); return 1; } - error = setup_inspector(&child, &events, &skel, &ring_buffer); + libbpf_set_print(libbpf_print_fn); + error = setup_inspector(&events, &skel, &ring_buffer); if (error) goto cleanup; - command_exit = wait_for_command(ring_buffer, &child, &events); - if (command_exit < 0) - goto cleanup; - result = report_result(skel, &events, command_exit); + error = monitor_execs(ring_buffer); + exec_image_inspector_bpf__detach(skel); + if (!error) + error = drain_pending_events(ring_buffer, skel); + report_result(skel, &events); + if (!error) + result = 0; cleanup: - stop_child(&child); ring_buffer__free(ring_buffer); exec_image_inspector_bpf__destroy(skel); return result; diff --git a/src/54-exec-image-inspector/exec_image_inspector.h b/src/54-exec-image-inspector/exec_image_inspector.h index 43801760..241fe137 100644 --- a/src/54-exec-image-inspector/exec_image_inspector.h +++ b/src/54-exec-image-inspector/exec_image_inspector.h @@ -4,7 +4,6 @@ #define EXEC_COMM_LEN 16 #define EXEC_PATH_LEN 256 -#define EXEC_PROBE_LEN 8 struct exec_event { unsigned int pid; @@ -17,13 +16,9 @@ struct exec_event { unsigned short elf_machine; int header_error; int path_error; - int direct_probe_error; - int deferred_probe_error; unsigned long long latency_ns; - unsigned long long probe_offset; char comm[EXEC_COMM_LEN]; char path[EXEC_PATH_LEN]; - unsigned char probe_bytes[EXEC_PROBE_LEN]; }; struct inspector_stats { @@ -31,13 +26,11 @@ struct inspector_stats { unsigned long long scheduled; unsigned long long schedule_errors; unsigned long long callbacks; + unsigned long long completed; unsigned long long header_errors; unsigned long long path_errors; - unsigned long long direct_probes; - unsigned long long direct_probe_errors; - unsigned long long deferred_probes; - unsigned long long deferred_probe_errors; unsigned long long dropped; + unsigned long long cleanup_errors; }; #endif /* __EXEC_IMAGE_INSPECTOR_H */ diff --git a/src/54-exec-image-inspector/tests/test_exec_image_inspector.py b/src/54-exec-image-inspector/tests/test_exec_image_inspector.py index cc322328..dbcd477b 100644 --- a/src/54-exec-image-inspector/tests/test_exec_image_inspector.py +++ b/src/54-exec-image-inspector/tests/test_exec_image_inspector.py @@ -1,26 +1,23 @@ #!/usr/bin/env python3 -"""Deterministic KVM checks for the deferred executable image inspector.""" +"""Deterministic KVM checks for the continuous executable image inspector.""" from __future__ import annotations import os import re -import shutil -import stat +import select +import signal import subprocess import sys -import tempfile +import time -MARKER = b"EIPROBE!" -PROBE_DISTANCE = 4 * 1024 * 1024 +WORKERS = 16 SUMMARY = re.compile( r"SUMMARY matched=(\d+) scheduled=(\d+) schedule_errors=(\d+) " - r"callbacks=(\d+) header_errors=(\d+) path_errors=(\d+) " - r"direct_probes=(\d+) direct_probe_errors=(\d+) " - r"deferred_probes=(\d+) deferred_probe_errors=(\d+) dropped=(\d+) " - r"events=(\d+) command_exit=(\d+)" + r"callbacks=(\d+) completed=(\d+) header_errors=(\d+) path_errors=(\d+) " + r"dropped=(\d+) cleanup_errors=(\d+) events=(\d+)" ) @@ -44,153 +41,112 @@ def parse_summary(output: str) -> dict[str, int]: "scheduled", "schedule_errors", "callbacks", + "completed", "header_errors", "path_errors", - "direct_probes", - "direct_probe_errors", - "deferred_probes", - "deferred_probe_errors", "dropped", + "cleanup_errors", "events", - "command_exit", ) return dict(zip(names, map(int, match.groups()))) -def create_probe_image(fixture: str, directory: str) -> tuple[str, int]: - image = os.path.join(directory, "exec_fixture_image") - shutil.copyfile(fixture, image) - os.chmod(image, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) - - original_size = os.path.getsize(image) - probe_offset = ((original_size + 4095) // 4096) * 4096 + PROBE_DISTANCE - with open(image, "r+b", buffering=0) as executable: - executable.seek(probe_offset) - executable.write(MARKER) - os.fsync(executable.fileno()) - if hasattr(os, "posix_fadvise"): - os.posix_fadvise(executable.fileno(), 0, 0, os.POSIX_FADV_DONTNEED) - return image, probe_offset - - -def test_missing_command(inspector: str) -> dict[str, int]: - missing = "/definitely/missing/exec-image-inspector-fixture" - result = run( - inspector, - "--timeout-ms", "500", - "--", - missing, - check=False, - ) - assert result.returncode != 0, result.stdout - assert f"failed to execute {missing}" in result.stdout, result.stdout - assert "no executable image event was observed" in result.stdout, result.stdout - stats = parse_summary(result.stdout) - assert stats["matched"] == 0, stats - assert stats["scheduled"] == 0, stats - assert stats["callbacks"] == 0, stats - assert stats["events"] == 0, stats - assert stats["command_exit"] == 127, stats - return stats - - -def test_timeout_cleanup(inspector: str) -> dict[str, int]: - result = run( - inspector, - "--timeout-ms", "200", - "--", - "/bin/sleep", "2", - check=False, +def test_cli(inspector: str) -> None: + help_result = run(inspector, "--help") + assert help_result.returncode == 0, help_result.stdout + assert "Continuously inspect executable images" in help_result.stdout + + +def stop_monitor( + monitor: subprocess.Popen[str], signal_number: signal.Signals +) -> tuple[int, str]: + monitor.send_signal(signal_number) + try: + output, _ = monitor.communicate(timeout=10) + except subprocess.TimeoutExpired: + monitor.kill() + output, _ = monitor.communicate() + raise AssertionError("monitor did not stop after signal:\n" + output) + return monitor.returncode, output + + +def wait_until_ready(monitor: subprocess.Popen[str]) -> list[str]: + lines: list[str] = [] + deadline = time.monotonic() + 8 + assert monitor.stdout is not None + + while time.monotonic() < deadline: + readable, _, _ = select.select([monitor.stdout], [], [], 0.25) + if readable: + line = monitor.stdout.readline() + if line: + lines.append(line) + if line.startswith("READY "): + return lines + if monitor.poll() is not None: + break + + monitor.kill() + remainder, _ = monitor.communicate() + raise AssertionError("monitor did not become ready:\n" + "".join(lines) + remainder) + + +def test_continuous_monitor( + inspector: str, fixture: str +) -> tuple[str, dict[str, int]]: + monitor = subprocess.Popen( + [inspector], + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, ) - assert result.returncode == 128 + 9, result.stdout - assert "command exceeded timeout; sending SIGKILL" in result.stdout, result.stdout - stats = parse_summary(result.stdout) - assert stats["matched"] == 1, stats - assert stats["scheduled"] == 1, stats - assert stats["callbacks"] == 1, stats - assert stats["events"] == 1, stats - assert stats["command_exit"] == 137, stats - return stats - - -def test_reexec_chain(inspector: str) -> tuple[dict[str, int], str]: + try: + lines = wait_until_ready(monitor) + + workers = [ + subprocess.Popen( + [fixture], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT + ) + for _ in range(WORKERS) + ] + for worker in workers: + assert worker.wait(timeout=5) == 0 + + reexec = run("/bin/sh", "-c", "exec /bin/true") + assert reexec.returncode == 0, reexec.stdout + time.sleep(1) + returncode, remainder = stop_monitor(monitor, signal.SIGINT) + output = "".join(lines) + remainder + finally: + if monitor.poll() is None: + monitor.kill() + monitor.wait() + + assert returncode == 0, output + assert "READY scope=system-wide" in output, output + + exec_lines = [line for line in output.splitlines() if line.startswith("EXEC ")] + fixture_lines = [line for line in exec_lines if f"path={fixture}" in line] + assert len(fixture_lines) == WORKERS, output + assert all("is_elf=1" in line for line in fixture_lines), fixture_lines + assert all("class=ELF64" in line for line in fixture_lines), fixture_lines + assert all("endian=LSB" in line for line in fixture_lines), fixture_lines + assert all("machine=EM_X86_64(62)" in line for line in fixture_lines) + assert all("header_error=0 path_error=0" in line for line in fixture_lines) + final_path = os.path.realpath("/bin/true") - result = run( - inspector, - "--timeout-ms", "1000", - "--", - "/bin/sh", "-c", "exec /bin/true", - check=False, - ) - assert result.returncode == 0, result.stdout - exec_lines = [ - line for line in result.stdout.splitlines() if line.startswith("EXEC ") - ] - assert len(exec_lines) == 2, result.stdout - assert f"path={final_path}" in exec_lines[-1], exec_lines[-1] - - stats = parse_summary(result.stdout) - assert stats["matched"] == 2, stats - assert stats["scheduled"] == 2, stats - assert stats["callbacks"] == 2, stats - assert stats["events"] == 2, stats - assert stats["command_exit"] == 0, stats - return stats, final_path - - -def test_deferred_probe(inspector: str, fixture: str) -> str: - with tempfile.TemporaryDirectory(prefix="exec-image-inspector-") as directory: - image, probe_offset = create_probe_image(fixture, directory) - result = run( - inspector, - "--probe-offset", str(probe_offset), - "--timeout-ms", "3000", - "--", - image, - check=False, - ) - assert result.returncode == 0, result.stdout - - exec_line = next( - (line for line in result.stdout.splitlines() if line.startswith("EXEC ")), - "", - ) - assert exec_line, result.stdout - assert f"path={image}" in exec_line, exec_line - assert "is_elf=1" in exec_line, exec_line - assert "class=ELF64" in exec_line, exec_line - assert "endian=LSB" in exec_line, exec_line - assert "machine=EM_X86_64(62)" in exec_line, exec_line - assert "header_error=0 path_error=0" in exec_line, exec_line - - probe_line = next( - (line for line in result.stdout.splitlines() if line.startswith("PROBE ")), - "", - ) - assert probe_line, result.stdout - assert f"offset={probe_offset}" in probe_line, probe_line - assert "direct_error=-14" in probe_line, probe_line - assert "deferred_error=0" in probe_line, probe_line - assert f"bytes={MARKER.hex()}" in probe_line, probe_line - - stats = parse_summary(result.stdout) - expected = { - "matched": 1, - "scheduled": 1, - "schedule_errors": 0, - "callbacks": 1, - "header_errors": 0, - "path_errors": 0, - "direct_probes": 1, - "direct_probe_errors": 1, - "deferred_probes": 1, - "deferred_probe_errors": 0, - "dropped": 0, - "events": 1, - "command_exit": 0, - } - assert stats == expected, stats - return result.stdout + assert any(f"path={final_path}" in line for line in exec_lines), output + + stats = parse_summary(output) + assert stats["matched"] >= WORKERS + 2, stats + assert stats["scheduled"] == stats["matched"], stats + assert stats["schedule_errors"] == 0, stats + assert stats["callbacks"] == stats["scheduled"], stats + assert stats["completed"] == stats["scheduled"], stats + assert stats["events"] == stats["completed"], stats + assert stats["dropped"] == 0, stats + assert stats["cleanup_errors"] == 0, stats + return output, stats def main() -> int: @@ -204,30 +160,17 @@ def main() -> int: sys.argv[2] if len(sys.argv) > 2 else "./tests/exec_fixture" ) - missing_stats = test_missing_command(inspector) - timeout_stats = test_timeout_cleanup(inspector) - reexec_stats, final_path = test_reexec_chain(inspector) - output = test_deferred_probe(inspector, fixture) - print( - "TEST-MISSING " - f"matched={missing_stats['matched']} events={missing_stats['events']} " - f"command_exit={missing_stats['command_exit']}" - ) - print( - "TEST-TIMEOUT " - f"matched={timeout_stats['matched']} callbacks={timeout_stats['callbacks']} " - f"events={timeout_stats['events']} command_exit={timeout_stats['command_exit']}" - ) + test_cli(inspector) + output, stats = test_continuous_monitor(inspector, fixture) print( - "TEST-REEXEC " - f"matched={reexec_stats['matched']} callbacks={reexec_stats['callbacks']} " - f"events={reexec_stats['events']} command_exit={reexec_stats['command_exit']} " - f"final_path={final_path}" + "TEST-CONTINUOUS " + f"workers={WORKERS} matched={stats['matched']} " + f"callbacks={stats['callbacks']} events={stats['events']}" ) print(output, end="") print( - "PASS: missing-command, timeout cleanup, re-exec drain, ELF decode, " - "and deferred file read succeeded" + "PASS: persistent monitoring, concurrent execs, signal cleanup, " + "and ELF decode succeeded" ) return 0