From 466e18072d999251f3aacbb246007367ff263246 Mon Sep 17 00:00:00 2001 From: sc07kvm Date: Tue, 4 Jun 2024 13:25:10 -0300 Subject: [PATCH] feat: Add method Run to BPFProg MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Run method is used to load and run the BPF program without attaching it to any hook. This is useful for testing the BPF program. It makes use of the bpf(BPF_PROG_TEST_RUN) via bpf_prog_test_run_opts(). The prog-run selftest is added to test the Run method. More examples can be found in the linux kernel source code: tools/testing/selftests/bpf/prog_tests. Original code #428 was authored by sc07kvm, and this commiter have only made cosmetics adjustments. Co-authored-by: Geyslan Gregório --- libbpfgo.c | 43 ++++++++++ libbpfgo.h | 14 ++++ prog.go | 158 +++++++++++++++++++++++++++++++++++ selftest/common/run-4.12.sh | 22 +++++ selftest/prog-run/Makefile | 1 + selftest/prog-run/go.mod | 7 ++ selftest/prog-run/go.sum | 8 ++ selftest/prog-run/main.bpf.c | 27 ++++++ selftest/prog-run/main.go | 52 ++++++++++++ selftest/prog-run/run.sh | 1 + 10 files changed, 333 insertions(+) create mode 100755 selftest/common/run-4.12.sh create mode 120000 selftest/prog-run/Makefile create mode 100644 selftest/prog-run/go.mod create mode 100644 selftest/prog-run/go.sum create mode 100644 selftest/prog-run/main.bpf.c create mode 100644 selftest/prog-run/main.go create mode 120000 selftest/prog-run/run.sh diff --git a/libbpfgo.c b/libbpfgo.c index 053711e1..d347865b 100644 --- a/libbpfgo.c +++ b/libbpfgo.c @@ -165,6 +165,49 @@ void cgo_bpf_iter_attach_opts_free(struct bpf_iter_attach_opts *opts) free(opts); } +struct bpf_test_run_opts *cgo_bpf_test_run_opts_new(const void *data_in, + void *data_out, + __u32 data_size_in, + __u32 data_size_out, + const void *ctx_in, + void *ctx_out, + __u32 ctx_size_in, + __u32 ctx_size_out, + int repeat, + __u32 flags, + __u32 cpu, + __u32 batch_size) +{ + struct bpf_test_run_opts *opts; + opts = calloc(1, sizeof(*opts)); + if (!opts) + return NULL; + + opts->sz = sizeof(*opts); + opts->data_in = data_in; + opts->data_out = data_out; + opts->data_size_in = data_size_in; + opts->data_size_out = data_size_out; + opts->ctx_in = ctx_in; + opts->ctx_out = ctx_out; + opts->ctx_size_in = ctx_size_in; + opts->ctx_size_out = ctx_size_out; + opts->repeat = repeat; + opts->flags = flags; + opts->cpu = cpu; + opts->batch_size = batch_size; + + return opts; +} + +void cgo_bpf_test_run_opts_free(struct bpf_test_run_opts *opts) +{ + if (!opts) + return; + + free(opts); +} + struct bpf_object_open_opts *cgo_bpf_object_open_opts_new(const char *btf_file_path, const char *kconfig_path, const char *bpf_obj_name, diff --git a/libbpfgo.h b/libbpfgo.h index de936ec4..4286da5d 100644 --- a/libbpfgo.h +++ b/libbpfgo.h @@ -41,6 +41,20 @@ struct bpf_iter_attach_opts *cgo_bpf_iter_attach_opts_new(__u32 map_fd, __u32 pid_fd); void cgo_bpf_iter_attach_opts_free(struct bpf_iter_attach_opts *opts); +struct bpf_test_run_opts *cgo_bpf_test_run_opts_new(const void *data_in, + void *data_out, + __u32 data_size_in, + __u32 data_size_out, + const void *ctx_in, + void *ctx_out, + __u32 ctx_size_in, + __u32 ctx_size_out, + int repeat, + __u32 flags, + __u32 cpu, + __u32 batch_size); +void cgo_bpf_test_run_opts_free(struct bpf_test_run_opts *opts); + struct bpf_object_open_opts *cgo_bpf_object_open_opts_new(const char *btf_file_path, const char *kconfig_path, const char *bpf_obj_name, diff --git a/prog.go b/prog.go index 6c14909e..e47c8ecf 100644 --- a/prog.go +++ b/prog.go @@ -12,6 +12,7 @@ import ( "path/filepath" "strings" "syscall" + "time" "unsafe" ) @@ -629,3 +630,160 @@ func (p *BPFProg) DetachGenericFD(targetFd int, attachType BPFAttachType) error return nil } + +// +// BPF_PROG_TEST_RUN +// + +type RunFlag uint32 + +const ( + RunFlagRunOnCPU RunFlag = C.BPF_F_TEST_RUN_ON_CPU + RunFlagXDPLiveFrames RunFlag = C.BPF_F_TEST_XDP_LIVE_FRAMES +) + +// RunOpts mirrors the C structure bpf_test_run_opts. +type RunOpts struct { + DataIn []byte + DataOut []byte + DataSizeIn uint32 + DataSizeOut uint32 + CtxIn []byte + CtxOut []byte + CtxSizeIn uint32 + CtxSizeOut uint32 + RetVal uint32 + Repeat int + Duration time.Duration + Flags RunFlag + CPU uint32 + BatchSize uint32 +} + +func runOptsToC(runOpts *RunOpts) (*C.struct_bpf_test_run_opts, error) { + if runOpts == nil { + return nil, nil + } + + var ( + dataIn unsafe.Pointer + dataSizeIn C.uint + dataOut unsafe.Pointer + dataSizeOut C.uint + ctxIn unsafe.Pointer + ctxSizeIn C.uint + ctxOut unsafe.Pointer + ctxSizeOut C.uint + ) + + if runOpts.DataIn != nil { + dataIn = unsafe.Pointer(&runOpts.DataIn[0]) + dataSizeIn = C.uint(runOpts.DataSizeIn) + } + if runOpts.DataOut != nil { + dataOut = unsafe.Pointer(&runOpts.DataOut[0]) + dataSizeOut = C.uint(runOpts.DataSizeOut) + } + if runOpts.CtxIn != nil { + ctxIn = unsafe.Pointer(&runOpts.CtxIn[0]) + ctxSizeIn = C.uint(runOpts.CtxSizeIn) + } + if runOpts.CtxOut != nil { + ctxOut = unsafe.Pointer(&runOpts.CtxOut[0]) + ctxSizeOut = C.uint(runOpts.CtxSizeOut) + } + optsC, errno := C.cgo_bpf_test_run_opts_new( + dataIn, + dataOut, + dataSizeIn, + dataSizeOut, + ctxIn, + ctxOut, + ctxSizeIn, + ctxSizeOut, + C.int(runOpts.Repeat), + C.uint(runOpts.Flags), + C.uint(runOpts.CPU), + C.uint(runOpts.BatchSize), + ) + if optsC == nil { + return nil, fmt.Errorf("failed to create bpf_test_run_opts: %w", errno) + } + + return optsC, nil +} + +func runOptsFromC(runOpts *RunOpts, optsC *C.struct_bpf_test_run_opts) { + if optsC == nil { + return + } + + if optsC.data_in != nil { + runOpts.DataIn = C.GoBytes(optsC.data_in, C.int(optsC.data_size_in)) + } + if optsC.data_out != nil { + runOpts.DataOut = C.GoBytes(optsC.data_out, C.int(optsC.data_size_out)) + } + if optsC.ctx_in != nil { + runOpts.CtxIn = C.GoBytes(optsC.ctx_in, C.int(optsC.ctx_size_in)) + } + if optsC.ctx_out != nil { + runOpts.CtxOut = C.GoBytes(optsC.ctx_out, C.int(optsC.ctx_size_out)) + } + + runOpts.RetVal = uint32(optsC.retval) + runOpts.Repeat = int(optsC.repeat) + runOpts.Duration = time.Duration(optsC.duration) * time.Nanosecond + runOpts.Flags = RunFlag(optsC.flags) + runOpts.CPU = uint32(optsC.cpu) + runOpts.BatchSize = uint32(optsC.batch_size) +} + +// Run executes the eBPF program without attaching it to actual hooks, filling +// the results in the provided RunOpts. +// Reference: +// - https://docs.kernel.org/bpf/bpf_prog_run.html +// - https://docs.kernel.org/userspace-api/ebpf/syscall.html +// +// Example Usage: +// +// /* +// SEC("tc") +// int test(struct __sk_buff *skb) +// { +// return foo() ? 1 : 0; +// } +// */ +// +// func TestFunc(t *testing.T) { +// ... +// prog, _ := module.GetProgram("test") +// opts := RunOpts{ +// DataIn: make([]byte, 0, 14), +// DataSizeIn: 14, +// DataOut: make([]byte, 0, 14), +// DataSizeOut: 14, +// Repeat: 1, +// } +// prog.Run(&opts) +// if opts.RetVal != 1 { +// t.Errorf("result = %d; want 1", opts.RetVal) +// } +// } +func (p *BPFProg) Run(opts *RunOpts) error { + optsC, err := runOptsToC(opts) + if err != nil { + return err + } + defer C.cgo_bpf_test_run_opts_free(optsC) + + retC := C.bpf_prog_test_run_opts(C.int(p.FileDescriptor()), optsC) + if retC < 0 { + return fmt.Errorf("failed to run program: %w", syscall.Errno(-retC)) + } + + // update runOpts with the values from the kernel and libbpf + runOptsFromC(opts, optsC) + + return nil +} diff --git a/selftest/common/run-4.12.sh b/selftest/common/run-4.12.sh new file mode 100755 index 00000000..e1b0669c --- /dev/null +++ b/selftest/common/run-4.12.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +# SETTINGS + +TEST=$(dirname $0)/$1 # execute +TIMEOUT=10 # seconds + +# COMMON + +COMMON="$(dirname $0)/../common/common.sh" +[[ -f $COMMON ]] && { . $COMMON; } || { error "no common"; exit 1; } + +# MAIN + +kern_version gt 4.12 + +check_build +check_ppid +test_exec +test_finish + +exit 0 diff --git a/selftest/prog-run/Makefile b/selftest/prog-run/Makefile new file mode 120000 index 00000000..d981720c --- /dev/null +++ b/selftest/prog-run/Makefile @@ -0,0 +1 @@ +../common/Makefile \ No newline at end of file diff --git a/selftest/prog-run/go.mod b/selftest/prog-run/go.mod new file mode 100644 index 00000000..1071f201 --- /dev/null +++ b/selftest/prog-run/go.mod @@ -0,0 +1,7 @@ +module github.com/aquasecurity/libbpfgo/selftest/testrun + +go 1.21 + +require github.com/aquasecurity/libbpfgo v0.0.0 + +replace github.com/aquasecurity/libbpfgo => ../../ diff --git a/selftest/prog-run/go.sum b/selftest/prog-run/go.sum new file mode 100644 index 00000000..5496456e --- /dev/null +++ b/selftest/prog-run/go.sum @@ -0,0 +1,8 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/selftest/prog-run/main.bpf.c b/selftest/prog-run/main.bpf.c new file mode 100644 index 00000000..acea88e4 --- /dev/null +++ b/selftest/prog-run/main.bpf.c @@ -0,0 +1,27 @@ +//+build ignore + +#include + +#include +#include + +SEC("tc") +int test_tc(struct __sk_buff *skb) +{ + void *data = (void *) (long) skb->data; + void *data_end = (void *) (long) skb->data_end; + if (data + 4 > data_end) { + return -1; + } + + if (*(__u32 *) data == 0xdeadbeef) { + char new_data[] = {0x01, 0x02, 0x03, 0x04}; + bpf_skb_store_bytes(skb, 0, new_data, 4, 0); + bpf_skb_change_tail(skb, 14, 0); + return 1; + } + + return 2; +} + +char LICENSE[] SEC("license") = "GPL"; diff --git a/selftest/prog-run/main.go b/selftest/prog-run/main.go new file mode 100644 index 00000000..32fde9c8 --- /dev/null +++ b/selftest/prog-run/main.go @@ -0,0 +1,52 @@ +package main + +import "C" + +import ( + "encoding/binary" + "log" + + bpf "github.com/aquasecurity/libbpfgo" +) + +func main() { + bpfModule, err := bpf.NewModuleFromFile("main.bpf.o") + if err != nil { + log.Fatalf("Failed to load BPF module: %v", err) + } + defer bpfModule.Close() + + err = bpfModule.BPFLoadObject() + if err != nil { + log.Fatalf("Failed to load object: %v", err) + } + + tcProg, err := bpfModule.GetProgram("test_tc") + if err != nil || tcProg == nil { + log.Fatalf("Failed to get prog test_tc: %v", err) + } + + dataIn := make([]byte, 16) + binary.LittleEndian.PutUint32(dataIn, 0xdeadbeef) + opts := bpf.RunOpts{ + DataIn: dataIn, + DataSizeIn: 16, + DataOut: make([]byte, 32), + DataSizeOut: 32, + Repeat: 1, + } + + err = tcProg.Run(&opts) + if err != nil { + log.Fatalf("Failed to run prog: %v", err) + } + if opts.RetVal != 1 { + log.Fatalf("retVal %d should be 1", opts.RetVal) + } + if len(opts.DataOut) != 14 { + log.Fatalf("dataOut len %v should be 14", opts.DataOut) + } + if binary.LittleEndian.Uint32(opts.DataOut) != 0x04030201 { + log.Fatalf("dataOut 0x%x should be 0x04030201", binary.LittleEndian.Uint32(opts.DataOut)) + } +} diff --git a/selftest/prog-run/run.sh b/selftest/prog-run/run.sh new file mode 120000 index 00000000..5f379d6e --- /dev/null +++ b/selftest/prog-run/run.sh @@ -0,0 +1 @@ +../common/run-4.12.sh \ No newline at end of file