tylerflint/bpftime-go
Go module for running eBPF programs without root privileges or kernel eBPF support, powered by bpftime.
bpftime-go
Go module for running eBPF programs without root privileges or kernel eBPF support, powered by bpftime.
Why this project exists
Go applications that use cilium/ebpf make BPF syscalls directly via assembly, bypassing bpftime's LD_PRELOAD syscall interposition. This means you can't simply LD_PRELOAD the bpftime agent into a Go binary to get userspace eBPF.
Traditional eBPF also requires root (or CAP_BPF) and a recent kernel — neither of which is always available in containers, edge devices, CI, or other restricted environments.
bpftime-go solves both problems:
- Native Go integration with bpftime's userspace eBPF runtime via CGO
- No root required — programs run entirely in userspace
- Single-process model — the loader and server run in one Go binary; target processes attach via shared memory
How it works
┌──────────────────────────────────────────────────────────────────┐
│ Your Go binary │
│ │
│ ┌────────────────┐ ┌─────────────────────────────────────┐ │
│ │ cilium/ebpf │ │ bpftime-go │ │
│ │ bpf2go │──▶│ │ │
│ │ (compile BPF) │ │ Backend interface │ │
│ └────────────────┘ │ ├── kernel.KernelBackend │ │
│ │ │ (wraps cilium/ebpf) │ │
│ │ └── bpftime.BpftimeBackend │ │
│ │ (CGO → libbpftime.a) │ │
│ └─────────────────────────────────────┘ │
│ │ │
│ shared memory │
│ │ │
│ ┌──────────────▼──────────────┐ │
│ │ Target process │ │
│ │ (LD_PRELOAD bpftime agent) │ │
│ └─────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
You write BPF C programs and compile them with cilium/ebpf's bpf2go — the standard workflow stays the same. bpftime-go provides Go interface types (Backend, Map, Program, Link, RingBufReader) that mirror cilium/ebpf's API. Two pluggable backends implement the Backend interface:
- Kernel backend — wraps cilium/ebpf, delegates to the kernel BPF subsystem (requires root)
- Bpftime backend — CGO to bpftime's userspace runtime (no root needed)
Switch backends at runtime with an environment variable or in code.
Quick start
go get github.com/tylerflint/bpftime-gopackage main
import (
"fmt"
"os"
bpftime "github.com/tylerflint/bpftime-go"
bpftimebe "github.com/tylerflint/bpftime-go/bpftime"
"github.com/tylerflint/bpftime-go/kernel"
)
func main() {
// Pick backend based on environment
var backend bpftime.Backend
if os.Getenv("USE_BPFTIME") != "" {
b, err := bpftimebe.NewBpftimeBackend(bpftimebe.Options{
ShmMode: bpftimebe.ShmRemoveAndCreate,
})
if err != nil {
panic(err)
}
backend = b
} else {
backend = kernel.NewKernelBackend()
}
defer backend.Close()
// Load BPF spec (generated by bpf2go)
spec, _ := LoadMyProgram()
// Load maps and programs
coll, _ := backend.LoadCollection(spec)
defer coll.Close()
// Attach a uprobe
link, _ := backend.AttachUprobe("malloc", "/lib/x86_64-linux-gnu/libc.so.6",
coll.Programs["my_prog"], nil)
defer link.Close()
// Read ring buffer events
reader, _ := backend.NewRingBufReader(coll.Maps["events"])
defer reader.Close()
for {
rec, err := reader.Read()
if err != nil {
break
}
fmt.Printf("Event: %d bytes\n", len(rec.RawSample))
}
}See the example apps for full working code.
API reference
Backend
The main entry point. Implementations load BPF programs and attach them to hook points.
type Backend interface {
LoadCollection(spec *ebpf.CollectionSpec) (*Collection, error)
AttachUprobe(symbol string, executable string, prog Program, opts *UprobeOptions) (Link, error)
AttachUretprobe(symbol string, executable string, prog Program, opts *UprobeOptions) (Link, error)
AttachKprobe(symbol string, prog Program, opts *KprobeOptions) (Link, error)
AttachKretprobe(symbol string, prog Program, opts *KprobeOptions) (Link, error)
AttachTracepoint(group string, name string, prog Program) (Link, error)
AttachTracing(prog Program, opts *TracingOptions) (Link, error)
AttachCgroup(prog Program, opts *CgroupOptions) (Link, error)
NewRingBufReader(rb Map) (RingBufReader, error)
Close() error
}Backend constructors:
// Kernel backend — delegates to cilium/ebpf and the kernel BPF subsystem.
kernel.NewKernelBackend() *KernelBackend
// Bpftime backend — userspace eBPF via CGO to libbpftime.
bpftime.NewBpftimeBackend(opts Options) (*BpftimeBackend, error)Collection
Holds related BPF maps and programs after loading.
type Collection struct {
Maps map[string]Map
Programs map[string]Program
}
func (c *Collection) Close() errorMap
Key-value operations on a BPF map.
type Map interface {
Lookup(key, valueOut interface{}) error
Update(key, value interface{}, flags MapUpdateFlags) error
Put(key, value interface{}) error // convenience: Update with BPF_ANY
Delete(key interface{}) error
Iterate() *MapIterator
NextKey(key interface{}) ([]byte, error) // nil key returns first key
FD() int
Info() (*MapInfo, error)
Close() error
}Program
A loaded BPF program.
type Program interface {
FD() int
Info() (*ProgramInfo, error)
Close() error
}Link
An attachment of a BPF program to a hook point.
type Link interface {
Close() error
FD() int
Info() (*LinkInfo, error)
}RingBufReader
Reads records from a BPF ring buffer map.
type RingBufReader interface {
Read() (Record, error)
ReadInto(rec *Record) error
SetDeadline(t time.Time)
Close() error
BufferSize() int
}Record
type Record struct {
RawSample []byte
Remaining int
}MapIterator
iter := m.Iterate()
var key []byte
var value uint64
for iter.Next(&key, &value) {
// process entry
}
if err := iter.Err(); err != nil {
// handle error
}Configuration types
type UprobeOptions struct {
Address uint64 // absolute address (skips symbol resolution)
Offset uint64 // offset from symbol
PID int // limit to specific process (0 = all)
Cookie uint64 // user-defined value passed to BPF program
RefCtrOffset uint64 // reference counter semaphore offset
}
type KprobeOptions struct {
Cookie uint64
Offset uint64
}
type TracingOptions struct {
Cookie uint64
}
type CgroupOptions struct {
Path string // cgroupv2 directory path
Attach ebpf.AttachType
}MapUpdateFlags
const (
BPF_ANY MapUpdateFlags = 0 // create or update
BPF_NOEXIST MapUpdateFlags = 1 // create only if key doesn't exist
BPF_EXIST MapUpdateFlags = 2 // update only if key exists
)UnsupportedMode
Controls how the bpftime backend handles operations it doesn't support (kprobes, fentry/fexit, cgroups, non-syscall tracepoints).
const (
UnsupportedError UnsupportedMode = iota // return ErrNotSupported (default)
UnsupportedIgnore // no-op, return NoopLink
)NoopLink is a Link that does nothing — Close() returns nil, FD() returns -1.
Error sentinels
var (
ErrNotFound // key or entry not found
ErrKeyExist // key already exists (BPF_NOEXIST)
ErrMapFull // map at max capacity
ErrNotSupported // operation not supported by backend
ErrClosed // operation on closed resource
)Backend comparison
| Capability | Kernel | Bpftime |
|---|---|---|
| Uprobe / Uretprobe | Yes | Yes |
| Kprobe / Kretprobe | Yes | No |
| Syscall tracepoints | Yes | Yes |
| Other tracepoints | Yes | No |
| fentry / fexit | Yes | No |
| Cgroup | Yes | No |
| Ring buffers | Yes | Yes |
| Hash maps | Yes | Yes |
| Requires root | Yes | No |
Example apps
All examples live under apps/. Each has a Makefile with build, test-kernel, and test-bpftime-* targets.
Common workflow:
cd apps/<example>
make build # runs go generate + go build
make test-kernel # run with kernel backend (requires root)
make test-bpftime-loader # terminal 1: start the loader
make test-bpftime-target # terminal 2: run a target processmalloc
Attaches a uprobe to libc malloc, sends events over a ring buffer.
# Kernel backend (requires root):
make test-kernel
# Bpftime backend (two terminals):
make test-bpftime-loader # starts loader with USE_BPFTIME=1
make test-bpftime-target # runs commands with LD_PRELOAD=libbpftime-agent.soopenat
Attaches to the sys_enter_openat syscall tracepoint, captures file open events.
# Kernel backend:
make test-kernel
# Bpftime backend (two terminals):
make test-bpftime-loader # starts loader with USE_BPFTIME=1
make test-bpftime-target # runs commands with LD_PRELOAD=libbpftime-agent-transformer.soopenat-go-target
A Go binary that exercises various openat syscall patterns (os.Open, os.ReadDir, os.CreateTemp, syscall.Open, syscall.Openat). Used as a target process for the openat loader to verify Go syscall interception works.
make build
make run # runs with LD_PRELOAD=libbpftime-agent-transformer.sounsupported-modes
Demonstrates UnsupportedMode behavior — verifies that the bpftime backend correctly returns ErrNotSupported in error mode and NoopLink in ignore mode for unsupported operations.
make test-kernel # kernel backend (all operations supported)
make test-bpftime # bpftime backend (tests both modes)Agent types
Different probe types require different bpftime agents:
| Probe type | Agent library | Environment |
|---|---|---|
| Uprobes | libbpftime-agent.so |
LD_PRELOAD=<path> |
| Syscall tracepoints | libbpftime-agent-transformer.so |
LD_PRELOAD=<path> AGENT_SO=<agent.so path> |
Platforms
- linux/amd64 (glibc)
- linux/arm64 (glibc)
musl/Alpine support is pending upstream bpftime changes.
Building bpftime from source
Most users don't need this — pre-built static libraries are bundled in bpftime/deps/ and are used automatically by go get.
Prerequisites
- Docker with buildx support
- QEMU for cross-architecture builds
Build targets
make release-current # build for current architecture only
make release # build all platforms (amd64 + arm64)
make update-deps # copy from dist/ to bpftime/deps/The build flow:
scripts/build-release.shbuildslibbpftime.avia Docker (docker/Dockerfile.release)- Output goes to
dist/<platform>/lib/libbpftime.a scripts/update-deps.shcopies tobpftime/deps/(committed to repo)
Updating bpftime version
BPFTIME_VERSION=v0.3.0 make release
git add bpftime/deps/
git commit -m "Update bpftime libraries to v0.3.0"Why no headers are bundled
The bpftime headers require Boost. Instead, bpftime/cgo_linux.cpp declares the needed types and functions inline, so only libbpftime.a needs to be bundled.
Development
# Start dev container (builds current platform release first)
make dev
# Run tests
go test ./...