diff --git a/bpf/tests/Makefile b/bpf/tests/Makefile index 53447ee06ca..7c9db843f69 100644 --- a/bpf/tests/Makefile +++ b/bpf/tests/Makefile @@ -1,6 +1,6 @@ include ../Makefile.defs -TESTS = prepend_name_test.o +TESTS = prepend_name_test.o pid_match_test.o OBJSDIR := objs/ OBJS := $(addprefix $(OBJSDIR),$(TESTS)) diff --git a/bpf/tests/pid_match_test.c b/bpf/tests/pid_match_test.c new file mode 100644 index 00000000000..2848894683a --- /dev/null +++ b/bpf/tests/pid_match_test.c @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +/* Copyright Authors of Cilium */ + +//go:build ignore + +#include "vmlinux.h" +#include "compiler.h" +#include "bpf_core_read.h" +#include "bpf_helpers.h" +#include "process/retprobe_map.h" +#include "process/types/basic.h" +#include "process/pfilter.h" + +char _license[] __attribute__((section("license"), used)) = "Dual BSD/GPL"; + +struct filter_map_value { + unsigned char buf[FILTER_SIZE]; +}; + +struct { + __uint(type, BPF_MAP_TYPE_ARRAY); + __uint(max_entries, 1); + __type(key, int); + __type(value, struct filter_map_value); +} test_filter_map SEC(".maps"); + +__attribute__((section("raw_tracepoint/test"), used)) int +test_pid_match() +{ + __u32 *f; + int zero = 0, index = 0; + struct pid_filter *pid; + struct execve_map_value *enter; + + f = map_lookup_elem(&test_filter_map, &zero); + if (!f) + return 0; + + pid = (struct pid_filter *)((u64)f + index); + index += sizeof(struct pid_filter); + + enter = map_lookup_elem(&execve_map, &zero); + if (!enter) + return 0; + + struct selector_filter sel = { + .index = index, + .ty = pid->op, + .flags = pid->flags, + .len = pid->len, + }; + + return selector_match(f, &sel, enter, NULL, &process_filter_pid); +} diff --git a/bpf/tests/pid_match_test.go b/bpf/tests/pid_match_test.go new file mode 100644 index 00000000000..a8f38dfc320 --- /dev/null +++ b/bpf/tests/pid_match_test.go @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Tetragon + +package bpf + +import ( + "errors" + "fmt" + "testing" + + "github.com/cilium/ebpf" + "github.com/cilium/tetragon/pkg/api/processapi" + "github.com/cilium/tetragon/pkg/k8s/apis/cilium.io/v1alpha1" + "github.com/cilium/tetragon/pkg/selectors" + "github.com/cilium/tetragon/pkg/sensors/exec/execvemap" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const programNamePidMatch = "test_pid_match" + +type testContext struct { + coll *ebpf.Collection + prog *ebpf.Program + execvMap *ebpf.Map +} + +// setupTest loads the test program and returns a test context. +func setupTest(t *testing.T) (*testContext, error) { + ctx := &testContext{} + // load test program + coll, err := ebpf.LoadCollection("objs/pid_match_test.o") + if err != nil { + var ve *ebpf.VerifierError + if errors.As(err, &ve) { + return nil, fmt.Errorf("verifier error: %+v", ve) + } + return nil, err + } + ctx.coll = coll + + var ok bool + ctx.prog, ok = coll.Programs[programNamePidMatch] + require.True(t, ok, "program %s not found", programNamePidMatch) + + ctx.execvMap, ok = coll.Maps["execve_map"] + require.True(t, ok, "execve_map not found") + + return ctx, nil +} + +func (ctx *testContext) cleanup() { + if ctx.coll != nil { + ctx.coll.Close() + } +} + +// runProg runs the test program with the given PID and returns the result. +func (ctx *testContext) runProg(selfPid uint32) (uint32, error) { + err := ctx.execvMap.Update(uint32(0), &execvemap.ExecveValue{ + Process: processapi.MsgExecveKey{Pid: selfPid}, + }, 0) + if err != nil { + return 0, fmt.Errorf("failed to update execve map: %w", err) + } + res, err := ctx.prog.Run(&ebpf.RunOptions{}) + if err != nil { + return 0, fmt.Errorf("failed to run program: %w", err) + } + return res, nil +} + +// initKernelStateData initializes the kernel state data with the given PIDs. +func (ctx *testContext) initKernelStateData(pids []uint32) error { + k := &selectors.KernelSelectorState{} + err := selectors.ParseMatchPid(k, &v1alpha1.PIDSelector{ + Operator: "In", + Values: pids, + IsNamespacePID: false, + FollowForks: false, + }) + if err != nil { + return fmt.Errorf("failed to parse PID selector: %w", err) + } + + filterMap, ok := ctx.coll.Maps["test_filter_map"] + if !ok { + return errors.New("test_filter_map not found") + } + + return filterMap.Update(uint32(0), k.Buffer(), 0) +} + +func Test_PidMatch(t *testing.T) { + ctx, err := setupTest(t) + require.NoError(t, err) + defer ctx.cleanup() + + tests := []struct { + name string + pids []uint32 + testPid uint32 + expected uint32 + }{ + // Test case where the test PID is the only PID to match. + { + name: "Match_1_PID", + pids: []uint32{1}, + testPid: 1, + expected: 1, + }, + // Test case where the test PID is in the list of 2 PIDs to match. + { + name: "Match_2_PID", + pids: []uint32{1, 2}, + testPid: 2, + expected: 1, + }, + // Test case where the test PID is not in the list of 2 PIDs to match. + { + name: "Match_2_PID_NOT_IN_LIST", + pids: []uint32{1, 2}, + testPid: 3, + expected: 0, + }, + // Test case where the test PID is in the list of 4 PIDs to match. + { + name: "Match_4_PID", + pids: []uint32{1, 2, 3, 4}, + testPid: 4, + expected: 1, + }, + // Test case where the test PID is not in the list of 4 PIDs to match. + { + name: "Match_4_PID_NOT_IN_LIST", + pids: []uint32{1, 2, 3, 4}, + testPid: 5, + expected: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.NoError(t, ctx.initKernelStateData(tt.pids)) + result, err := ctx.runProg(tt.testPid) + require.NoError(t, err) + assert.Equal(t, tt.expected, result) + }) + } +}