GitHunt
TY

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-go
package 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() error

Map

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
}

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 process

malloc

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.so

openat

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.so

openat-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.so

unsupported-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:

  1. scripts/build-release.sh builds libbpftime.a via Docker (docker/Dockerfile.release)
  2. Output goes to dist/<platform>/lib/libbpftime.a
  3. scripts/update-deps.sh copies to bpftime/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 ./...
tylerflint/bpftime-go | GitHunt