CO-RE and BTF: Portable eBPF Programs¶
CO-RE (Compile Once, Run Everywhere) and BTF (BPF Type Format) are revolutionary technologies that solve one of eBPF's biggest challenges: kernel version compatibility. This guide covers how to write portable eBPF programs that work across different kernel versions without recompilation.
🎯 The Problem CO-RE Solves¶
Traditional eBPF Challenges¶
// ❌ Problem: Kernel structure layouts change between versions
struct task_struct {
int pid; // Kernel 4.19: offset 1234
char comm[16]; // Kernel 5.4: offset 1248
// ... but in kernel 5.10, pid might be at offset 1240!
};
// This breaks when you access fields directly
int get_pid_old_way(void) {
struct task_struct *task = (struct task_struct *)bpf_get_current_task();
return task->pid; // ❌ Might access wrong memory location
}
CO-RE Solution¶
// ✅ Solution: CO-RE handles kernel differences automatically
#include "vmlinux.h"
#include <bpf/bpf_core_read.h>
int get_pid_core_way(void) {
struct task_struct *task = (struct task_struct *)bpf_get_current_task();
return BPF_CORE_READ(task, pid); // ✅ Works across kernel versions
}
🧬 What is BTF?¶
BTF (BPF Type Format) is a compact metadata format that describes:
- Data structure layouts in the kernel
- Function signatures and parameters
- Type relationships and dependencies
- Source code locations for debugging
BTF in Action¶
# Check if your kernel has BTF support
ls /sys/kernel/btf/vmlinux
# View BTF information
bpftool btf dump file /sys/kernel/btf/vmlinux | grep "STRUCT 'task_struct'"
BTF Type Information¶
// BTF contains metadata like this for every kernel type
struct task_struct {
// BTF knows: field 'pid' is at offset X in kernel version Y
int pid; /* offset: varies by kernel */
int tgid; /* offset: varies by kernel */
char comm[16]; /* offset: varies by kernel */
struct mm_struct *mm; /* offset: varies by kernel */
// ... hundreds more fields
};
🔧 CO-RE Programming Patterns¶
1. Field Access with BPF_CORE_READ¶
#include "vmlinux.h"
#include <bpf/bpf_core_read.h>
struct process_info {
u32 pid;
u32 tgid;
char comm[16];
u32 ppid;
};
SEC("kprobe/do_exit")
int trace_process_exit(struct pt_regs *ctx) {
struct task_struct *task = (struct task_struct *)bpf_get_current_task();
struct process_info info = {};
// ✅ CO-RE field access - works across kernel versions
info.pid = BPF_CORE_READ(task, pid);
info.tgid = BPF_CORE_READ(task, tgid);
BPF_CORE_READ_STR_INTO(&info.comm, task, comm);
// Access nested fields safely
struct task_struct *parent = BPF_CORE_READ(task, real_parent);
if (parent) {
info.ppid = BPF_CORE_READ(parent, tgid);
}
// Submit to ring buffer
bpf_ringbuf_output(&events, &info, sizeof(info), 0);
return 0;
}
2. Handling Field Existence¶
// Some fields might not exist in older kernels
SEC("kprobe/do_fork")
int trace_fork(struct pt_regs *ctx) {
struct task_struct *task = (struct task_struct *)bpf_get_current_task();
// Check if field exists before accessing
if (bpf_core_field_exists(task->cgroups)) {
// This field was added in kernel 4.6
void *cgroups = BPF_CORE_READ(task, cgroups);
// Use cgroups information
}
// Alternative: use different fields based on kernel version
u64 start_time = 0;
if (bpf_core_field_exists(task->start_boottime)) {
// Newer kernels
start_time = BPF_CORE_READ(task, start_boottime);
} else if (bpf_core_field_exists(task->real_start_time)) {
// Older kernels
start_time = BPF_CORE_READ(task, real_start_time.tv_sec);
}
return 0;
}
3. Type Preservation¶
// CO-RE preserves type information
struct file_event {
u32 pid;
u32 fd;
char filename[256];
u64 inode;
};
SEC("kprobe/vfs_open")
int trace_file_open(struct pt_regs *ctx) {
// PT_REGS_PARM1 is CO-RE-aware for function parameters
struct path *path = (struct path *)PT_REGS_PARM1(ctx);
struct file_event event = {};
event.pid = bpf_get_current_pid_tgid() >> 32;
// Navigate complex nested structures with CO-RE
struct dentry *dentry = BPF_CORE_READ(path, dentry);
struct inode *inode = BPF_CORE_READ(dentry, d_inode);
if (inode) {
event.inode = BPF_CORE_READ(inode, i_ino);
}
// Read filename from dentry
BPF_CORE_READ_STR_INTO(&event.filename, dentry, d_name.name);
bpf_ringbuf_output(&events, &event, sizeof(event), 0);
return 0;
}
🚀 Advanced CO-RE Features¶
1. Conditional Compilation¶
// Compile different code paths based on kernel features
#if __has_builtin(__builtin_preserve_enum_value)
// Use modern eBPF features
#define USE_MODERN_FEATURES 1
#else
// Fallback for older kernels
#define USE_MODERN_FEATURES 0
#endif
SEC("tracepoint/syscalls/sys_enter_openat")
int trace_openat(struct trace_event_raw_sys_enter *ctx) {
#if USE_MODERN_FEATURES
// Use advanced tracepoint features
u64 filename_ptr = ctx->args[1];
char filename[256];
bpf_probe_read_user_str(&filename, sizeof(filename), (void *)filename_ptr);
#else
// Fallback method
char filename[256] = "unknown";
#endif
return 0;
}
2. Enum Value Preservation¶
// Safely use kernel enum values
enum {
TRACE_EVENT_TYPE_EXEC = 1,
TRACE_EVENT_TYPE_EXIT = 2,
TRACE_EVENT_TYPE_FORK = 3,
};
// Preserve kernel enum values for compatibility
static const int TASK_RUNNING = __builtin_preserve_enum_value(*(typeof(TASK_RUNNING) *)TASK_RUNNING);
static const int TASK_INTERRUPTIBLE = __builtin_preserve_enum_value(*(typeof(TASK_INTERRUPTIBLE) *)TASK_INTERRUPTIBLE);
SEC("kprobe/schedule")
int trace_schedule(struct pt_regs *ctx) {
struct task_struct *task = (struct task_struct *)bpf_get_current_task();
long state = BPF_CORE_READ(task, __state); // or 'state' in older kernels
if (state == TASK_RUNNING) {
// Handle running task
} else if (state == TASK_INTERRUPTIBLE) {
// Handle sleeping task
}
return 0;
}
3. Relocations and Field Sizes¶
// Handle varying field sizes across kernel versions
struct network_event {
u32 pid;
u32 saddr;
u32 daddr;
u16 sport;
u16 dport;
u8 protocol;
};
SEC("kprobe/tcp_v4_connect")
int trace_tcp_connect(struct pt_regs *ctx) {
struct sock *sk = (struct sock *)PT_REGS_PARM1(ctx);
struct network_event event = {};
event.pid = bpf_get_current_pid_tgid() >> 32;
// CO-RE handles different socket structure layouts
struct inet_sock *inet = (struct inet_sock *)sk;
// These field accesses work across kernel versions
event.saddr = BPF_CORE_READ(inet, inet_saddr);
event.daddr = BPF_CORE_READ(inet, inet_daddr);
event.sport = BPF_CORE_READ(inet, inet_sport);
event.dport = BPF_CORE_READ(inet, inet_dport);
bpf_ringbuf_output(&events, &event, sizeof(event), 0);
return 0;
}
📦 Complete CO-RE Example: Process Lineage Tracker¶
Here's a complete example showing advanced CO-RE usage:
// process_lineage.c
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_core_read.h>
struct process_lineage {
u32 pid;
u32 ppid;
u32 tgid;
char comm[16];
char parent_comm[16];
u64 start_time;
u32 uid;
u32 gid;
u8 depth; // How deep in the process tree
};
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 1 << 24);
} events SEC(".maps");
// Helper to safely get process lineage
static int get_process_lineage(struct task_struct *task, struct process_lineage *lineage) {
if (!task || !lineage)
return -1;
// Basic process info
lineage->pid = BPF_CORE_READ(task, pid);
lineage->tgid = BPF_CORE_READ(task, tgid);
BPF_CORE_READ_STR_INTO(&lineage->comm, task, comm);
// Get credentials (might vary by kernel version)
const struct cred *cred = BPF_CORE_READ(task, cred);
if (cred) {
lineage->uid = BPF_CORE_READ(cred, uid.val);
lineage->gid = BPF_CORE_READ(cred, gid.val);
}
// Parent process info with CO-RE
struct task_struct *parent = BPF_CORE_READ(task, real_parent);
if (parent && parent != task) { // Avoid init process loop
lineage->ppid = BPF_CORE_READ(parent, tgid);
BPF_CORE_READ_STR_INTO(&lineage->parent_comm, parent, comm);
// Calculate process tree depth
lineage->depth = 1;
struct task_struct *current_parent = parent;
#pragma unroll
for (int i = 0; i < 10; i++) { // Limit depth to prevent verifier issues
struct task_struct *next_parent = BPF_CORE_READ(current_parent, real_parent);
if (!next_parent || next_parent == current_parent)
break;
lineage->depth++;
current_parent = next_parent;
}
}
// Get start time (field name varies by kernel version)
if (bpf_core_field_exists(task->start_boottime)) {
lineage->start_time = BPF_CORE_READ(task, start_boottime);
} else if (bpf_core_field_exists(task->real_start_time)) {
// Handle older kernel versions
lineage->start_time = BPF_CORE_READ(task, real_start_time.tv_sec);
}
return 0;
}
SEC("tracepoint/sched/sched_process_exec")
int trace_process_lineage(struct trace_event_raw_sched_process_exec *ctx) {
struct task_struct *task = (struct task_struct *)bpf_get_current_task();
struct process_lineage *lineage = bpf_ringbuf_reserve(&events, sizeof(*lineage), 0);
if (!lineage)
return 0;
if (get_process_lineage(task, lineage) < 0) {
bpf_ringbuf_discard(lineage, 0);
return 0;
}
bpf_ringbuf_submit(lineage, 0);
return 0;
}
char _license[] SEC("license") = "GPL";
🛠️ Building CO-RE Programs¶
1. Makefile for CO-RE¶
# Generate vmlinux.h for your kernel
gen_vmlinux:
bpftool btf dump file /sys/kernel/btf/vmlinux format c > bpf/headers/vmlinux.h
# Build with CO-RE support
build_core:
clang -g -O2 -target bpf -D__TARGET_ARCH_x86 \
-I./bpf/headers \
-c bpf/process_lineage.c \
-o bpf/process_lineage.o
# Generate Go bindings
generate:
go generate ./...
2. Go Integration¶
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -target native -type process_lineage processlineage ../bpf/process_lineage.c
package main
import (
"bytes"
"encoding/binary"
"log"
"os"
"os/signal"
"syscall"
"github.com/cilium/ebpf/link"
"github.com/cilium/ebpf/ringbuf"
)
func main() {
// Load the CO-RE program
objs := processlineageObjects{}
if err := loadProcesslineageObjects(&objs, nil); err != nil {
log.Fatalf("loading objects: %v", err)
}
defer objs.Close()
// Attach to tracepoint
l, err := link.Tracepoint("sched", "sched_process_exec", objs.TraceProcessLineage, nil)
if err != nil {
log.Fatalf("opening tracepoint: %v", err)
}
defer l.Close()
// Read events
rd, err := ringbuf.NewReader(objs.Events)
if err != nil {
log.Fatalf("opening ringbuf reader: %v", err)
}
defer rd.Close()
// Handle events
go handleEvents(rd)
// Wait for signal
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
<-sig
}
func handleEvents(rd *ringbuf.Reader) {
for {
record, err := rd.Read()
if err != nil {
log.Printf("reading from reader: %v", err)
continue
}
var event processlineageProcessLineage
if err := binary.Read(bytes.NewReader(record.RawSample), binary.LittleEndian, &event); err != nil {
log.Printf("parsing event: %v", err)
continue
}
log.Printf("Process: %s (PID=%d, PPID=%d, Depth=%d, UID=%d)",
nullTerminatedString(event.Comm[:]),
event.Pid,
event.Ppid,
event.Depth,
event.Uid)
}
}
func nullTerminatedString(b []byte) string {
for i, c := range b {
if c == 0 {
return string(b[:i])
}
}
return string(b)
}
🔍 Debugging CO-RE Programs¶
1. BTF Debug Information¶
# Check BTF generation
bpftool gen skeleton bpf/process_lineage.o
# Verify relocations
llvm-objdump -r bpf/process_lineage.o
# Check CO-RE relocations
bpftool prog show id <prog_id> --pretty
2. Verifier with BTF¶
# Load with verifier debug info
echo 2 > /proc/sys/net/core/bpf_jit_enable
echo 1 > /proc/sys/kernel/bpf_stats_enabled
# View detailed verifier log
bpftool prog load bpf/process_lineage.o /sys/fs/bpf/test_prog type kprobe \
log_level 2 log_file verifier.log
⚠️ CO-RE Limitations and Best Practices¶
Limitations¶
- BTF Requirement: Target kernel must have BTF enabled
- Field Dependencies: Some fields might not exist in older kernels
- Compilation Complexity: More complex build process
- Debug Challenges: Harder to debug relocation issues
Best Practices¶
// ✅ Always check field existence for optional fields
if (bpf_core_field_exists(task->some_new_field)) {
value = BPF_CORE_READ(task, some_new_field);
}
// ✅ Use CO-RE read macros consistently
data = BPF_CORE_READ(ptr, field); // Single field
BPF_CORE_READ_STR_INTO(&dest, ptr, field); // String field
// ✅ Handle nested structures safely
nested_ptr = BPF_CORE_READ(outer, inner_ptr);
if (nested_ptr) {
value = BPF_CORE_READ(nested_ptr, field);
}
// ❌ Don't mix CO-RE and direct access
struct task_struct *task = get_current_task();
pid = task->pid; // Wrong - bypasses CO-RE
🎯 When to Use CO-RE¶
Perfect for:¶
- Production deployments across multiple kernel versions
- Distribution packages that need wide compatibility
- Open source tools used by diverse environments
- Long-term maintenance scenarios
Consider alternatives for:¶
- Single kernel environment with guaranteed version
- Rapid prototyping where compatibility isn't critical
- Learning eBPF (start simple, add CO-RE later)
CO-RE and BTF represent the future of eBPF development, enabling truly portable programs that work across the entire Linux ecosystem. Master these concepts to build professional-grade eBPF tools! 🚀