Skip to content

Commit

Permalink
draft implem of BPF unit tests
Browse files Browse the repository at this point in the history
Signed-off-by: Mahe Tardy <mahe.tardy@gmail.com>
  • Loading branch information
mtardy committed Dec 15, 2023
1 parent 26a647f commit 7aeea29
Show file tree
Hide file tree
Showing 3 changed files with 226 additions and 1 deletion.
2 changes: 1 addition & 1 deletion bpf/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down
58 changes: 58 additions & 0 deletions bpf/test/bpf_prepend_name_test.c
Original file line number Diff line number Diff line change
@@ -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;
}
167 changes: 167 additions & 0 deletions tests/bpf/prepend_name_test.go
Original file line number Diff line number Diff line change
@@ -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()
})

}

0 comments on commit 7aeea29

Please sign in to comment.