BTF and CO-RE: Portable eBPF Programs Explained
An eBPF program can be valid on one Linux kernel and fail on another because internal structure layouts, available fields, and kernel features differ. BTF and CO-RE provide a way to adapt certain type-dependent instructions when a program is loaded, rather than hard-coding every field offset for one build. This explainer is for developers and operators who build tracing, networking, or security tools and need to understand what that portability does - and does not - guarantee.
Why BTF and CO-RE Matter
eBPF programs run at selected points in the Linux kernel, where they may need to inspect kernel data structures. Those structures are implementation details: their layouts can change between kernel versions or configurations. A program compiled with a fixed offset can then read the wrong data or be rejected by the verifier.
The eBPF project describes the technology’s use in networking, observability, tracing, and security. As these use cases move between distributions and kernel builds, developers need a way to resolve type-dependent accesses against the kernel that will actually run the program. BPF Type Format (BTF) supplies type metadata, and Compile Once - Run Everywhere (CO-RE) uses that metadata to adjust supported accesses during loading.
What Are BTF and CO-RE?
BTF is a compact metadata format for BPF programs, maps, and relevant kernel types. It can describe data types such as structures and enums, as well as function and source-line information. The Linux kernel BTF documentation defines both the kernel-facing format and how BTF is represented in an ELF object file. A kernel may expose its type information at /sys/kernel/btf/vmlinux; a BPF object can carry its own local BTF.
CO-RE is a workflow built on that metadata, compiler-generated relocation records, and a loader such as libbpf. The program is compiled with references to its expected type layout. At load time, the loader compares those references with the target kernel’s BTF and patches supported instruction offsets or values before asking the kernel to verify and load the program. The libbpf overview documents this object-loading lifecycle and libbpf’s CO-RE support.
BTF is not the eBPF instruction set, and CO-RE is not an alternative instruction format. The IETF BPF Instruction Set Architecture specification defines instructions; it does not standardize BTF, CO-RE, Linux hooks, helper availability, or a stable kernel data-structure ABI.
The Problem: Kernel Layouts Are Not a Stable ABI
Suppose a tracing program reads a field from struct task_struct. If it assumes that the field is always at byte offset 16, it depends on a particular layout. A compiler, kernel configuration, or later kernel change can move that field. The eBPF verifier checks memory access against the program’s known constraints, but it cannot make a stale hard-coded offset refer to the intended field.
One workaround is to compile separately for each target kernel or distribute source that compiles on the host. That can mean shipping a compiler and headers, creating packages for many kernel builds, or managing a build step on every machine. CO-RE instead lets a program carry its type information and relocation requests so a compatible loader can resolve selected differences at load time.
The aim is narrower than a promise to run unchanged everywhere. CO-RE can adapt a field offset when the target’s BTF has a compatible type and field. It cannot provide a missing field, add an unsupported helper, make a tracepoint appear, or change the semantics of a kernel hook.
How CO-RE Works
A typical CO-RE load follows these steps:
- The developer writes a BPF program using types that describe the expected kernel data.
- Clang emits BPF instructions, local BTF, and CO-RE relocation records for supported type-dependent accesses.
- A loader reads the object and obtains target BTF, commonly from the running kernel’s BTF interface.
- libbpf matches local type and field references to target types, then applies relocations for compatible accesses.
- The loader submits the adjusted program to the kernel, where normal verifier and load checks still apply.
The kernel’s BPF relocation documentation explains that CO-RE relocation records are stored in .BTF.ext and can adjust instruction fields at load time. Relocations cover field, type, and enum information. A program can, for example, ask whether a field or type exists and select a compatible path, rather than assuming every kernel has the same layout.
| Approach | How it handles kernel types | Where adaptation happens | Main limitation |
|---|---|---|---|
| Hard-coded offsets | Assumes a particular layout | Before deployment, by the developer | Breaks when the expected layout differs |
| Build for each target | Uses headers or metadata for each kernel | At build or packaging time | Requires per-target artifacts or build infrastructure |
| CO-RE with BTF | Carries local type data and relocation requests | During object loading | Needs usable target BTF, a compatible loader, and supported target features |
| Runtime compilation | Builds for the host’s available kernel headers | On the target machine | Requires a compiler/toolchain and matching headers at runtime |
These approaches can coexist. For example, an application may use CO-RE for structure layout while still selecting a different program or hook for kernels that lack a required feature. The loader is part of the compatibility design: it must understand the object and perform the relocations before load.
Components and Key Concepts
Local BTF: The compiled object contains a description of the types the source was built against. The compiler uses field access information to create relocation records. vmlinux.h, often generated from kernel BTF, supplies declarations for kernel types without requiring the full kernel source header tree.
Target BTF: This describes types available on the kernel that will run the program. The loader uses it as the reference for resolving relocations. If the target kernel does not expose suitable BTF, normal CO-RE relocation cannot simply infer the missing layout.
CO-RE relocations: These records identify an instruction and the type or field access it depends on. They are distinct from ordinary ELF relocations. A relocation can describe a field offset, field existence, type size, or enum value; the compiler and loader coordinate how that information is applied.
libbpf and the loader: libbpf parses the object, resolves maps and relocations, loads programs, and can attach them to supported hooks. Object loading is separate from attaching a program and from the program’s behavior after attachment. Applications often use a generated libbpf skeleton to simplify these lifecycle steps.
Verifier and kernel capabilities: CO-RE changes selected values in a program; it does not bypass the verifier. The kernel still checks the resulting program, and the requested program type, helper, map, hook, permissions, and configuration must be supported. The BPF ISA is standardized by the IETF, but Linux kernel APIs and attachment points have their own compatibility story.
Real-World Uses
- Observability agents: A tracer can read selected kernel fields while supporting multiple kernel builds, reducing the need to ship a separate offset table for each system. The eBPF observability explainer covers hooks, maps, and data collection.
- Runtime security: A security sensor can inspect process or kernel context across supported hosts. Tetragon’s eBPF runtime security architecture shows why kernel compatibility is only one part of a production monitoring design.
- Scheduler experiments: A
sched_extBPF scheduler can use type metadata for supported kernel details, but it still depends on the kernel’s sched_ext interface and policy behavior. See Linux sched_ext and BPF scheduling. - Portable tools for heterogeneous fleets: Platform teams can use one object for compatible kernel variants, then test and group machines by BTF, kernel configuration, and hook availability instead of assuming that a shared architecture name means identical support.
In each case, CO-RE helps manage a specific source of variation. It does not replace testing against the kernel builds and policies that the tool is expected to support.
Getting Started: Compile and Inspect a CO-RE Object
Start on a Linux development machine with kernel BTF enabled. On Debian or Ubuntu, install the compiler, BPF tooling, and libbpf development files:
sudo apt update
sudo apt install clang llvm bpftool libbpf-dev libelf-dev zlib1g-dev
Generate a C header from the running kernel’s BTF. This gives the compiler local declarations; it does not freeze the target kernel layout into the resulting program.
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
Create probe.bpf.c with a small tracepoint program. BPF_CORE_READ marks the pid access for CO-RE handling, while bpf_printk makes the example’s result visible for a short test.
#include "vmlinux.h"
#include <bpf/bpf_core_read.h>
#include <bpf/bpf_helpers.h>
char LICENSE[] SEC("license") = "GPL";
SEC("tp/sched/sched_process_exec")
int observe_exec(void *ctx)
{
struct task_struct *task =
(struct task_struct *)bpf_get_current_task_btf();
int pid = BPF_CORE_READ(task, pid);
bpf_printk("process exec pid=%d", pid);
return 0;
}
Compile the program and inspect its sections:
clang -g -O2 -target bpf -c probe.bpf.c -o probe.bpf.o
llvm-objdump -h probe.bpf.o | grep -E '\.BTF(\.ext)?'
bpftool btf dump file probe.bpf.o format raw | head
Debug information (-g) is important for emitting BTF and relocation metadata. A CO-RE object commonly includes .BTF and .BTF.ext; libbpf uses these when it loads the object. These commands compile and inspect an object only - they do not load or attach it. Running the program requires a compatible loader, suitable privileges, the tracepoint and helper on the target, and a kernel with the necessary BTF support. bpf_printk is for short debugging sessions, not high-volume production output.
If the BTF dump fails, check whether /sys/kernel/btf/vmlinux exists and whether the kernel exposes BTF. If compilation cannot find bpf/bpf_helpers.h, confirm that the distribution’s libbpf development package is installed. If object loading fails, keep the full libbpf and verifier error output: it can distinguish a relocation mismatch from a missing helper, unsupported program type, permission restriction, or verifier rejection. Do not treat successful compilation as proof that the object will load on another kernel.
Common Misconceptions
“Compile once” means one object works on every Linux kernel. CO-RE handles selected type differences when compatible BTF exists. Kernel features, helpers, hooks, and behavior still vary.
BTF is a stable kernel ABI. BTF describes types for tooling and relocation. Its presence does not promise that internal structures or semantics will never change.
CO-RE replaces the verifier or loader. The loader must process relocations, and the kernel verifier still checks the adjusted program. CO-RE makes neither step optional.
Related Articles
- eBPF Observability: How Kernel Tracing Works explains how programs attach to kernel events and collect data.
- Tetragon Explained: eBPF Runtime Security Architecture covers a production runtime-security use of eBPF.
- Linux sched_ext Explained: Extensible BPF Scheduling explores a kernel feature that uses BPF for scheduling policy.
Changelog and Last Updated
Initial publication. Last updated: November 14.

