Summary
Add a #[btf_preserve_access] attribute for aggregate types. On builds
targeting BPF targets (bpfel-unknown-none, bpfeb-unknown-none) that emit
BTF (-C debuginfo=2), field and array-element accesses through values of
attributed types emit BTF relocations, allowing eBPF loaders to relocate those
accesses against the target Linux kernel's BTF data at load time.
Motivation
BTF, the BPF Type Format, is the type metadata format used by the Linux kernel and by eBPF tooling. eBPF loaders such as Aya and libbpf use BTF for Compile Once, Run Everywhere (CO-RE) relocations: the compiled program records which field or array element it intended to access, and the loader rewrites the bytecode to match the layout of the kernel it is about to run on.
clang and GCC are capable of emitting such relocations.
Rust can already target eBPF, but it does not currently have a way to emit these BTF access relocations. In practice that means Rust eBPF programs often have to pick one of three inconvenient options:
- Vendor exact kernel type definitions and rebuild for each supported kernel layout.
- Avoid typed field access and manually encode offsets, sacrificing readability and maintainability.
- Write a module in C solely for accessing kernel types and use build.rs to link it to the Rust project.
The goal of this RFC is to give Rust codegen enough information to emit the same relocation-friendly IR that clang and GCC emit today, without introducing a new source-language model for kernel types. Rust users should be able to define the subset of kernel types they care about, mark those definitions as relocation-aware, then write normal field and indexing code against them.
Guide-level explanation
Suppose an eBPF program needs to read the pid and tgid fields from Linux's
task_struct. The exact layout of task_struct varies across
kernel versions, so using the full type definition or hard-coding byte offsets
is not a robust option.
After enabling the feature:
#![feature(btf_preserve_access)]
the program can declare only the part of the kernel type it intends to use:
#[btf_preserve_access]
#[expect(non_camel_case_types, reason = "Linux kernel type")]
struct task_struct {
pid: i32,
tgid: i32,
}
The important part is the Rust type's shape and names. For CO-RE to work, the field names and nesting must correspond to the kernel BTF that the loader will relocate against.
Ordinary Rust field accesses then become relocation-aware:
fn process_task(task: &task_struct) {
let pid = task.pid;
let tgid = task.tgid;
}
Pattern matching over fields is also supported because it lowers to the same field projections:
fn process_task(task: &task_struct) {
let task_struct { pid, tgid } = task;
}
Likewise, fixed-size arrays nested within an attributed type can use indexed
accesses. For example, networking programs often work with
[net_device][net-device], which contains name[IFNAMSIZ]:
#[btf_preserve_access]
#[expect(non_camel_case_types, reason = "Linux kernel type")]
struct net_device {
name: [u8; 16],
}
fn process_dev(dev: &net_device) {
let first_char = dev.name[0];
}
When compiling for a BPF target with BTF emission enabled, rustc records those accesses in the generated IR using BTF-preserving intrinsics. The eBPF loader then patches the bytecode so that the program uses the offsets and indices appropriate for the target kernel.
Outside BPF/BTF-producing builds, the attribute has no effect.
Reference-level explanation
Syntax
This RFC introduces an unstable built-in attribute:
#[btf_preserve_access]
The attribute is permitted on struct and union items. It is guarded by the
btf_preserve_access feature gate.
Codegen
LLVM provides dedicated intrinsics for emitting BTF relocations:
@llvm.preserve.array.access.indexfor index projections from an array.@llvm.preserve.struct.access.indexfor field projections from a struct.@llvm.preserve.union.access.indexfor field projections from a union.
IRBuilder provides methods that language frontends can use for creating the
intrinsic calls:
CreatePreserveArrayAccessIndexCreatePreserveStructAccessIndexCreatePreserveUnionAccessIndex
The implementation strategy is to expose corresponding hooks on the codegen backend abstraction:
btf_preserve_array_access_indexbtf_preserve_struct_access_indexbtf_preserve_union_access_index
The LLVM backend lowers these directly to the corresponding IRBuilder
methods.
GCC's BPF backend also supports this relocation model through the
preserve_access_index type attribute, which GCC documents as
being equivalent to wrapping each access in __builtin_preserve_access_index
built-in function.
The binary object keeps the relocations in the BTF.ext ELF section.
Backends that do not provide an equivalent preserve-access mechanism would still lower the hooks to ordinary projections.
Scope of preserved accesses
The attribute affects projections whose base type is marked with
#[btf_preserve_access], as well as projections continuing from a value
obtained through such an access. In practice this means:
task.pidpreserves access information becausetask_structis annotated.task.values[i]preserves both the field projection and the array index whenvaluesis a fixed-size array.
MIR and compiler representation
No new Rust syntax beyond the attribute is needed, but rustc must carry the fact that a projection originates from an attributed aggregate far enough into codegen to choose the preserving intrinsics instead of ordinary GEP-like lowering.
One implementation strategy is:
- Parse
#[btf_preserve_access]as a built-in type attribute. - Record it in MIR and type metadata.
- When lowering
PlaceReffield and index projections in codegen, inspect the base aggregate type and dispatch to the preserving backend hooks.
Target and backend interactions
The attribute is intended for BPF targets, where BTF relocation emission is meaningful. On other targets, rustc accepts the attribute under the feature gate but emits ordinary accesses.
Alternative backends are not required to implement relocation emission for this RFC to be useful. The backend abstraction allows LLVM and GCC to provide the full feature immediately while other backends remain semantically correct. A backend that later gains BPF+BTF support can implement the same hooks without changing the Rust syntax.
In particular, a GCC backend implementation does not need a Rust-specific relocation design. It can reuse GCC's existing BPF CO-RE support while sharing the same Rust frontend attribute and MIR/codegen mechanism as the LLVM backend.
Drawbacks
This is a niche feature aimed at one target family and one ecosystem workflow. Yet it expands MIR and its entire code that deals with field and index projection with its concepts.
Rationale and alternatives
The main alternative is to emit BTF relocations in bpf-linker, which is a bitcode linker used exclusively for BPF targets.
That approach has the following disadvantages:
- By link time, the compiler has already lowered field access into offset-based operations. Reconstructing the original typed access path requires bpf-linker to traverse the IR.
- It prevents us from supporting ld type of linkers (e.g. binutils, lld) for BPF targets.
Prior art
clang and GCC support this feature through:
__attribute__((preserve_access_index))that can be applied to a type, e.g.
struct task_struct {
pid_t pid;
pid_t tgid;
} __attribute__((preserve_access_index));
- Built-in function
__builtin_preserve_access_indexthat can be applied to a single field access, e.g.
pid_t pid = __builtin_preserve_access_index(task->pid);
In clang, that attribute causes accesses to lower to the same family of LLVM intrinsics mentioned previously.
In GCC, the entire BPF CO-RE mechanism relies on the
__builtin_preserve_access_index built-in function.
__attribute__((preserve_access_index)) is equivalent to implicitly wrapping
all accesses to the type with the built-in.
Unresolved questions
- Is
btf_preserve_accessthe right long-term spelling for the feature gate and attribute, or should the name align more closely withpreserve_access_indexused in clang and GCC? - Apart from providing an equivalent to
__attribute__((preserve_access_index))(type-wide annotation), should we provide a way to annotate a single field access, like__builtin_preserve_access_indexin clang and GCC does?- The argument against is that the type attribute is much more widely used and even the kernel type headers [provided by libbpf][libbpf-headers] and generated by [bpftool][bpftool] apply the type attribute.
Future possibilities
If cranelift eventually introduces BPF support, the backend hooks introduced here provide a natural reference point for emitting equivalent relocations there as well.