From c16e7ef934173c926bf99577c334e3e312ce889d Mon Sep 17 00:00:00 2001 From: Christiano Haesbaert Date: Wed, 20 Nov 2024 13:39:40 +0100 Subject: [PATCH] Include namespace inode numbers on process events (#213) This diff adds the inode number of each namespace the process belongs to, to process events. Two namespaces were excluded: pid_for_children and time_for_children, see namespaces(7). The pid namespace is a bit special as it's not in nsproxy, the user facing pid level is the last/deepest. There was an inclusion of mnt ns, that is not used by beats or endpoint, I kept it but made it fetch from the new "path". While here, add support for changing the compiler in gen_initramfs.sh by setting CC. While Fedora includes a ${arch}-linux-gnu-gcc, it doesn't include matching headers, so I can't really compile the tests locally. --- GPL/Events/EbpfEventProto.h | 13 +++++ GPL/Events/File/Probe.bpf.c | 25 +++++---- GPL/Events/Helpers.h | 21 ++++++++ GPL/Events/Process/Probe.bpf.c | 3 ++ non-GPL/Events/EventsTrace/EventsTrace.c | 23 +++++++++ testing/scripts/gen_initramfs.sh | 2 +- testing/testrunner/ebpf_test.go | 9 ++-- testing/testrunner/utils.go | 64 +++++++++++++++++++++++- 8 files changed, 142 insertions(+), 18 deletions(-) diff --git a/GPL/Events/EbpfEventProto.h b/GPL/Events/EbpfEventProto.h index da57b426..fc697e74 100644 --- a/GPL/Events/EbpfEventProto.h +++ b/GPL/Events/EbpfEventProto.h @@ -157,6 +157,16 @@ struct ebpf_file_info { uint64_t ctime; } __attribute__((packed)); +struct ebpf_namespace_info { + uint32_t uts_inonum; + uint32_t ipc_inonum; + uint32_t mnt_inonum; + uint32_t net_inonum; + uint32_t cgroup_inonum; + uint32_t time_inonum; + uint32_t pid_inonum; +} __attribute__((packed)); + // Full events follow struct ebpf_file_delete_event { struct ebpf_event_header hdr; @@ -223,6 +233,7 @@ struct ebpf_process_fork_event { struct ebpf_cred_info creds; struct ebpf_tty_dev ctty; char comm[TASK_COMM_LEN]; + struct ebpf_namespace_info ns; // Variable length fields: pids_ss_cgroup_path struct ebpf_varlen_fields_start vl_fields; @@ -238,6 +249,7 @@ struct ebpf_process_exec_event { struct ebpf_cred_info creds; struct ebpf_tty_dev ctty; char comm[TASK_COMM_LEN]; + struct ebpf_namespace_info ns; uint32_t inode_nlink; uint32_t flags; @@ -251,6 +263,7 @@ struct ebpf_process_exit_event { struct ebpf_cred_info creds; struct ebpf_tty_dev ctty; char comm[TASK_COMM_LEN]; + struct ebpf_namespace_info ns; int32_t exit_code; // Variable length fields: pids_ss_cgroup_path diff --git a/GPL/Events/File/Probe.bpf.c b/GPL/Events/File/Probe.bpf.c index 4053a89f..8b7982b7 100644 --- a/GPL/Events/File/Probe.bpf.c +++ b/GPL/Events/File/Probe.bpf.c @@ -31,11 +31,6 @@ DECL_FUNC_ARG_EXISTS(vfs_rename, rd); DECL_FUNC_ARG(do_truncate, filp); DECL_FUNC_RET(do_truncate); -static int mntns(const struct task_struct *task) -{ - return BPF_CORE_READ(task, nsproxy, mnt_ns, ns.inum); -} - static int do_unlinkat__enter() { struct ebpf_events_state state = {}; @@ -129,9 +124,11 @@ static int vfs_unlink__exit(int ret) ebpf_cred_info__fill(&event->creds, task); struct path p; - p.dentry = &state->unlink.de; - p.mnt = state->unlink.mnt; - event->mntns = mntns(task); + p.dentry = &state->unlink.de; + p.mnt = state->unlink.mnt; + struct ebpf_namespace_info ns; + ebpf_ns__fill(&ns, task); + event->mntns = ns.mnt_inonum; bpf_get_current_comm(event->comm, TASK_COMM_LEN); ebpf_file_info__fill(&event->finfo, p.dentry); @@ -236,7 +233,9 @@ static void prepare_and_send_file_event(struct file *f, struct path p = BPF_CORE_READ(f, f_path); ebpf_pid_info__fill(&event->pids, task); ebpf_cred_info__fill(&event->creds, task); - event->mntns = mntns(task); + struct ebpf_namespace_info ns; + ebpf_ns__fill(&ns, task); + event->mntns = ns.mnt_inonum; bpf_get_current_comm(event->comm, TASK_COMM_LEN); ebpf_file_info__fill(&event->finfo, p.dentry); @@ -489,7 +488,9 @@ static int vfs_rename__exit(int ret) event->hdr.ts_boot = bpf_ktime_get_boot_ns_helper(); ebpf_pid_info__fill(&event->pids, task); ebpf_cred_info__fill(&event->creds, task); - event->mntns = mntns(task); + struct ebpf_namespace_info ns; + ebpf_ns__fill(&ns, task); + event->mntns = ns.mnt_inonum; bpf_get_current_comm(event->comm, TASK_COMM_LEN); ebpf_file_info__fill(&event->finfo, de); @@ -559,7 +560,9 @@ static void file_modify_event__emit(enum ebpf_file_change_type typ, struct path event->change_type = typ; ebpf_pid_info__fill(&event->pids, task); ebpf_cred_info__fill(&event->creds, task); - event->mntns = mntns(task); + struct ebpf_namespace_info ns; + ebpf_ns__fill(&ns, task); + event->mntns = ns.mnt_inonum; bpf_get_current_comm(event->comm, TASK_COMM_LEN); struct dentry *d = BPF_CORE_READ(path, dentry); ebpf_file_info__fill(&event->finfo, d); diff --git a/GPL/Events/Helpers.h b/GPL/Events/Helpers.h index c8c4a383..f3d97abe 100644 --- a/GPL/Events/Helpers.h +++ b/GPL/Events/Helpers.h @@ -306,6 +306,27 @@ static void ebpf_comm__fill(char *comm, size_t len, const struct task_struct *ta read_kernel_str_or_empty_str(comm, len, BPF_CORE_READ(task, comm)); } +static void ebpf_ns__fill(struct ebpf_namespace_info *nsi, const struct task_struct *task) +{ + struct pid *pid; + int pid_level; + + nsi->uts_inonum = BPF_CORE_READ(task, nsproxy, uts_ns, ns.inum); + nsi->ipc_inonum = BPF_CORE_READ(task, nsproxy, ipc_ns, ns.inum); + nsi->mnt_inonum = BPF_CORE_READ(task, nsproxy, mnt_ns, ns.inum); + nsi->net_inonum = BPF_CORE_READ(task, nsproxy, net_ns, ns.inum); + nsi->cgroup_inonum = BPF_CORE_READ(task, nsproxy, cgroup_ns, ns.inum); + nsi->time_inonum = BPF_CORE_READ(task, nsproxy, time_ns, ns.inum); + + pid = BPF_CORE_READ(task, thread_pid); + if (pid == NULL) { + nsi->pid_inonum = 0; + return; + } + pid_level = BPF_CORE_READ(pid, level); + nsi->pid_inonum = BPF_CORE_READ(pid, numbers[pid_level].ns, ns.inum); +} + struct { __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY); __type(key, u32); diff --git a/GPL/Events/Process/Probe.bpf.c b/GPL/Events/Process/Probe.bpf.c index c49b71c5..0df9d831 100644 --- a/GPL/Events/Process/Probe.bpf.c +++ b/GPL/Events/Process/Probe.bpf.c @@ -60,6 +60,7 @@ int BPF_PROG(sched_process_fork, const struct task_struct *parent, const struct ebpf_cred_info__fill(&event->creds, parent); ebpf_ctty__fill(&event->ctty, child); ebpf_comm__fill(event->comm, sizeof(event->comm), child); + ebpf_ns__fill(&event->ns, child); // Variable length fields ebpf_vl_fields__init(&event->vl_fields); @@ -111,6 +112,7 @@ int BPF_PROG(sched_process_exec, ebpf_cred_info__fill(&event->creds, task); ebpf_ctty__fill(&event->ctty, task); ebpf_comm__fill(event->comm, sizeof(event->comm), task); + ebpf_ns__fill(&event->ns, task); // set setuid and setgid flags struct file *f = BPF_CORE_READ(binprm, file); @@ -211,6 +213,7 @@ static int taskstats_exit__enter(const struct task_struct *task, int group_dead) ebpf_cred_info__fill(&event->creds, task); ebpf_ctty__fill(&event->ctty, task); ebpf_comm__fill(event->comm, sizeof(event->comm), task); + ebpf_ns__fill(&event->ns, task); // Variable length fields ebpf_vl_fields__init(&event->vl_fields); diff --git a/non-GPL/Events/EventsTrace/EventsTrace.c b/non-GPL/Events/EventsTrace/EventsTrace.c index f27d5ab6..2ed50925 100644 --- a/non-GPL/Events/EventsTrace/EventsTrace.c +++ b/non-GPL/Events/EventsTrace/EventsTrace.c @@ -414,6 +414,26 @@ static void out_file_info(const char *name, struct ebpf_file_info *finfo) out_object_end(); } +static void out_ns_info(const char *name, struct ebpf_namespace_info *ns) +{ + printf("\"%s\":", name); + out_object_start(); + out_uint("uts", ns->uts_inonum); + out_comma(); + out_uint("ipc", ns->ipc_inonum); + out_comma(); + out_uint("mnt", ns->mnt_inonum); + out_comma(); + out_uint("net", ns->net_inonum); + out_comma(); + out_uint("cgroup", ns->cgroup_inonum); + out_comma(); + out_uint("time", ns->time_inonum); + out_comma(); + out_uint("pid", ns->pid_inonum); + out_object_end(); +} + static void out_null_delimited_string_array(const char *name, char *buf, size_t buf_size) { // buf is an array (argv, env etc.) with multiple values delimited by a '\0' @@ -768,6 +788,9 @@ static void out_process_fork(struct ebpf_process_fork_event *evt) out_comma(); out_string("comm", evt->comm); + out_comma(); + + out_ns_info("ns", &evt->ns); struct ebpf_varlen_field *field; FOR_EACH_VARLEN_FIELD(evt->vl_fields, field) diff --git a/testing/scripts/gen_initramfs.sh b/testing/scripts/gen_initramfs.sh index d8f3fcfb..ead26666 100755 --- a/testing/scripts/gen_initramfs.sh +++ b/testing/scripts/gen_initramfs.sh @@ -56,7 +56,7 @@ build_testbins() { for c_src in *.c; do local bin_path=bin/$arch/$(basename $c_src .c) - ${arch}-linux-gnu-gcc -g -static $c_src -o $bin_path \ + ${CC-${arch}-linux-gnu-gcc} -g -static $c_src -o $bin_path \ || exit_error "compilation of $c_src for $arch failed (see above)" done diff --git a/testing/testrunner/ebpf_test.go b/testing/testrunner/ebpf_test.go index cc654eb4..c815c9d3 100644 --- a/testing/testrunner/ebpf_test.go +++ b/testing/testrunner/ebpf_test.go @@ -94,6 +94,11 @@ func ForkExit(t *testing.T, et *Runner) { require.Equal(t, forkEvent.ChildPids.Sid, forkEvent.ParentPids.Sid) require.Equal(t, forkEvent.ChildPids.Pgid, forkEvent.ParentPids.Pgid) require.NotEqual(t, forkEvent.ChildPids.Tgid, forkEvent.ParentPids.Tgid) + + // Check if all namespace values match /proc/self/ns/* + ns, err := FetchNsFromProc() + require.NoError(t, err) + require.Equal(t, forkEvent.Ns, ns) } func ForkExec(t *testing.T, et *Runner) { @@ -154,7 +159,6 @@ func ForkExec(t *testing.T, et *Runner) { require.Equal(t, execEvent.Env[0], "TEST_ENV_KEY1=TEST_ENV_VAL1") require.Equal(t, execEvent.Env[1], "TEST_ENV_KEY2=TEST_ENV_VAL2") require.Equal(t, execEvent.Cwd, "/") - } func FileCreate(t *testing.T, et *Runner) { @@ -185,7 +189,6 @@ func FileCreate(t *testing.T, et *Runner) { } func FileDelete(t *testing.T, et *Runner) { - var binOutput struct { PidInfo TestPidInfo `json:"pid_info"` FileNameOrig string `json:"filename_orig"` @@ -426,7 +429,6 @@ func Tcpv4ConnectionAttempt(t *testing.T, et *Runner) { require.Equal(t, ev.Net.DestPort, binOutput.ServerPort) require.Equal(t, ev.Net.NetNs, binOutput.NetNs) require.Equal(t, ev.Comm, "tcpv4_connect") - } func Tcpv4ConnectionAccept(t *testing.T, et *Runner) { @@ -665,5 +667,4 @@ func TestEbpf(t *testing.T) { run.Stop() }) } - } diff --git a/testing/testrunner/utils.go b/testing/testrunner/utils.go index 301ed07a..fbfc550f 100644 --- a/testing/testrunner/utils.go +++ b/testing/testrunner/utils.go @@ -18,6 +18,7 @@ import ( "os/exec" "path/filepath" "runtime" + "strconv" "strings" "testing" @@ -90,11 +91,22 @@ type FileInfo struct { Ctime uint64 `json:"ctime"` } +type NsInfo struct { + Uts uint32 `json:"uts"` + Ipc uint32 `json:"ipc"` + Mnt uint32 `json:"mnt"` + Net uint32 `json:"net"` + Cgroup uint32 `json:"cgroup"` + Time uint32 `json:"time"` + Pid uint32 `json:"pid"` +} + type ProcessForkEvent struct { ParentPids PidInfo `json:"parent_pids"` ChildPids PidInfo `json:"child_pids"` Creds CredInfo `json:"creds"` Ctty TtyInfo `json:"ctty"` + Ns NsInfo `json:"ns"` } type ProcessExecEvent struct { @@ -195,8 +207,10 @@ var testBinaryPath = "/" var eventsTracePath = "/EventsTrace" // Path to the TC filter test binary and probe. This one is weird and lives outside the rest of the test binaries -var tcTestPath = "/BPFTcFilterTests" -var tcObjPath = "/TcFilter.bpf.o" +var ( + tcTestPath = "/BPFTcFilterTests" + tcObjPath = "/TcFilter.bpf.o" +) // init will run at startup and figure out if we're running in the bluebox test env or not, // and set paths for the binaries as needed @@ -327,3 +341,49 @@ func PrintDebugOutputOnFail() { fmt.Println("BPF test failed, see errors and stacktrace above") } + +func FetchNsFromProc() (NsInfo, error) { + var ns NsInfo + + fetch := func(name string, dst *uint32) error { + s, err := os.Readlink("/proc/self/ns/" + name) + if err != nil { + return err + } + start := strings.IndexByte(s, '[') + if start == -1 { + return fmt.Errorf("`[` not found for ns %s", name) + } + start++ + end := strings.IndexByte(s, ']') + if end == -1 { + return fmt.Errorf("`]` not found for ns %s", name) + } + v, err := strconv.Atoi(s[start:end]) + if err != nil { + return err + } + *dst = uint32(v) + return nil + } + + calls := []struct { + name string + dst *uint32 + }{ + {"uts", &ns.Uts}, + {"ipc", &ns.Ipc}, + {"mnt", &ns.Mnt}, + {"net", &ns.Net}, + {"cgroup", &ns.Cgroup}, + {"time", &ns.Time}, + {"pid", &ns.Pid}, + } + for _, call := range calls { + if err := fetch(call.name, call.dst); err != nil { + return ns, err + } + } + + return ns, nil +}