mirror of
https://github.com/eunomia-bpf/bpf-developer-tutorial.git
synced 2026-11-02 13:45:08 +08:00
Make exec image inspection a persistent monitor (#223)
* Rebase lesson 54 as a standalone monitor * docs: preserve lesson 54 introduction * docs: remove design aside from lesson 54 * Simplify lesson 54 to the monitor path --------- Co-authored-by: LinuxDev9002 <i@littlefisher.me>
This commit is contained in:
@@ -1,241 +1,90 @@
|
||||
// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
|
||||
#include <errno.h>
|
||||
#include <getopt.h>
|
||||
#include <limits.h>
|
||||
#include <signal.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#include <bpf/libbpf.h>
|
||||
#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;
|
||||
}
|
||||
|
||||
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);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int report_result(const struct exec_image_inspector_bpf *skel,
|
||||
const struct event_context *events, int command_exit)
|
||||
static int drain_pending_events(struct ring_buffer *ring_buffer,
|
||||
const struct exec_image_inspector_bpf *skel)
|
||||
{
|
||||
int attempts, error;
|
||||
|
||||
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, "ring-buffer shutdown poll failed: %s\n",
|
||||
strerror(-error));
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
fprintf(stderr, "timed out waiting for %llu scheduled callbacks\n",
|
||||
skel->bss->stats.scheduled - skel->bss->stats.completed);
|
||||
return -ETIMEDOUT;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user