mirror of
https://github.com/eunomia-bpf/bpf-developer-tutorial.git
synced 2026-10-31 20:54:56 +08:00
Add advanced eBPF tutorials 51–54 and tutorial agent skills (#220)
* Add targeted TCP connection quarantine tool * Document TCP quarantine workflow * Refresh package indexes before CI install * Document TCP quarantine namespace scope * Require BPF events for TCP iterator * Add production fsession latency tracer * Document fsession latency workflow * Reject empty fsession CLI values * Preserve successful fsession exit status * Document fsession deadline regression test * Drain fsession events before summary * Document fsession PID and shutdown semantics * Simplify fsession polling control flow * Add bounded BPF egress pacer tutorial * Test queued packet cleanup on signal * Document the BPF egress pacer * Polish and review the BPF egress pacer * Refactor egress pacer option parsing * Add executable image inspector lesson * Build the exec image inspector in CI * Document executable image inspection * Handle re-exec chains in the image inspector * Polish exec image inspector guidance * Reduce exec inspector wait complexity * Rewrite the egress pacer tutorial * Rewrite the exec image inspector tutorial * Polish the exec image inspector tutorial * Correct the egress pacer tutorial * Make the exec tutorial Chinese natural * Fix egress tutorial prose checks * Finish the exec tutorial style pass * Clarify exec tutorial runtime evidence * Polish exec tutorial scope wording * Rewrite the egress pacer tutorial for readers * Regenerate tutorial indexes * Teach the egress pacer component by component * Use standard Markdown in the egress tutorial * Strengthen the egress pacer tutorial narrative * jiji * Use positive framing in the egress tutorial * Rewrite the exec image inspector tutorial * Rewrite the TCP quarantine tutorial * Add the egress pacer lifecycle diagram * Refresh TCP tutorial indexes * Add the exec image inspection flow diagram * Use one source directory link * Rewrite the fsession latency tutorial * Add fsession feature background * Add BPF qdisc feature background * Add TCP iterator feature background * Add exec inspection feature background * Require feature background in tutorial openings * Polish fsession background prose * Simplify repository tutorial skills * Update exec inspector tooling revisions * Restructure tutorials 51-54 following advanced guideline - Update SKILL.md with strict section ordering and "why" requirements - Rewrite README.zh.md for tutorials 51-54: - Add background sections explaining why traditional approaches fail - Move compilation/execution sections after code analysis - Add high-level mechanism explanation before code - Use paragraph style instead of bullet lists * Consolidate skill guidelines and remove duplicates - Update write-bpf-production-tutorial skill to reference 47, 48, 49 - Add structure requirements section with "Why" section emphasis - Remove duplicate advanced-tutorial-guidelines.md (use scripts/guideline_advance.md instead) * Rewrite English READMEs 51-54 to match Chinese structure Restructure all four English tutorials to match their Chinese counterparts: - Add "Why this approach is needed" sections explaining traditional limitations - Add high-level mechanism sections before code - Move compilation/execution sections after code analysis - Add environment requirements tables - Use consistent section ordering across all tutorials * Regenerate TOC after tutorial title changes Run generate_toc.py to sync SUMMARY and README files with updated tutorial titles from the restructuring. * Regenerate compatibility tables after title changes --------- Co-authored-by: LinuxDev9002 <i@littlefisher.me>
This commit is contained in:
536
src/54-exec-image-inspector/exec_image_inspector.c
Normal file
536
src/54-exec-image-inspector/exec_image_inspector.c
Normal file
@@ -0,0 +1,536 @@
|
||||
// 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;
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
error = parse_args(argc, argv);
|
||||
if (error) {
|
||||
usage(argv[0]);
|
||||
return 2;
|
||||
}
|
||||
|
||||
libbpf_set_print(libbpf_print_fn);
|
||||
error = start_blocked_child(&child);
|
||||
if (error) {
|
||||
fprintf(stderr, "failed to create command process: %s\n",
|
||||
strerror(-error));
|
||||
return 1;
|
||||
}
|
||||
|
||||
error = setup_inspector(&child, &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);
|
||||
|
||||
cleanup:
|
||||
stop_child(&child);
|
||||
ring_buffer__free(ring_buffer);
|
||||
exec_image_inspector_bpf__destroy(skel);
|
||||
return result;
|
||||
}
|
||||
Reference in New Issue
Block a user