diff --git a/detector.go b/detector.go index ef22fd2..5c4c5ec 100644 --- a/detector.go +++ b/detector.go @@ -222,7 +222,8 @@ func (d *Detector) processExecDetails(pid int) (*ProcessExecDetails, error) { // (i.e we missed the exec event), try to get the container PID from the /proc file system cPID, err = proc.InnerMostPID(pid) if err != nil { - d.l.Error("failed to get container PID", "pid", pid, "error", err) + // on kernels older than 4.1, the inner most PID might not be available in proc fs. + d.l.Debug("failed to get container PID", "pid", pid, "error", err) } } diff --git a/internal/probe/bpf_arm64_bpfel.go b/internal/probe/bpf_arm64_bpfel.go index e2401de..8a855fe 100644 --- a/internal/probe/bpf_arm64_bpfel.go +++ b/internal/probe/bpf_arm64_bpfel.go @@ -82,6 +82,7 @@ type bpfSpecs struct { // // It can be passed ebpf.CollectionSpec.Assign. type bpfProgramSpecs struct { + TracepointSchedSchedProcessExec *ebpf.ProgramSpec `ebpf:"tracepoint__sched__sched_process_exec"` TracepointSchedSchedProcessExit *ebpf.ProgramSpec `ebpf:"tracepoint__sched__sched_process_exit"` TracepointSyscallsSysEnterExecve *ebpf.ProgramSpec `ebpf:"tracepoint__syscalls__sys_enter_execve"` TracepointSyscallsSysEnterOpen *ebpf.ProgramSpec `ebpf:"tracepoint__syscalls__sys_enter_open"` @@ -170,6 +171,7 @@ type bpfVariables struct { // // It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. type bpfPrograms struct { + TracepointSchedSchedProcessExec *ebpf.Program `ebpf:"tracepoint__sched__sched_process_exec"` TracepointSchedSchedProcessExit *ebpf.Program `ebpf:"tracepoint__sched__sched_process_exit"` TracepointSyscallsSysEnterExecve *ebpf.Program `ebpf:"tracepoint__syscalls__sys_enter_execve"` TracepointSyscallsSysEnterOpen *ebpf.Program `ebpf:"tracepoint__syscalls__sys_enter_open"` @@ -184,6 +186,7 @@ type bpfPrograms struct { func (p *bpfPrograms) Close() error { return _BpfClose( + p.TracepointSchedSchedProcessExec, p.TracepointSchedSchedProcessExit, p.TracepointSyscallsSysEnterExecve, p.TracepointSyscallsSysEnterOpen, diff --git a/internal/probe/bpf_no_btf_arm64_bpfel.go b/internal/probe/bpf_no_btf_arm64_bpfel.go index fdfcfab..f8b433e 100644 --- a/internal/probe/bpf_no_btf_arm64_bpfel.go +++ b/internal/probe/bpf_no_btf_arm64_bpfel.go @@ -82,6 +82,7 @@ type bpf_no_btfSpecs struct { // // It can be passed ebpf.CollectionSpec.Assign. type bpf_no_btfProgramSpecs struct { + TracepointSchedSchedProcessExec *ebpf.ProgramSpec `ebpf:"tracepoint__sched__sched_process_exec"` TracepointSchedSchedProcessExit *ebpf.ProgramSpec `ebpf:"tracepoint__sched__sched_process_exit"` TracepointSchedSchedProcessFork *ebpf.ProgramSpec `ebpf:"tracepoint__sched__sched_process_fork"` TracepointSyscallsSysEnterExecve *ebpf.ProgramSpec `ebpf:"tracepoint__syscalls__sys_enter_execve"` @@ -170,6 +171,7 @@ type bpf_no_btfVariables struct { // // It can be passed to loadBpf_no_btfObjects or ebpf.CollectionSpec.LoadAndAssign. type bpf_no_btfPrograms struct { + TracepointSchedSchedProcessExec *ebpf.Program `ebpf:"tracepoint__sched__sched_process_exec"` TracepointSchedSchedProcessExit *ebpf.Program `ebpf:"tracepoint__sched__sched_process_exit"` TracepointSchedSchedProcessFork *ebpf.Program `ebpf:"tracepoint__sched__sched_process_fork"` TracepointSyscallsSysEnterExecve *ebpf.Program `ebpf:"tracepoint__syscalls__sys_enter_execve"` @@ -184,6 +186,7 @@ type bpf_no_btfPrograms struct { func (p *bpf_no_btfPrograms) Close() error { return _Bpf_no_btfClose( + p.TracepointSchedSchedProcessExec, p.TracepointSchedSchedProcessExit, p.TracepointSchedSchedProcessFork, p.TracepointSyscallsSysEnterExecve, diff --git a/internal/probe/bpf_no_btf_x86_bpfel.go b/internal/probe/bpf_no_btf_x86_bpfel.go index 9db8bf6..136d5d6 100644 --- a/internal/probe/bpf_no_btf_x86_bpfel.go +++ b/internal/probe/bpf_no_btf_x86_bpfel.go @@ -82,6 +82,7 @@ type bpf_no_btfSpecs struct { // // It can be passed ebpf.CollectionSpec.Assign. type bpf_no_btfProgramSpecs struct { + TracepointSchedSchedProcessExec *ebpf.ProgramSpec `ebpf:"tracepoint__sched__sched_process_exec"` TracepointSchedSchedProcessExit *ebpf.ProgramSpec `ebpf:"tracepoint__sched__sched_process_exit"` TracepointSchedSchedProcessFork *ebpf.ProgramSpec `ebpf:"tracepoint__sched__sched_process_fork"` TracepointSyscallsSysEnterExecve *ebpf.ProgramSpec `ebpf:"tracepoint__syscalls__sys_enter_execve"` @@ -170,6 +171,7 @@ type bpf_no_btfVariables struct { // // It can be passed to loadBpf_no_btfObjects or ebpf.CollectionSpec.LoadAndAssign. type bpf_no_btfPrograms struct { + TracepointSchedSchedProcessExec *ebpf.Program `ebpf:"tracepoint__sched__sched_process_exec"` TracepointSchedSchedProcessExit *ebpf.Program `ebpf:"tracepoint__sched__sched_process_exit"` TracepointSchedSchedProcessFork *ebpf.Program `ebpf:"tracepoint__sched__sched_process_fork"` TracepointSyscallsSysEnterExecve *ebpf.Program `ebpf:"tracepoint__syscalls__sys_enter_execve"` @@ -184,6 +186,7 @@ type bpf_no_btfPrograms struct { func (p *bpf_no_btfPrograms) Close() error { return _Bpf_no_btfClose( + p.TracepointSchedSchedProcessExec, p.TracepointSchedSchedProcessExit, p.TracepointSchedSchedProcessFork, p.TracepointSyscallsSysEnterExecve, diff --git a/internal/probe/bpf_small_arm64_bpfel.go b/internal/probe/bpf_small_arm64_bpfel.go index 5bfb36d..cf36475 100644 --- a/internal/probe/bpf_small_arm64_bpfel.go +++ b/internal/probe/bpf_small_arm64_bpfel.go @@ -82,6 +82,7 @@ type bpf_smallSpecs struct { // // It can be passed ebpf.CollectionSpec.Assign. type bpf_smallProgramSpecs struct { + TracepointSchedSchedProcessExec *ebpf.ProgramSpec `ebpf:"tracepoint__sched__sched_process_exec"` TracepointSchedSchedProcessExit *ebpf.ProgramSpec `ebpf:"tracepoint__sched__sched_process_exit"` TracepointSyscallsSysEnterExecve *ebpf.ProgramSpec `ebpf:"tracepoint__syscalls__sys_enter_execve"` TracepointSyscallsSysEnterOpen *ebpf.ProgramSpec `ebpf:"tracepoint__syscalls__sys_enter_open"` @@ -170,6 +171,7 @@ type bpf_smallVariables struct { // // It can be passed to loadBpf_smallObjects or ebpf.CollectionSpec.LoadAndAssign. type bpf_smallPrograms struct { + TracepointSchedSchedProcessExec *ebpf.Program `ebpf:"tracepoint__sched__sched_process_exec"` TracepointSchedSchedProcessExit *ebpf.Program `ebpf:"tracepoint__sched__sched_process_exit"` TracepointSyscallsSysEnterExecve *ebpf.Program `ebpf:"tracepoint__syscalls__sys_enter_execve"` TracepointSyscallsSysEnterOpen *ebpf.Program `ebpf:"tracepoint__syscalls__sys_enter_open"` @@ -184,6 +186,7 @@ type bpf_smallPrograms struct { func (p *bpf_smallPrograms) Close() error { return _Bpf_smallClose( + p.TracepointSchedSchedProcessExec, p.TracepointSchedSchedProcessExit, p.TracepointSyscallsSysEnterExecve, p.TracepointSyscallsSysEnterOpen, diff --git a/internal/probe/bpf_small_no_btf_arm64_bpfel.go b/internal/probe/bpf_small_no_btf_arm64_bpfel.go index 4b0f35e..cc08fcd 100644 --- a/internal/probe/bpf_small_no_btf_arm64_bpfel.go +++ b/internal/probe/bpf_small_no_btf_arm64_bpfel.go @@ -82,6 +82,7 @@ type bpf_small_no_btfSpecs struct { // // It can be passed ebpf.CollectionSpec.Assign. type bpf_small_no_btfProgramSpecs struct { + TracepointSchedSchedProcessExec *ebpf.ProgramSpec `ebpf:"tracepoint__sched__sched_process_exec"` TracepointSchedSchedProcessExit *ebpf.ProgramSpec `ebpf:"tracepoint__sched__sched_process_exit"` TracepointSchedSchedProcessFork *ebpf.ProgramSpec `ebpf:"tracepoint__sched__sched_process_fork"` TracepointSyscallsSysEnterExecve *ebpf.ProgramSpec `ebpf:"tracepoint__syscalls__sys_enter_execve"` @@ -170,6 +171,7 @@ type bpf_small_no_btfVariables struct { // // It can be passed to loadBpf_small_no_btfObjects or ebpf.CollectionSpec.LoadAndAssign. type bpf_small_no_btfPrograms struct { + TracepointSchedSchedProcessExec *ebpf.Program `ebpf:"tracepoint__sched__sched_process_exec"` TracepointSchedSchedProcessExit *ebpf.Program `ebpf:"tracepoint__sched__sched_process_exit"` TracepointSchedSchedProcessFork *ebpf.Program `ebpf:"tracepoint__sched__sched_process_fork"` TracepointSyscallsSysEnterExecve *ebpf.Program `ebpf:"tracepoint__syscalls__sys_enter_execve"` @@ -184,6 +186,7 @@ type bpf_small_no_btfPrograms struct { func (p *bpf_small_no_btfPrograms) Close() error { return _Bpf_small_no_btfClose( + p.TracepointSchedSchedProcessExec, p.TracepointSchedSchedProcessExit, p.TracepointSchedSchedProcessFork, p.TracepointSyscallsSysEnterExecve, diff --git a/internal/probe/bpf_small_no_btf_x86_bpfel.go b/internal/probe/bpf_small_no_btf_x86_bpfel.go index 8a7b7ba..1188b00 100644 --- a/internal/probe/bpf_small_no_btf_x86_bpfel.go +++ b/internal/probe/bpf_small_no_btf_x86_bpfel.go @@ -82,6 +82,7 @@ type bpf_small_no_btfSpecs struct { // // It can be passed ebpf.CollectionSpec.Assign. type bpf_small_no_btfProgramSpecs struct { + TracepointSchedSchedProcessExec *ebpf.ProgramSpec `ebpf:"tracepoint__sched__sched_process_exec"` TracepointSchedSchedProcessExit *ebpf.ProgramSpec `ebpf:"tracepoint__sched__sched_process_exit"` TracepointSchedSchedProcessFork *ebpf.ProgramSpec `ebpf:"tracepoint__sched__sched_process_fork"` TracepointSyscallsSysEnterExecve *ebpf.ProgramSpec `ebpf:"tracepoint__syscalls__sys_enter_execve"` @@ -170,6 +171,7 @@ type bpf_small_no_btfVariables struct { // // It can be passed to loadBpf_small_no_btfObjects or ebpf.CollectionSpec.LoadAndAssign. type bpf_small_no_btfPrograms struct { + TracepointSchedSchedProcessExec *ebpf.Program `ebpf:"tracepoint__sched__sched_process_exec"` TracepointSchedSchedProcessExit *ebpf.Program `ebpf:"tracepoint__sched__sched_process_exit"` TracepointSchedSchedProcessFork *ebpf.Program `ebpf:"tracepoint__sched__sched_process_fork"` TracepointSyscallsSysEnterExecve *ebpf.Program `ebpf:"tracepoint__syscalls__sys_enter_execve"` @@ -184,6 +186,7 @@ type bpf_small_no_btfPrograms struct { func (p *bpf_small_no_btfPrograms) Close() error { return _Bpf_small_no_btfClose( + p.TracepointSchedSchedProcessExec, p.TracepointSchedSchedProcessExit, p.TracepointSchedSchedProcessFork, p.TracepointSyscallsSysEnterExecve, diff --git a/internal/probe/bpf_small_x86_bpfel.go b/internal/probe/bpf_small_x86_bpfel.go index d1d7337..a1cf159 100644 --- a/internal/probe/bpf_small_x86_bpfel.go +++ b/internal/probe/bpf_small_x86_bpfel.go @@ -82,6 +82,7 @@ type bpf_smallSpecs struct { // // It can be passed ebpf.CollectionSpec.Assign. type bpf_smallProgramSpecs struct { + TracepointSchedSchedProcessExec *ebpf.ProgramSpec `ebpf:"tracepoint__sched__sched_process_exec"` TracepointSchedSchedProcessExit *ebpf.ProgramSpec `ebpf:"tracepoint__sched__sched_process_exit"` TracepointSyscallsSysEnterExecve *ebpf.ProgramSpec `ebpf:"tracepoint__syscalls__sys_enter_execve"` TracepointSyscallsSysEnterOpen *ebpf.ProgramSpec `ebpf:"tracepoint__syscalls__sys_enter_open"` @@ -170,6 +171,7 @@ type bpf_smallVariables struct { // // It can be passed to loadBpf_smallObjects or ebpf.CollectionSpec.LoadAndAssign. type bpf_smallPrograms struct { + TracepointSchedSchedProcessExec *ebpf.Program `ebpf:"tracepoint__sched__sched_process_exec"` TracepointSchedSchedProcessExit *ebpf.Program `ebpf:"tracepoint__sched__sched_process_exit"` TracepointSyscallsSysEnterExecve *ebpf.Program `ebpf:"tracepoint__syscalls__sys_enter_execve"` TracepointSyscallsSysEnterOpen *ebpf.Program `ebpf:"tracepoint__syscalls__sys_enter_open"` @@ -184,6 +186,7 @@ type bpf_smallPrograms struct { func (p *bpf_smallPrograms) Close() error { return _Bpf_smallClose( + p.TracepointSchedSchedProcessExec, p.TracepointSchedSchedProcessExit, p.TracepointSyscallsSysEnterExecve, p.TracepointSyscallsSysEnterOpen, diff --git a/internal/probe/bpf_x86_bpfel.go b/internal/probe/bpf_x86_bpfel.go index b4c7744..9853bab 100644 --- a/internal/probe/bpf_x86_bpfel.go +++ b/internal/probe/bpf_x86_bpfel.go @@ -82,6 +82,7 @@ type bpfSpecs struct { // // It can be passed ebpf.CollectionSpec.Assign. type bpfProgramSpecs struct { + TracepointSchedSchedProcessExec *ebpf.ProgramSpec `ebpf:"tracepoint__sched__sched_process_exec"` TracepointSchedSchedProcessExit *ebpf.ProgramSpec `ebpf:"tracepoint__sched__sched_process_exit"` TracepointSyscallsSysEnterExecve *ebpf.ProgramSpec `ebpf:"tracepoint__syscalls__sys_enter_execve"` TracepointSyscallsSysEnterOpen *ebpf.ProgramSpec `ebpf:"tracepoint__syscalls__sys_enter_open"` @@ -170,6 +171,7 @@ type bpfVariables struct { // // It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. type bpfPrograms struct { + TracepointSchedSchedProcessExec *ebpf.Program `ebpf:"tracepoint__sched__sched_process_exec"` TracepointSchedSchedProcessExit *ebpf.Program `ebpf:"tracepoint__sched__sched_process_exit"` TracepointSyscallsSysEnterExecve *ebpf.Program `ebpf:"tracepoint__syscalls__sys_enter_execve"` TracepointSyscallsSysEnterOpen *ebpf.Program `ebpf:"tracepoint__syscalls__sys_enter_open"` @@ -184,6 +186,7 @@ type bpfPrograms struct { func (p *bpfPrograms) Close() error { return _BpfClose( + p.TracepointSchedSchedProcessExec, p.TracepointSchedSchedProcessExit, p.TracepointSyscallsSysEnterExecve, p.TracepointSyscallsSysEnterOpen, diff --git a/internal/probe/ebpf/detector.bpf.c b/internal/probe/ebpf/detector.bpf.c index 00cd11f..61949c4 100644 --- a/internal/probe/ebpf/detector.bpf.c +++ b/internal/probe/ebpf/detector.bpf.c @@ -393,25 +393,13 @@ int tracepoint__syscalls__sys_enter_execve(struct syscall_trace_enter* ctx) { return 0; } -SEC("tracepoint/syscalls/sys_exit_execve") -int tracepoint__syscalls__sys_exit_execve(struct syscall_trace_exit* ctx) { - u64 pid_tgid = bpf_get_current_pid_tgid(); +static __always_inline int report_exec_event(void *ctx, u64 pid_tgid) { u32 pid = (u32)(pid_tgid & 0xFFFFFFFF); u32 tgid = (u32)(pid_tgid >> 32); pids_in_ns_t pids = {0}; long ret = 0; struct task_struct *task = NULL; - if (ctx->ret < 0) { - // exec failed - goto cleanup; - } - - u8 *exist = bpf_map_lookup_elem(&ongoing_exec_tgids, &tgid); - if (exist == NULL) { - return 0; - } - #ifdef NO_BTF pids.configured_ns_pid = pid; pids.last_level_pid = 0; @@ -419,18 +407,18 @@ int tracepoint__syscalls__sys_exit_execve(struct syscall_trace_exit* ctx) { task = (struct task_struct *)bpf_get_current_task(); ret = get_pid_for_configured_ns(task, &pids, pid); if (ret < 0) { - goto cleanup; + return ret; } #endif ret = bpf_map_update_elem(&tracked_pids_to_ns_pids, &pid, &pids.configured_ns_pid, BPF_ANY); if (ret != 0) { - goto cleanup; + return ret; } ret = bpf_map_update_elem(&user_pid_to_container_pid, &pids.configured_ns_pid, &pids.last_level_pid, BPF_ANY); if (ret != 0) { - goto cleanup; + return ret; } process_event_t event = { @@ -439,6 +427,34 @@ int tracepoint__syscalls__sys_exit_execve(struct syscall_trace_exit* ctx) { }; bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &event, sizeof(event)); + return 0; +} + +// this prove is only used as a fallback when the execve syscall are not present in the kernel, +// it is usually the case on older kernels (e.g RHEL7 which patches eBPF functionality). +SEC("tracepoint/sched/sched_process_exec") +int tracepoint__sched__sched_process_exec(struct trace_event_raw_sched_process_exec* ctx) { + u64 pid_tgid = bpf_get_current_pid_tgid(); + report_exec_event(ctx, pid_tgid); + return 0; +} + +SEC("tracepoint/syscalls/sys_exit_execve") +int tracepoint__syscalls__sys_exit_execve(struct syscall_trace_exit* ctx) { + u64 pid_tgid = bpf_get_current_pid_tgid(); + u32 tgid = (u32)(pid_tgid >> 32); + + if (ctx->ret < 0) { + // exec failed + goto cleanup; + } + + u8 *exist = bpf_map_lookup_elem(&ongoing_exec_tgids, &tgid); + if (exist == NULL) { + return 0; + } + + report_exec_event(ctx, pid_tgid); cleanup: bpf_map_delete_elem(&ongoing_exec_tgids, &tgid); diff --git a/internal/probe/probe.go b/internal/probe/probe.go index 99aa068..0b12d11 100644 --- a/internal/probe/probe.go +++ b/internal/probe/probe.go @@ -46,6 +46,9 @@ type Probe struct { // Once the probe is loaded, these PIDs will be written to the eBPF map and tracked properly. pendingPIDsToTrack []int mu sync.Mutex + + // for testing purposes, allowing to override the attach function. + attachTracepointFn func(group, name string, prog *ebpf.Program, opts *link.TracepointOptions) (link.Link, error) } type processEvent struct { @@ -62,6 +65,7 @@ const ( processForkNoBTFProgramName = "tracepoint__sched__sched_process_fork" processForkProgramName = "tracepoint_btf__sched__sched_process_fork" processExitProgramName = "tracepoint__sched__sched_process_exit" + processExecProgramName = "tracepoint__sched__sched_process_exec" pidToContainerPIDMapName = "user_pid_to_container_pid" envPrefixMapName = "env_prefix" @@ -97,11 +101,12 @@ type Config struct { func New(logger *slog.Logger, f common.ProcessesFilter, config Config) *Probe { return &Probe{ - logger: logger, - consumer: f, - envPrefixFilter: config.EnvPrefixFilter, - openFilesToTrack: config.OpenFilesToTrack, - execFilesToFilter: config.ExecFilesToFilter, + logger: logger, + consumer: f, + envPrefixFilter: config.EnvPrefixFilter, + openFilesToTrack: config.OpenFilesToTrack, + execFilesToFilter: config.ExecFilesToFilter, + attachTracepointFn: link.Tracepoint, } } @@ -278,25 +283,48 @@ func (p *Probe) attach() error { return errors.New("no eBPF collection loaded") } + if p.attachTracepointFn == nil { + p.attachTracepointFn = link.Tracepoint + } + reader, err := perf.NewReader(p.c.Maps[eventsMapName], PerfBufferDefaultSizeInPages*os.Getpagesize()) if err != nil { return fmt.Errorf("can't create perf reader: %w", err) } p.reader = reader - l, err := link.Tracepoint("syscalls", "sys_enter_execve", p.c.Programs[execveSyscallProgramName], nil) + // try to attach to the syscall entry point of execve first + l, err := p.attachTracepointFn("syscalls", "sys_enter_execve", p.c.Programs[execveSyscallProgramName], nil) if err != nil { - return fmt.Errorf("can't attach probe sys_enter_execve: %w", err) - } - p.links = append(p.links, l) - - l, err = link.Tracepoint("syscalls", "sys_exit_execve", p.c.Programs[execveSyscallExitProgramName], nil) - if err != nil { - return fmt.Errorf("can't attach probe sys_exit_execve: %w", err) + if !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("can't attach probe sys_enter_execve: %w", err) + } + // as a fallback for old kernels that don't have the sys_enter_execve tracepoint, try to attach to sched_process_exec tracepoint + // which is available in older kernels. this comes with the limitation that we won't be able to apply the env-prefix filter for exec events. + fallbackLink, fallbackErr := p.attachTracepointFn("sched", "sched_process_exec", p.c.Programs[processExecProgramName], nil) + if fallbackErr != nil { + return fmt.Errorf("can't attach probe to sys_enter_execve: %w, also failed to attach to sched_process_exec: %w", err, fallbackErr) + } + p.logger.Warn( + "syscalls/sys_enter_execve tracepoint is unavailable on this kernel; "+ + "falling back to the sched/sched_process_exec tracepoint for exec detection. "+ + "In this mode the eBPF program cannot pre-filter execs by env-prefix, so every process "+ + "exec is forwarded to user space (higher overhead), and fork/file-open events are not "+ + "env-prefix filtered.", + "envPrefixFilter", p.envPrefixFilter, + ) + p.links = append(p.links, fallbackLink) + } else { + // if we managed to attach to the entry point of execve, we need to attach to its return point as well. + p.links = append(p.links, l) + l, err = p.attachTracepointFn("syscalls", "sys_exit_execve", p.c.Programs[execveSyscallExitProgramName], nil) + if err != nil { + return fmt.Errorf("can't attach probe sys_exit_execve: %w", err) + } + p.links = append(p.links, l) } - p.links = append(p.links, l) - l, err = link.Tracepoint("sched", "sched_process_exit", p.c.Programs[processExitProgramName], nil) + l, err = p.attachTracepointFn("sched", "sched_process_exit", p.c.Programs[processExitProgramName], nil) if err != nil { return fmt.Errorf("can't attach probe sched_process_exit: %w", err) } @@ -320,7 +348,7 @@ func (p *Probe) attach() error { return errors.New("sched_process_fork program not found") } - l, err = link.Tracepoint("sched", "sched_process_fork", prog, nil) + l, err = p.attachTracepointFn("sched", "sched_process_fork", prog, nil) if err != nil { return fmt.Errorf("can't attach probe sched_process_fork (no BTF): %w", err) } @@ -344,12 +372,12 @@ func (p *Probe) attachOpenPrograms() error { // attach to open syscall // open() is not present on arm64 if runtime.GOARCH != "arm64" { - l, err = link.Tracepoint("syscalls", "sys_enter_open", p.c.Programs[openSyscallProgramName], nil) + l, err = p.attachTracepointFn("syscalls", "sys_enter_open", p.c.Programs[openSyscallProgramName], nil) if err != nil { return fmt.Errorf("can't attach probe sys_enter_open: %w", err) } p.links = append(p.links, l) - l, err = link.Tracepoint("syscalls", "sys_exit_open", p.c.Programs[openSyscallExitProgramName], nil) + l, err = p.attachTracepointFn("syscalls", "sys_exit_open", p.c.Programs[openSyscallExitProgramName], nil) if err != nil { return fmt.Errorf("can't attach probe sys_exit_open: %w", err) } @@ -357,12 +385,12 @@ func (p *Probe) attachOpenPrograms() error { } // attach to openat syscall - l, err = link.Tracepoint("syscalls", "sys_enter_openat", p.c.Programs[openatSyscallProgramName], nil) + l, err = p.attachTracepointFn("syscalls", "sys_enter_openat", p.c.Programs[openatSyscallProgramName], nil) if err != nil { return fmt.Errorf("can't attach probe sys_enter_openat: %w", err) } p.links = append(p.links, l) - l, err = link.Tracepoint("syscalls", "sys_exit_openat", p.c.Programs[openatSyscallExitProgramName], nil) + l, err = p.attachTracepointFn("syscalls", "sys_exit_openat", p.c.Programs[openatSyscallExitProgramName], nil) if err != nil { return fmt.Errorf("can't attach probe sys_exit_openat: %w", err) } @@ -370,13 +398,13 @@ func (p *Probe) attachOpenPrograms() error { // attach to openat2 syscall which is optional here // since linux v5.5 support openat2(2), commit fddb5d430ad9 ("open: introduce openat2(2) syscall") - l, err = link.Tracepoint("syscalls", "sys_enter_openat2", p.c.Programs[openat2SyscallProgramName], nil) + l, err = p.attachTracepointFn("syscalls", "sys_enter_openat2", p.c.Programs[openat2SyscallProgramName], nil) if err != nil { p.logger.Debug("failed to attch openat2 tracepoint") return nil } p.links = append(p.links, l) - l, err = link.Tracepoint("syscalls", "sys_exit_openat2", p.c.Programs[openat2SyscallExitProgramName], nil) + l, err = p.attachTracepointFn("syscalls", "sys_exit_openat2", p.c.Programs[openat2SyscallExitProgramName], nil) if err != nil { p.logger.Debug("failed to attch openat2 tracepoint") } diff --git a/internal/probe/probe_test.go b/internal/probe/probe_test.go index de160c9..246d44d 100644 --- a/internal/probe/probe_test.go +++ b/internal/probe/probe_test.go @@ -1,15 +1,21 @@ package probe import ( + "context" "log/slog" "os" "os/exec" + "sync" "syscall" "testing" "time" + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/link" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/odigos-io/runtime-detector/internal/common" ) func repeatedString(length int, s string) string { @@ -321,3 +327,95 @@ func assertMapsAreEmpty(t *testing.T, p *Probe) { assert.NoError(t, iterator.Err()) } } + +// recordingConsumer is a common.ProcessesFilter that records every event it +// receives, used by tests to assert which events the probe reported. +type recordingConsumer struct { + mu sync.Mutex + events []common.PIDEvent +} + +func (c *recordingConsumer) Add(pid int, eventType common.EventType) { + c.mu.Lock() + defer c.mu.Unlock() + c.events = append(c.events, common.PIDEvent{Pid: pid, Type: eventType}) +} + +func (c *recordingConsumer) Remove(pid int) { + c.mu.Lock() + defer c.mu.Unlock() + // ReadEvents calls Remove for exit events, record it as such. + c.events = append(c.events, common.PIDEvent{Pid: pid, Type: common.EventTypeExit}) +} + +func (c *recordingConsumer) Close() error { return nil } + +func (c *recordingConsumer) has(pid int, eventType common.EventType) bool { + c.mu.Lock() + defer c.mu.Unlock() + for _, e := range c.events { + if e.Pid == pid && e.Type == eventType { + return true + } + } + return false +} + +func TestExecFallbackToSchedProcessExec(t *testing.T) { + sleepPath, err := exec.LookPath("sleep") + if err != nil { + t.Skip("sleep not found in PATH, skipping") + } + + consumer := &recordingConsumer{} + + var ( + enterExecveAttempted bool + schedExecAttached bool + ) + + p := &Probe{ + logger: slog.Default(), + consumer: consumer, + // Force the fallback path: pretend the kernel is missing the + // syscalls/sys_enter_execve tracepoint, while delegating every other + // attach to the real link.Tracepoint. + attachTracepointFn: func(group, name string, prog *ebpf.Program, opts *link.TracepointOptions) (link.Link, error) { + if group == "syscalls" && name == "sys_enter_execve" { + enterExecveAttempted = true + return nil, os.ErrNotExist + } + if group == "sched" && name == "sched_process_exec" { + schedExecAttached = true + } + return link.Tracepoint(group, name, prog, opts) + }, + } + + require.NoError(t, p.load(0)) + defer p.Close() + require.NoError(t, p.attach()) + + require.True(t, enterExecveAttempted, "expected an attempt to attach sys_enter_execve") + require.True(t, schedExecAttached, "expected the fallback sched_process_exec tracepoint to be attached") + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + go p.ReadEvents(ctx) + + cmd := exec.Command(sleepPath, "1") + require.NoError(t, cmd.Start()) + pid := cmd.Process.Pid + require.NoError(t, cmd.Wait()) + + assert.Eventually(t, func() bool { + return consumer.has(pid, common.EventTypeExec) + }, 5*time.Second, 20*time.Millisecond, + "expected an exec event for pid %d via the sched_process_exec fallback", pid) + + assert.Eventually(t, func() bool { + return consumer.has(pid, common.EventTypeExit) + }, 5*time.Second, 20*time.Millisecond, + "expected an exit event for pid %d via the sched_process_exec fallback", pid) +}