Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ TESTS_BIN_DIR = test/bin
FILE_OPEN_PROG_BIN = ${TESTS_BIN_DIR}/file_open
THREAD_EXEC_PROG_BIN = ${TESTS_BIN_DIR}/thread_exec
THREAD_FORK_PROG_BIN = ${TESTS_BIN_DIR}/thread_fork
SIGNAL_FORK_PROG_BIN = ${TESTS_BIN_DIR}/signal_fork
BASE_IMAGE = keyval/odiglet-base:v1.10

GOLANGCI_LINT_VERSION := v2.7.2
Expand Down Expand Up @@ -44,7 +45,7 @@ generate:
docker-generate:
docker run --rm -v $(shell pwd):/app $(BASE_IMAGE) /bin/sh -c "cd ../app && make generate"

compile-c-tests: $(FILE_OPEN_PROG_BIN) $(THREAD_EXEC_PROG_BIN) $(THREAD_FORK_PROG_BIN)
compile-c-tests: $(FILE_OPEN_PROG_BIN) $(THREAD_EXEC_PROG_BIN) $(THREAD_FORK_PROG_BIN) $(SIGNAL_FORK_PROG_BIN)

$(FILE_OPEN_PROG_BIN): test/c_processes/file_open.c | $(TESTS_BIN_DIR)
gcc test/c_processes/file_open.c -o $(FILE_OPEN_PROG_BIN)
Expand All @@ -55,6 +56,9 @@ $(THREAD_EXEC_PROG_BIN): test/c_processes/thread_exec.c | $(TESTS_BIN_DIR)
$(THREAD_FORK_PROG_BIN): test/c_processes/thread_fork.c | $(TESTS_BIN_DIR)
gcc test/c_processes/thread_fork.c -o $(THREAD_FORK_PROG_BIN) -lpthread

$(SIGNAL_FORK_PROG_BIN): test/c_processes/signal_fork.c | $(TESTS_BIN_DIR)
gcc test/c_processes/signal_fork.c -o $(SIGNAL_FORK_PROG_BIN)

.PHONY: docker-test-debian docker-test-alpine
docker-test:
docker run --rm \
Expand Down
91 changes: 91 additions & 0 deletions detector_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package detector

import (
"bufio"
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"slices"
"strings"
"syscall"
"testing"
"time"

Expand Down Expand Up @@ -513,6 +515,95 @@ func TestDetectorInitialScan(t *testing.T) {
}
}

func TestSignalFork(t *testing.T) {
currentDir, err := os.Getwd()
require.NoError(t, err)

signalForkBin := filepath.Join(currentDir, "test/bin/signal_fork")

events := make(chan ProcessEvent, 100)

// Start the target process BEFORE the detector.
cmd := exec.Command(signalForkBin)
cmd.Env = append(os.Environ(), "USER_ENV=value")

stdout, err := cmd.StdoutPipe()
require.NoError(t, err)

var stderr strings.Builder
cmd.Stderr = &stderr

err = cmd.Start()
require.NoError(t, err)
defer func() {
_ = cmd.Process.Signal(syscall.SIGTERM)
_ = cmd.Wait()
}()

// Wait for the process to signal readiness.
scanner := bufio.NewScanner(stdout)
require.True(t, scanner.Scan(), "expected 'ready' line from signal_fork")
require.Equal(t, "ready", scanner.Text())

// Now start the detector — this triggers the initial scan and should pick up
// the already-running signal_fork process.
opts := []DetectorOption{
WithMinDuration(0),
WithExePathsToFilter(bashLocation),
WithEnvironments("USER_ENV"),
WithEnvPrefixFilter("USER_E"),
}

d, err := NewDetector(events, opts...)
require.NoError(t, err)

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

go func() {
err := d.Run(ctx)
require.NoError(t, err)
}()

// Collect the initial exec event from the initial scan.
var execEvent ProcessEvent
select {
case execEvent = <-events:
case <-time.After(3 * time.Second):
t.Fatal("timed out waiting for initial exec event")
}
assert.Equal(t, ProcessExecEvent.String(), execEvent.EventType.String(), "first event should be exec")
assert.Equal(t, signalForkBin, execEvent.ExecDetails.ExePath)
t.Logf("got exec event for pid %d\n", execEvent.PID)

time.Sleep(time.Second)
// Send SIGUSR1 to trigger a fork inside the target process.
err = cmd.Process.Signal(syscall.SIGUSR1)
require.NoError(t, err)

// Collect fork event
var forkEvent ProcessEvent
select {
case forkEvent = <-events:
case <-time.After(3 * time.Second):
t.Fatal("timed out waiting fork event")
}
assert.Equal(t, ProcessForkEvent.String(), forkEvent.EventType.String(), "second event should be fork")

// Send SIGTERM to trigger exit inside the target process.
err = cmd.Process.Signal(syscall.SIGTERM)
require.NoError(t, err)

// Collect exit event
var exitEvent ProcessEvent
select {
case exitEvent = <-events:
case <-time.After(3 * time.Second):
t.Fatal("timed out waiting for exit event")
}
assert.Equal(t, ProcessExitEvent.String(), exitEvent.EventType.String(), "third event should be exit")
}

func envVarsToSlice(envVars map[string]string) []string {
var result []string
for k, v := range envVars {
Expand Down
33 changes: 24 additions & 9 deletions internal/probe/ebpf/detector.bpf.c
Original file line number Diff line number Diff line change
Expand Up @@ -463,35 +463,44 @@ int BPF_PROG(tracepoint_btf__sched__sched_process_fork, struct task_struct *pare
}

// filter only relevant pids based on the parent
// check if that this clone/fork is called from a process we are tracking
// since fork can be called by a non-leader thread, we need to check the parent tgid in the map
void *found = bpf_map_lookup_elem(&tracked_pids_to_ns_pids, &parent_tgid);
// check if that this clone/fork is called from a process we are tracking.
// since fork can be called by a non-leader thread, we must use the group leader's task
// since the execve probe updates the map based on tgid.
// In addition, user-space can write to user_pid_to_container_pid map in order to tell us to track this process.
// hence we need to look based on that map, otherwise we might miss the event.
struct task_struct *parent_leader = BPF_CORE_READ(parent, group_leader);
pids_in_ns_t parent_pids = {0};
ret_code = get_pid_for_configured_ns(parent_leader, &parent_pids, parent_tgid);
if (ret_code < 0) {
return 0;
}
void *found = bpf_map_lookup_elem(&user_pid_to_container_pid, &parent_pids.configured_ns_pid);
if (found == NULL) {
return 0;
}

pids_in_ns_t pids = {0};
pids_in_ns_t child_pids = {0};

ret_code = get_pid_for_configured_ns(child, &pids, child_tgid);
ret_code = get_pid_for_configured_ns(child, &child_pids, child_tgid);
if (ret_code < 0) {
return 0;
}

// track this child pid
ret_code = bpf_map_update_elem(&tracked_pids_to_ns_pids, &child_tgid, &pids.configured_ns_pid, BPF_ANY);
ret_code = bpf_map_update_elem(&tracked_pids_to_ns_pids, &child_tgid, &child_pids.configured_ns_pid, BPF_ANY);
if (ret_code != 0) {
return 0;
}

// populate the map with the container pid, so that user space can read it
ret_code = bpf_map_update_elem(&user_pid_to_container_pid, &pids.configured_ns_pid, &pids.last_level_pid, BPF_ANY);
ret_code = bpf_map_update_elem(&user_pid_to_container_pid, &child_pids.configured_ns_pid, &child_pids.last_level_pid, BPF_ANY);
if (ret_code != 0) {
return 0;
}

process_event_t event = {
.type = PROCESS_FORK,
.pid = pids.configured_ns_pid,
.pid = child_pids.configured_ns_pid,
};

bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &event, sizeof(event));
Expand All @@ -507,7 +516,13 @@ int tracepoint__sched__sched_process_fork(struct trace_event_raw_sched_process_f
// check if that this clone/fork is called from a process we are tracking (went through execve)
void *found = bpf_map_lookup_elem(&tracked_pids_to_ns_pids, &parent_pid);
if (found == NULL) {
return 0;
// fallback to try the user_pid map, this can be populated if the user call TrackProcesses
// for an already running process before the execve probe was attached
// this will only work if user space is running in the host pid namespace
found = bpf_map_lookup_elem(&user_pid_to_container_pid, &parent_pid);
if (found == NULL) {
return 0;
}
}

// we can't make sure here that the child pid is a new process, and not a thread.
Expand Down
53 changes: 53 additions & 0 deletions test/c_processes/signal_fork.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* signal_fork - a process that forks in response to SIGUSR1.
* Exits cleanly on SIGTERM.
*/
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

static volatile sig_atomic_t got_term = 0;
static volatile sig_atomic_t got_usr1 = 0;

static void handle_usr1(int sig) {
got_usr1 = 1;
}

static void handle_term(int sig) {
got_term = 1;
}

int main(void) {
struct sigaction sa1 = { .sa_handler = handle_usr1 };
struct sigaction sa_term = { .sa_handler = handle_term };
sigaction(SIGUSR1, &sa1, NULL);
sigaction(SIGTERM, &sa_term, NULL);

/* Signal readiness by writing to stdout. */
printf("ready\n");
fflush(stdout);

while (!got_term) {
// pause() causes the calling process (or thread) to sleep until a
// signal is delivered that either terminates the process or causes
// the invocation of a signal-catching function
pause();

if (got_usr1) {
got_usr1 = 0;
pid_t pid = fork();
if (pid == 0) {
/* Child: sleep briefly so the detector can observe it. */
sleep(1);
_exit(0);
}
/* Parent: reap the child. */
if (pid > 0)
waitpid(pid, NULL, 0);
}
}

return 0;
}
2 changes: 1 addition & 1 deletion test/script.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
echo "Running bash script"

for i in {1..10}; do
sleep 0.08
sleep 0.04
done

echo "Bash script completed"