diff --git a/bpf/Makefile b/bpf/Makefile index 0301a64cb8c..4a62ea9e881 100644 --- a/bpf/Makefile +++ b/bpf/Makefile @@ -38,7 +38,7 @@ PROCESS = bpf_execve_event.o bpf_execve_event_v53.o bpf_fork.o bpf_exit.o bpf_ge bpf_killer.o bpf_multi_killer.o CGROUP = bpf_cgroup_mkdir.o bpf_cgroup_rmdir.o bpf_cgroup_release.o -BPFTEST = bpf_lseek.o bpf_globals.o +BPFTEST = bpf_lseek.o bpf_globals.o bpf_prepend_name_test.o IDIR = ./include/ LIBBPF = ./libbpf/ diff --git a/bpf/test/bpf_prepend_name_test.c b/bpf/test/bpf_prepend_name_test.c new file mode 100644 index 00000000000..257d0a0c99c --- /dev/null +++ b/bpf/test/bpf_prepend_name_test.c @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +/* Copyright Authors of Cilium */ + +#include "vmlinux.h" + +#include "bpf_tracing.h" // bpf_printk + +#include "bpf_task.h" +#include "process/bpf_process_event.h" + +char _license[] __attribute__((section("license"), used)) = "Dual BSD/GPL"; + +#define MAX_BUF_LEN 4096 + +struct test_prepend_name_state_map_value { + char buf[MAX_BUF_LEN]; + u64 buflen; + char dname[MAX_BUF_LEN]; + u32 dlen; + u32 offset; +}; + +struct { + __uint(type, BPF_MAP_TYPE_ARRAY); + __uint(max_entries, 1); + __type(key, int); + __type(value, struct test_prepend_name_state_map_value); +} test_prepend_name_state_map SEC(".maps"); + +__attribute__((section("raw_tracepoint/test"), used)) int +test_prepend_name() +{ + struct test_prepend_name_state_map_value *ts; + int zero = 0; + + ts = map_lookup_elem(&test_prepend_name_state_map, &zero); + if (!ts) + return 1; + + if (ts->buflen < 0 || ts->buflen > 256) + return 2; + + char *bufptr = ts->buf + ts->buflen; + + // bpf_printk("buf: 0x%x, bufptr: 0x%x", ts->buf, bufptr); + // bpf_printk("buffer: %s", ts->buf); + // bpf_printk("buflen: %d", ts->buflen); + // bpf_printk("dentry->name: %s", ts->dname); + // bpf_printk("dentry->len: %d", ts->dlen); + + ts->dlen &= 255; + + int ret = prepend_name((char *) &ts->buf, &bufptr, (int *) &ts->buflen, ts->dname, ts->dlen); + + ts->offset = bufptr - (char *)&ts->buf; + + return ret; +} diff --git a/tests/bpf/prepend_name_test.go b/tests/bpf/prepend_name_test.go new file mode 100644 index 00000000000..f53ddfbfc95 --- /dev/null +++ b/tests/bpf/prepend_name_test.go @@ -0,0 +1,167 @@ +package bpf + +import ( + "bytes" + "errors" + "slices" + "strings" + "testing" + + "github.com/cilium/ebpf" + "github.com/cilium/tetragon/pkg/testutils" + "github.com/stretchr/testify/assert" +) + +const ( + // those constants must be synchronized with the BPF code + MAX_BUF_LEN = 4096 + testPrependNameStateMapName = "test_prepend_name_state_map" + programName = "test_prepend_name" + + bufLen = 10 +) + +var ( + zero uint32 = 0 +) + +type testPrependNameStateMapValue struct { + Buf [MAX_BUF_LEN]byte + Buflen uint64 + Dname [MAX_BUF_LEN]byte + Dlen uint32 + Offset uint32 +} + +func updateDentry(t *testing.T, stateMap *ebpf.Map, dentry string) { + var state testPrependNameStateMapValue + + err := stateMap.Lookup(&zero, &state) + if err != nil { + t.Fatal(err) + } + + dentryName := [MAX_BUF_LEN]byte{} + length := copy(dentryName[:], []byte(dentry)) + if length != len(dentry) { + t.Fatalf("dentry buffer is too small for string: %s", dentry) + } + + state.Dname = dentryName + state.Dlen = uint32(length) + + err = stateMap.Update(&zero, &state, ebpf.UpdateAny) + if err != nil { + t.Fatal(err) + } +} + +func Test_PrependName(t *testing.T) { + // load test program + coll, err := ebpf.LoadCollection(testutils.RepoRootPath("bpf/objs/bpf_prepend_name_test.o")) + if err != nil { + var ve *ebpf.VerifierError + if errors.As(err, &ve) { + t.Logf("Verifier error: %+v\n", ve) + } + t.Fatal(err) + } + defer coll.Close() + + // get ref to objects + prog, ok := coll.Programs[programName] + if !ok { + t.Fatalf("%s not found", programName) + } + stateMap := coll.Maps[testPrependNameStateMapName] + if stateMap == nil { + t.Fatalf("%s not found", testPrependNameStateMapName) + } + var state testPrependNameStateMapValue + + // reset the test state map + resetState := func(buflen int) { + state = testPrependNameStateMapValue{ + Buf: [MAX_BUF_LEN]byte{}, + Buflen: uint64(buflen), + Dname: [MAX_BUF_LEN]byte{}, + Dlen: 0, + } + err = stateMap.Update(&zero, &state, ebpf.UpdateAny) + if err != nil { + t.Fatal(err) + } + } + + // runPrependName BPF code + runPrependName := func() int { + code, err := prog.Run(&ebpf.RunOptions{}) + if err != nil { + t.Fatal(err) + } + return int(code) + } + + // simulate a dentry walk on path + testPath := func(path string) { + // simulate dentries walk + dentries := strings.Split(path, "/") + if len(dentries) > 0 && strings.HasPrefix(path, "/") { + dentries = dentries[1:] + } + slices.Reverse(dentries) // walk from local to root + for _, dentry := range dentries { + // update dentry + updateDentry(t, stateMap, dentry) + + // run prepend_name + code := runPrependName() + if code != 0 { + t.Fatalf("unexpected return code: %d", code) + } + } + } + + // this should be a method on state I guess or the buffer + bufferToString := func() string { + err = stateMap.Lookup(&zero, &state) + if err != nil { + t.Fatal(err) + } + return string(bytes.TrimRight(state.Buf[state.Offset:], "\x00")) + } + + logBuffer := func() { + err = stateMap.Lookup(&zero, &state) + if err != nil { + t.Fatal(err) + } + t.Log(string(state.Buf[:])) + } + + t.Run("ExactBufferSize", func(t *testing.T) { + resetState(len("/usr/bin/cat")) + + updateDentry(t, stateMap, "cat") + code := runPrependName() + assert.Equal(t, 0, code) + assert.Equal(t, "/cat", bufferToString()) + + updateDentry(t, stateMap, "bin") + code = runPrependName() + assert.Equal(t, 0, code) + assert.Equal(t, "/bin/cat", bufferToString()) + + updateDentry(t, stateMap, "usr") + code = runPrependName() + assert.Equal(t, 0, code) + assert.Equal(t, "/usr/bin/cat", bufferToString()) + }) + + t.Run("BufferTooSmall", func(t *testing.T) { + resetState(10) + testPath("/home/mahe/recipes/pizza") + logBuffer() + }) + +}