summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-12 22:39:06 +0200
committerPaul Buetow <paul@buetow.org>2026-03-12 22:39:06 +0200
commit13e7970afb3eeac69f82df833f030711e5cf12ec (patch)
tree098c4fb8c5a8c8f27547f03f40c9fee0be63fe35
parent1b21e818a69bf73fde3ca60f89d2dc82a79fd605 (diff)
internal: embed BPF object into ior binary
-rw-r--r--Magefile.go17
-rw-r--r--integrationtests/README.md5
-rw-r--r--integrationtests/cleanup_test.go10
-rw-r--r--integrationtests/harness.go11
-rw-r--r--integrationtests/harness_test.go29
-rw-r--r--integrationtests/helpers_test.go2
-rw-r--r--internal/bpfembed.go31
-rw-r--r--internal/ior.go4
-rw-r--r--internal/ior_setup_test.go97
9 files changed, 184 insertions, 22 deletions
diff --git a/Magefile.go b/Magefile.go
index cc1feeb..36db529 100644
--- a/Magefile.go
+++ b/Magefile.go
@@ -70,19 +70,23 @@ func All() error {
return nil
}
-// BpfBuild builds the BPF object and copies it to the repo root.
+// BpfBuild builds the embedded BPF object used by the Go binary.
func BpfBuild() error {
if err := ensureVMLINUX(); err != nil {
return err
}
- if err := buildBPFObject(); err != nil {
- return err
- }
+ return buildBPFObject()
+}
+
+// BpfExport copies the built BPF object to the repo root for debug workflows.
+func BpfExport() error {
+ mg.Deps(BpfBuild)
return sh.RunV("cp", "-v", bpfObjectPath, bpfOutputPath)
}
// Test runs the full test suite.
func Test() error {
+ mg.Deps(BpfBuild)
if err := sh.RunWithV(goEnv(), "go", "clean", "-testcache"); err != nil {
return err
}
@@ -91,6 +95,7 @@ func Test() error {
// TestRace runs the full test suite with the race detector enabled.
func TestRace() error {
+ mg.Deps(BpfBuild)
if err := sh.RunWithV(goEnv(), "go", "clean", "-testcache"); err != nil {
return err
}
@@ -99,6 +104,7 @@ func TestRace() error {
// TestWithName runs a specific test by name.
func TestWithName() error {
+ mg.Deps(BpfBuild)
if err := sh.RunWithV(goEnv(), "go", "clean", "-testcache"); err != nil {
return err
}
@@ -132,6 +138,7 @@ func TestWithName() error {
// Bench runs benchmarks.
func Bench() error {
+ mg.Deps(BpfBuild)
if err := sh.RunWithV(goEnv(), "go", "test", "./...", "-v", "-bench=.", "-run", "xxx"); err != nil {
return err
}
@@ -150,6 +157,7 @@ func PrReview() error {
// BenchProf runs pipeline benchmarks and writes timestamped pprof artifacts.
func BenchProf() error {
+ mg.Deps(BpfBuild)
if err := ensureBenchProfilesDir(); err != nil {
return err
}
@@ -244,6 +252,7 @@ func BenchFlameCmp() error {
// BenchCompare runs all benchmarks repeatedly and stores output for benchstat.
func BenchCompare() error {
+ mg.Deps(BpfBuild)
if err := ensureBenchProfilesDir(); err != nil {
return err
}
diff --git a/integrationtests/README.md b/integrationtests/README.md
index 99a3fc0..be65499 100644
--- a/integrationtests/README.md
+++ b/integrationtests/README.md
@@ -6,9 +6,12 @@ harness asserts the captured `.ior.zst` output matches expectations.
## Prerequisites
-- Built `ior` binary and `ior.bpf.o` (`mage all`)
+- Built `ior` binary (`mage all`)
- Root privileges or `CAP_BPF` (required for BPF tracepoint attachment)
+The binary embeds its default BPF object. Set `IOR_BPF_OBJECT=/path/to/ior.bpf.o`
+only when you explicitly want to override the embedded object during testing.
+
## Running
```bash
diff --git a/integrationtests/cleanup_test.go b/integrationtests/cleanup_test.go
index 18a0531..316c1b1 100644
--- a/integrationtests/cleanup_test.go
+++ b/integrationtests/cleanup_test.go
@@ -80,8 +80,7 @@ func TestCleanupOutputDirContainsOnlyExpectedFiles(t *testing.T) {
for _, e := range entries {
name := e.Name()
validSuffix := strings.HasSuffix(name, ".ior.zst") ||
- strings.HasSuffix(name, ".svg") ||
- name == "ior.bpf.o" // symlink created by startIor
+ strings.HasSuffix(name, ".svg")
if !validSuffix {
t.Errorf("unexpected file in output dir: %s", name)
}
@@ -172,11 +171,8 @@ func TestCleanupOutputDirEmptyAfterIorFailure(t *testing.T) {
t.Fatalf("read output dir: %v", err)
}
- // Only the BPF symlink should exist; ior produced no output.
- for _, e := range entries {
- if e.Name() != "ior.bpf.o" {
- t.Errorf("unexpected file in output dir after ior failure: %s", e.Name())
- }
+ if len(entries) != 0 {
+ t.Fatalf("expected empty output dir after ior failure, found %d entries", len(entries))
}
}
diff --git a/integrationtests/harness.go b/integrationtests/harness.go
index 17ae994..e3ee900 100644
--- a/integrationtests/harness.go
+++ b/integrationtests/harness.go
@@ -15,6 +15,7 @@ import (
const (
workloadStartupTimeout = 5 * time.Second
iorShutdownGrace = 30 * time.Second
+ bpfObjectOverrideEnv = "IOR_BPF_OBJECT"
)
// TestHarness orchestrates integration tests by starting an ior trace
@@ -22,7 +23,7 @@ const (
type TestHarness struct {
IorBinary string // path to built ior binary
WorkloadBinary string // path to built ioworkload binary
- BpfObject string // path to ior.bpf.o
+ BpfObject string // optional path to external BPF object override
OutputDir string // temp dir for .ior.zst output
}
@@ -120,11 +121,6 @@ func (h *TestHarness) startWorkload(scenario string) (*exec.Cmd, int, error) {
}
func (h *TestHarness) startIor(pid int, scenario string, duration int, extraArgs []string) (*exec.Cmd, error) {
- bpfLink := filepath.Join(h.OutputDir, "ior.bpf.o")
- if err := os.Symlink(h.BpfObject, bpfLink); err != nil {
- return nil, fmt.Errorf("symlink bpf object: %w", err)
- }
-
args := []string{
"-pid", strconv.Itoa(pid),
"-flamegraph",
@@ -136,6 +132,9 @@ func (h *TestHarness) startIor(pid int, scenario string, duration int, extraArgs
cmd.Dir = h.OutputDir
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
+ if h.BpfObject != "" {
+ cmd.Env = append(os.Environ(), bpfObjectOverrideEnv+"="+h.BpfObject)
+ }
if err := cmd.Start(); err != nil {
return nil, fmt.Errorf("start ior: %w", err)
diff --git a/integrationtests/harness_test.go b/integrationtests/harness_test.go
index 6e076ad..27a122c 100644
--- a/integrationtests/harness_test.go
+++ b/integrationtests/harness_test.go
@@ -178,3 +178,32 @@ func TestIorStartFailureCleansUpWorkload(t *testing.T) {
}
}
}
+
+func TestStartIorPassesBPFObjectOverrideEnv(t *testing.T) {
+ tmpDir := t.TempDir()
+ outputDir := t.TempDir()
+ overridePath := filepath.Join(tmpDir, "fake.bpf.o")
+ iorBin := writeScript(t, tmpDir, "ior", `printf '%s' "$IOR_BPF_OBJECT" > "$PWD/override.txt"`)
+
+ h := TestHarness{
+ IorBinary: iorBin,
+ BpfObject: overridePath,
+ OutputDir: outputDir,
+ }
+
+ cmd, err := h.startIor(1234, "test", 5, nil)
+ if err != nil {
+ t.Fatalf("startIor returned error: %v", err)
+ }
+ if err := cmd.Wait(); err != nil {
+ t.Fatalf("wait for fake ior: %v", err)
+ }
+
+ data, err := os.ReadFile(filepath.Join(outputDir, "override.txt"))
+ if err != nil {
+ t.Fatalf("read override marker: %v", err)
+ }
+ if got, want := string(data), overridePath; got != want {
+ t.Fatalf("IOR_BPF_OBJECT = %q, want %q", got, want)
+ }
+}
diff --git a/integrationtests/helpers_test.go b/integrationtests/helpers_test.go
index feb7a55..6ef7ba7 100644
--- a/integrationtests/helpers_test.go
+++ b/integrationtests/helpers_test.go
@@ -9,7 +9,6 @@ import (
const (
iorBinaryDefault = "../ior"
workloadBinaryDefault = "../ioworkload"
- bpfObjectDefault = "../ior.bpf.o"
defaultDuration = 10
parallelEnvVar = "IOR_INTEGRATION_PARALLEL"
)
@@ -23,7 +22,6 @@ func newTestHarness(t *testing.T) TestHarness {
return TestHarness{
IorBinary: absPath(t, iorBinaryDefault),
WorkloadBinary: absPath(t, workloadBinaryDefault),
- BpfObject: absPath(t, bpfObjectDefault),
OutputDir: t.TempDir(),
}
}
diff --git a/internal/bpfembed.go b/internal/bpfembed.go
new file mode 100644
index 0000000..fce784c
--- /dev/null
+++ b/internal/bpfembed.go
@@ -0,0 +1,31 @@
+package internal
+
+import (
+ _ "embed"
+ "os"
+
+ bpf "github.com/aquasecurity/libbpfgo"
+)
+
+const (
+ bpfObjectOverrideEnv = "IOR_BPF_OBJECT"
+ embeddedBPFObjectName = "ior.bpf.o"
+)
+
+//go:embed c/ior.bpf.o
+var embeddedBPFObject []byte
+
+var (
+ newBPFModuleFromFile = bpf.NewModuleFromFile
+ newBPFModuleFromBuffer = bpf.NewModuleFromBuffer
+)
+
+func loadBPFModule() (*bpf.Module, string, error) {
+ if path := os.Getenv(bpfObjectOverrideEnv); path != "" {
+ module, err := newBPFModuleFromFile(path)
+ return module, "load module from override file", err
+ }
+
+ module, err := newBPFModuleFromBuffer(embeddedBPFObject, embeddedBPFObjectName)
+ return module, "load embedded module", err
+}
diff --git a/internal/ior.go b/internal/ior.go
index c14ab37..3f145a9 100644
--- a/internal/ior.go
+++ b/internal/ior.go
@@ -342,9 +342,9 @@ func setupBPFModuleError(stage string, err error) error {
func setupBPFModule(parentCtx context.Context, cfg flags.Config) (*bpf.Module, *probemanager.Manager, func(), error) {
releaseBindings := func() {}
- bpfModule, err := bpf.NewModuleFromFile("ior.bpf.o")
+ bpfModule, stage, err := loadBPFModule()
if err != nil {
- return nil, nil, releaseBindings, setupBPFModuleError("load module from file", err)
+ return nil, nil, releaseBindings, setupBPFModuleError(stage, err)
}
if err := resizeBPFMaps(cfg, bpfModule); err != nil {
bpfModule.Close()
diff --git a/internal/ior_setup_test.go b/internal/ior_setup_test.go
index 9c8b1b3..9c804c7 100644
--- a/internal/ior_setup_test.go
+++ b/internal/ior_setup_test.go
@@ -1,8 +1,12 @@
package internal
import (
+ "bytes"
"errors"
+ "os"
"testing"
+
+ bpf "github.com/aquasecurity/libbpfgo"
)
func TestSetupBPFModuleErrorWrapsStage(t *testing.T) {
@@ -25,3 +29,96 @@ func TestSetupBPFModuleErrorNil(t *testing.T) {
t.Fatalf("expected nil error passthrough, got %v", err)
}
}
+
+func TestLoadBPFModuleUsesEmbeddedObjectByDefault(t *testing.T) {
+ origFile := newBPFModuleFromFile
+ origBuffer := newBPFModuleFromBuffer
+ origOverride, hadOverride := os.LookupEnv(bpfObjectOverrideEnv)
+ t.Cleanup(func() {
+ newBPFModuleFromFile = origFile
+ newBPFModuleFromBuffer = origBuffer
+ if hadOverride {
+ os.Setenv(bpfObjectOverrideEnv, origOverride)
+ return
+ }
+ os.Unsetenv(bpfObjectOverrideEnv)
+ })
+ os.Unsetenv(bpfObjectOverrideEnv)
+
+ wantErr := errors.New("buffer load failed")
+ newBPFModuleFromFile = func(string) (*bpf.Module, error) {
+ t.Fatal("expected embedded loader, not file loader")
+ return nil, nil
+ }
+
+ var gotBytes []byte
+ var gotName string
+ newBPFModuleFromBuffer = func(data []byte, name string) (*bpf.Module, error) {
+ gotBytes = append([]byte(nil), data...)
+ gotName = name
+ return nil, wantErr
+ }
+
+ module, stage, err := loadBPFModule()
+ if module != nil {
+ t.Fatalf("expected nil module from stubbed loader, got %v", module)
+ }
+ if got, want := stage, "load embedded module"; got != want {
+ t.Fatalf("stage = %q, want %q", got, want)
+ }
+ if !errors.Is(err, wantErr) {
+ t.Fatalf("expected embedded loader error, got %v", err)
+ }
+ if !bytes.Equal(gotBytes, embeddedBPFObject) {
+ t.Fatalf("embedded loader received unexpected object bytes")
+ }
+ if got, want := gotName, embeddedBPFObjectName; got != want {
+ t.Fatalf("embedded loader name = %q, want %q", got, want)
+ }
+}
+
+func TestLoadBPFModuleUsesOverridePathWhenConfigured(t *testing.T) {
+ origFile := newBPFModuleFromFile
+ origBuffer := newBPFModuleFromBuffer
+ origOverride, hadOverride := os.LookupEnv(bpfObjectOverrideEnv)
+ t.Cleanup(func() {
+ newBPFModuleFromFile = origFile
+ newBPFModuleFromBuffer = origBuffer
+ if hadOverride {
+ os.Setenv(bpfObjectOverrideEnv, origOverride)
+ return
+ }
+ os.Unsetenv(bpfObjectOverrideEnv)
+ })
+
+ overridePath := "/tmp/custom-ior.bpf.o"
+ if err := os.Setenv(bpfObjectOverrideEnv, overridePath); err != nil {
+ t.Fatalf("set override env: %v", err)
+ }
+
+ wantErr := errors.New("file load failed")
+ newBPFModuleFromBuffer = func([]byte, string) (*bpf.Module, error) {
+ t.Fatal("expected file loader, not embedded loader")
+ return nil, nil
+ }
+
+ var gotPath string
+ newBPFModuleFromFile = func(path string) (*bpf.Module, error) {
+ gotPath = path
+ return nil, wantErr
+ }
+
+ module, stage, err := loadBPFModule()
+ if module != nil {
+ t.Fatalf("expected nil module from stubbed loader, got %v", module)
+ }
+ if got, want := stage, "load module from override file"; got != want {
+ t.Fatalf("stage = %q, want %q", got, want)
+ }
+ if !errors.Is(err, wantErr) {
+ t.Fatalf("expected override loader error, got %v", err)
+ }
+ if got, want := gotPath, overridePath; got != want {
+ t.Fatalf("override path = %q, want %q", got, want)
+ }
+}