[Pre-RFC] BTF relocations

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")]
#[repr(C)]
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")]
#[repr(C)]
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 which use the C type representation (repr(C)). It is guarded by the btf_preserve_access feature gate.

Codegen

LLVM provides dedicated intrinsics for emitting BTF relocations:

  • @llvm.preserve.array.access.index for index projections from an array.
  • @llvm.preserve.struct.access.index for field projections from a struct.
  • @llvm.preserve.union.access.index for field projections from a union.

IRBuilder provides methods that language frontends can use for creating the intrinsic calls:

  • CreatePreserveArrayAccessIndex
  • CreatePreserveStructAccessIndex
  • CreatePreserveUnionAccessIndex

The implementation strategy is to expose corresponding hooks on the codegen backend abstraction:

  • btf_preserve_array_access_index
  • btf_preserve_struct_access_index
  • btf_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.pid preserves access information because task_struct is annotated.
  • task.values[i] preserves both the field projection and the array index when values is 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 PlaceRef field 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_index that 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_access the right long-term spelling for the feature gate and attribute, or should the name align more closely with preserve_access_index used 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_index in 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.

4 Likes

Here is code implementation, currently only for the LLVM backend:

I'm going to prepare a repository with practical eBPF code examples that work on top of this feature and post it here as well.

A couple of thoughts.

First, this is, in effect, about changing Rust's type layout algorithms in order to place the fields at the same offsets that the Linux kernel would place them. As such, I'm wondering whether the new attribute should be #[repr(btf)], given that it's mutually exclusive with other repr attributes and has the same purpose of changing where in a struct its data is stored.

The other thought is to wonder about the interaction of this with compile-time introspection. For example, macros like offset_of! normally assume that the offset of a field within a struct is known at compile time, whereas if it's being specified by relocations, that is not the case. There are existing exceptions where a field offset might not be known at compile time (e.g. a #[repr(C)] struct containing a u8 and a dyn Trait can't determine the offset of its second field because it depends on its alignment, which for dyn Trait can vary at runtime); but all the existing exceptions are for unsized types. Should that imply that BTF types should not be Sized? (Thinking about it, they are logically unSized because the size may change after the compile has already happened, due to the relocations, and thus the size is not known at compile time.)

5 Likes

Currently, in Rust, it is possible to replace every field access by a use of offset with a compile-time constant determined with offset_of! (except for field with a dyn Trait unsized tail). That's no longer true with this proposal. So this proposal is making deep changes to the foundations of the language itself, its operational semantics. This is way more than just a proposal for a bit of new syntax, and it doesn't just expand rustc implementation details like MIR, it expands the core language in a way that's relevant for unsafe code authors. The proposal needs to explain what the new operational semantics are, and how they are supposed to work in a tool like Miri.

At the very least, it seems like offset_of! on such fields should fail to compile. But the changes have to go deeper than that; the assumption of a fixed layout is deeply rooted everywhere in the compiler and the language.

4 Likes

How does this all work in C? Because C also assumes you can use offsetof on any struct field (and sizeof on any complete struct type, and everything implied by that).

2 Likes

I guess this kind of attribute has very similar consequences for the struct as the ARM scalable vectors that the sized hierarchy work is looking at. BTF provides relocations for both field offsets and the struct size, so this means that task_struct is Sized but not const Sized under the terminology introduced here. Its size is constant at runtime, but not known at compile time.

7 Likes

I like the idea. I'm not really attached to the naming used in C (I already added the btf prefix to the attributes, since I find the original Clang/GCC naming too generic). The good thing about having a separate repr is that it lets us enforce a C-compatible ABI.

First of all, let's clarify one thing. The offsetof macro is part of stddef.h in libc and therefore cannot be used in eBPF programs. In musl, the definition lowers either to __builtin_offsetof or to a field access:

What's available on BPF targets is a direct call to __builtin_offsetof. And that indeed does not emit relocations.

struct inner {
  int x;
  int y;
};

struct outer {
  int a;
  struct inner b;
} __attribute__((preserve_access_index));

unsigned long plain_offsetof(void) {
  return __builtin_offsetof(struct outer, b.y);
}

lowers to:

define dso_local i64 @plain_offsetof() #0 !dbg !7 {
  ret i64 8, !dbg !11
}

I do agree that offset_of! on relocatable types/fields should fail to compile, and we shouldn't repeat the mistake of C compilers silently emitting code that is not going to work. The same goes for core::mem::size_of, core::mem::align_of, and probably many other macros and const functions that I haven't thought of yet. I will take some time to go through the entire core API and figure out what else we need to add to that list.

At the same time, I think we could add an intrinsic btf_field_byte_offset that serves as a replacement for offset_of, but emits a relocation for annotated types when building for BPF targets with BTF. On LLVM, it would lower to the @llvm.bpf.preserve.field.info(..., FIELD_BYTE_OFFSET) intrinsic call. On GCC, it would call the __builtin_preserve_field_info(..., FIELD_BYTE_OFFSET) built-in function.

I actually managed to get it working already (again, only for LLVM; I haven't started on the GCC implementation yet), and the change doesn't seem difficult:

If you agree with this approach, I'm happy to update the RFC with it.

Miri does not work for BPF targets for unrelated reasons, and for similar reasons it does not run on embedded targets either: BPF binaries can't be executed on regular operating systems; they can be executed only by virtual machines designed for the BPF ISA, including the one provided by the Linux kernel. We don't really have any solution for testing BPF binaries in Aya right now, apart from extracting testable code into crates that can be used for unit tests with std on host targets. The same approach works for Miri as well.

Apart from the Linux kernel BPF VM, there are some user-space implementations, including rBPF and Solana's SBPF, but we haven't really tried using them to run unit tests. That said, if that ever works, I guess we could look into getting Miri running on one of them as well.

But I don't think that should be a requirement for this proposal.

The approach of treating #[repr(btf)] types as Sized and not const Sized sounds great to me. But given that the RFC mentioned above has not been accepted and this distinction is not yet implemented, the only option for now seems to be treating them as not Sized. I will try implementing that and see whether it has any consequences that make it unworkable.

Why? Can't it just compile down to emitting a relocation so that it evaluates to the correct offset?

I believe that would be sufficient for most use-cases.

1 Like

As a more general comment, I do think the sized hierarchy work is the right way to approach this issue. Perhaps one could expose some entirely different way to access btf data? For example, a macro that takes a struct name and field name and returns the offset. The program can then use that value on a raw pointer explicitly.

3 Likes

A few questions:

  • Can a relocation affect whether two fields overlap (e.g. if a struct changes to a union)?
  • Can a relocation change an array index?
  • Can a relocation affect the number of pointer dereferences necessary to access a field?
  • Is it ever possible to construct one of these structs? Or does the potential for additional fields Rust doesn't know about mean construction must be forbidden?
  • Do we need an analogue of #[link_name] for struct and field names?
  • What is the failure mode like? If you make a mistake or run on an unsupported kernel version, and the relocations aren't sufficient to make offets or types line up, is the verifier guaranteed to reject the program?
  • Is there any connection to/similarity with Swift stable ABI?
2 Likes

I didn't mean to suggest that this should actually be implemented in Miri before an RFC can be accepted, sorry if I was unclear. What I meant is that the semantics of this type need to be described at a level of abstraction and detail that it is clear how they would be implemented in Miri. This is crucial; we should not add new pieces to the language that we don't know how to describe at the level of the Abstract Machine. "It emits this kind of machine code" is not an acceptable specification for a language feature.

I am saying this with my t-opsem hat on. It is literally the responsibility of my team to ensure we have a proper Abstract Machine with an operational semantics for Rust. We're happy to help you work this out (come find us on Zulip), but an RFC without a proper opsem discussion is not going to be accepted.

FWIW this will be very non-trivial because Rust has generics. Today one can write a generic function that takes an arbitrary T (implicitly: T: Sized) and then obtain its size as a compile-time constant. This is not something we can break. That's why people are saying that the Sized hierarchy work is a prerequisite for supporting this kind of a type.

4 Likes

Because the offset_of intrinsic is const:

pub const fn offset_of<T: PointeeSized>(variant: u32, field: u32) -> usize;

and I'm under the impression that we want to treat BTF-relocatable field access as non-const. Nobody said this explicitly, but if we want #[repr(btf)] types to be Sized but not const Sized, then emitting a relocation in a const intrinsic seems inconsistent with that principle. That's why I'm proposing a new BTF-aware intrinsic:

pub fn btf_field_byte_offset<T: PointeeSized>(variant: u32, field: u32) -> usize;

Do you think relocation emission could still be modeled as a const intrinsic somehow?

The sized hierarchy idea definitely seems very helpful for this RFC. I'll try to help make some progress on it.

Yes, I think that aligns with my idea of the btf_field_byte_offset intrinsic. It could definitely be exposed through a macro.

No. The type of the container (struct, union) containing the field is encoded in the relocation.

No. For a type:

struct foo {
    int arr[3];
}

An access to a specific index (e.g. foo->arr[3]) emits a relocation with that index encoded. What the loader does when reading such a relocation is find the 3rd element of the array field in the target layout and apply whatever offset adjustment is needed if new fields were inserted before arr.

No. A BTF relocation only adjusts layout-dependent properties along a fixed access path; it does not rewrite the path itself. So it can change field offsets, sizes, or compatibility checks, but it cannot change how many pointer dereferences are required to perform the access.

If a target type evolved in a way that would require a different number of dereferences, that would be a compatibility break rather than something BTF relocations can adapt to.

Excellent question.

I tried compiling a C program that initializes a struct annotated with __attribute__((preserve_access_index)), and it compiles. I haven't tested it against a real kernel yet, but it seems unsound.

If the program constructs such a value using one kernel layout and then gets loaded on a kernel with a different layout, relocated accesses could interpret that locally constructed value using the target kernel's layout instead of the layout it was actually initialized with.

Forbidding the construction in Rust absolutely makes sense to me.

Good question.

It's not strictly necessary, because matching container type, its name and matching field name are sufficient for the relocation to work.

But given that this is all done on C kernel types with C-style snake_case names, defining such structures in Rust code will often trigger lint issues. For someone who really wants to keep Rust-style type names, such an attribute could be helpful.

I'm still leaning towards saying "no", since using C-like type names and suppressing these lints is a pretty common thing to do in bindgen, and implementing a dedicated attribute sounds like overkill to me.

There are two possible failure modes.

If the relocated access ends up outside the valid bounds that the verifier can prove, the verifier should reject the program.

But if the relocated access still stays within some valid memory region while pointing to the wrong field, then this can become a silent logic bug instead. So I would not claim that the verifier is guaranteed to reject every semantically wrong relocation outcome.

Not really. Actually I don't even think Rust ABI should be used for BTF relocatable types. #[repr(btf)] should enforce the C ABI, I'm going to update the RFC and examples accordingly.

Thanks for the explanation. I must be honest: I hadn't thought about the Abstract Machine description while writing the RFC and working on the prototype. I need some time to reason about it, and I will definitely reach out once I'm ready.

I'm fine with making support for BTF relocations depend on the Sized hierarchy work if there is strong opposition to introducing #[repr(btf)] types that don't work with generics.

1 Like

Presumably, the intrinsic would become conditionally const (leveraging https://github.com/rust-lang/rfcs/pull/3762).

In one of the posts you linked, I see mention of version suffixes in the names, which are ignored for relocations. Should that mechanism work in Rust as well? (If not, then an alternative mechanism, e.g. attribute, is presumably necessary.)

To clarify the point I was driving at: Swift's ABI supports making limited changes to struct layouts while preserving compatibility, so it's conceptually similar to BTF relocations. I was wondering if any new syntax or high-level language APIs we add to support BTF relocations, could also be leveraged to support Swift interop—even if the backend implementation is very different.

Logic bug, or UB?

1 Like

It may not be as hard of a blocker as that makes it sound.

First, you may be able to start with an unstable implementation even if the Sized hierarchy is a blocker for stabilization. This is the case for scalable vectors, which already have a prototype implementation in the compiler despite the Sized hierarchy not yet having one.

Second, you may be able to come up with a subset of the feature that doesn't depend on the Sized hierarchy at all. This should be easier for BTF types than for scalable vector types. The prototype of scalable vector types is pretty hacky: the compiler treats these types as Sized (despite the lack of a compile-time constant size), with the intent to downgrade to runtime-sized once that becomes a thing. This is because scalable vectors need to be passed by value and treated as Copy, which isn't supported for !Sized types. For BTF types, though, passing by value is not that essential, so you could probably start by making them !Sized, with the intent to upgrade to runtime-sized once that becomes a thing. This would be backwards-compatible and wouldn't break any existing generic code, so maybe it would even be stabilizable? (but I'm only speculating on that.)

The innovation would be having a type be !Sized but still allowing struct field access on it. You still need to work out the proper operational semantics for that, but that seems reasonably orthogonal to the Sized hierarchy.

Unfortunately, this would probably only work for structs. For arrays, Rust array types ([T]) require the element to be Sized, so it probably makes sense to wait for the Sized hierarchy before trying to relax that.

2 Likes

I think the opsem here is to have what is in effect a global variable (static used like a const) that stores the offset of a particular field, for field projection to be implemented by pointer arithmetic using the offset from the global variable, and for other field accesses to be implemented in terms of field projection. One advantage of doing things this way is that it should desugar pretty easily into MIR or even surface Rust, so the opsem aspects would be confined to stating/implementing the lowering rather than needing to add new opsem rules.

2 Likes

Yeah that is plausible -- it's basically a struct layout that's defined at program start time rather than at compile time (and might be different for different program executions).

But of course that is a huge departure from everything in Rust so far assuming that field offsets (to sized fields) are constant, so there's still a good chance for interesting surprises when pushing such a change through the compiler. For instance, this would be the first time that a field projection would have to be disallowed in const fn.

2 Likes

If I remember correctly, something like this is also how Swift (or possibly Dart) handles dynamic linking of libraries that provide generics, so there might be some useful lessons there.

Good point, sorry for missing it. Supporting version suffixes sounds like something we should do and the way clang implemented it makes sense to me. I will try to implement it and report once done.

Unsure about alternatives. If we introduce a custom attribute instead, that would likely require more compiler and language-design work to carry the versioning information through the pipeline. The clang approach seems less intrusive and already has an existence proof.

Thanks for the clarification, I get your point now.

The syntax I initially proposed for BTF relocations was a new attribute (it's still in the pre-RFC text):

#[btf_preserve_access_index]
#[repr(C)]
pub struct foo {
    [...]
}

However, @ais523 proposed a new type representation repr(btf):

#[repr(btf)]
pub struct foo {
    [...]
}

If we end up sticking with the first idea, a separate attribute, it could be shared between BTF relocations and Swift ABI resilience, if we give it a more neutral name (e.g. resilent). I could imagine it being used as follows in BPF programs:

#[resilent]
#[repr(C)]
pub struct foo {
    [...]
}

And similarly for Swift resilient types, assuming there is repr(swift):

#[resilent]
#[repr(swift)]
pub struct foo {
    [...]
}

The question is whether such a syntax makes sense to developers who are used to Swift. From what I gather, Swift makes all public types resilient if the -enable-library-evolution flag is provided to swiftc. One can opt out for individual types using the @frozen attribute. The question is: what kind of assumptions would work best for repr(swift) types in Rust?

  • Should we default to non-resilient types and require people to annotate them with #[resilent]? That would be compatible with the idea of sharing the attribute with BTF types.
  • Should we default to resilient types and have an opt-out annotation #[freeze]? That would defeat the idea.

Given that ABI resilience is still opt-in in swiftc, I'm leaning towards the first option.

To sum it up, yes, I think there is a possibility to have the same annotations.

I couldn't find any mechanism like that in Dart.

But yes, Swift's ABI resilience and BTF relocations are similar on the surface, but they have some differences as well.

  • Swift ABI resilience works at runtime. BTF relocations are applied at load time: the program loader patches the bytecode, and the BPF virtual machine is unaware of the concept of relocation, because it operates in terms of offsets.
  • Swift's ABI resilience changes the way types are accessed. Access to BTF types is still based on offsets.

Given these differences, the implementations will likely be very different. One implementation similarity that comes to my mind would be extending PlaceRef::project_field and PlaceRef::project_index, but then the further steps would be completely different.

Potentially UB. A wrong relocation can also remain a silent logic bug, but it may become UB if it causes the program to perform an invalid access, for example by reading a value with the wrong type, size, or alignment assumptions. I still need to experiment with some real examples and see how verifier reacts to such programs.

That said, in Rust we could prevent that by forbidding the value initialization.

There is one problem with BTF types starting as !Sized - some kernel structures are nested by value. For example, struct task_struct has the following fields:

struct task_struct {
    [...]
    struct sched_entity		se;
    struct sched_rt_entity		rt;
    struct sched_dl_entity		dl;
    [...]
}

However, I think we could still start an experimental implementation with BTF types being !Sized, with a limitation that only primitive fields (including pointers to other BTF-relocatable structs) can be accessed. Kernel types accessible directly from a BPF program context can be accessed only as raw pointers.

That would still not solve the task_struct case in full. Accesses such as task->pid and task->tgid would fit within that subset, but accesses crossing an inline composite field boundary, such as task->se.vruntime, would remain unsupported.

To elaborate on that, BPF programs usually access kernel data through a context pointer, which in case of kprobe programs (one of the most common types) is pt_regs. Such programs usually limit themselves to inspecting a small number of fields, or sending them to the user-space, rather than traversing large portions of nested kernel state.

The vast majority of BPF programs interacting with kernel types, stripping away all the abstraction provided by Aya, look similar to the following example of a kprobe attached to the try_to_wake_up function:

use core::c_ulong;

// Representation of the registers stored on the stack during a system call.
#[repr(C)]
pub struct pt_regs {
    pub r15: c_ulong,
    pub r14: c_ulong,
    pub r13: c_ulong,
    pub r12: c_ulong,
    pub rbp: c_ulong,
    pub rbx: c_ulong,
    pub r11: c_ulong,
    pub r10: c_ulong,
    pub r9: c_ulong,
    pub r8: c_ulong,
    pub rax: c_ulong,
    pub rcx: c_ulong,
    pub rdx: c_ulong,
    pub rsi: c_ulong,
    pub rdi: c_ulong,
    pub orig_rax: c_ulong,
    pub rip: c_ulong,
    pub cs: c_ulong,
    pub eflags: c_ulong,
    pub rsp: c_ulong,
    pub ss: c_ulong,
}


#[repr(C)]
struct task_struct {
    [...]
    pid: i32,
    tgid: i32,
    [...]
}

// BPF expects programs to be functions in specific sections with integer
// return codes.
#[unsafe(no_mangle)]
#[unsafe(link_section = "kprobe/try_to_wake_up")]
pub fn my_kprobe(ctx: *mut core::ffi::c_void) -> u32 {
    match try_my_kprobe(ctx) {
        Ok(ret) => ret,
        Err(_) => 0,
    }
}

// Convenience helper that allows us to return `Result`.
fn try_my_kprobe(ctx: *mut core::ffi::c_void) -> Result<u32, i32> {
    let regs: *mut pt_regs = ctx.cast();

    // Retrieve the first argument of `try_to_wake_up` of type `task_struct`.
    let task: *const task_struct = unsafe { (*regs).rdi as *const _ };

    // Inspect the `task` - check the fields we are interested in, log them etc.
    let pid = unsafe { bpf_probe_read_kernel(&(*task).pid)? };
    let tgid = unsafe { bpf_probe_read_kernel(&(*task).tgid)? };
    info!(&ctx, "kprobe called: pid: {}, tgid: {}", pid, tgid);

    Ok(0)
}

With BTF relocations supported, the only difference would be a concise definition of task_struct, containing only the fields we intend to use, annotated with #[repr(btf)]:

#[repr(btf)]
struct task_struct {
    pid: i32,
    tgid: i32,
}

This is exactly the kind of access pattern I think an initial !Sized experiment can support. By contrast, an access such as (*task).se.vruntime would still be out of scope, because it requires traversing an inline nested struct by value.

So the point is not that !Sized fully solves CO-RE for task_struct, but rather that it may already cover a useful experimental subset for types one retrieves directly from the BPF context.

I think my current PoC implementation is quite aligned with this.

1 Like

It's actually quite common to have task->se.vruntime accesses in C BPF programs, so supporting this from the get go would be essential to make this useful for real-world applications.

Re: naming. "preserve_access_index" naming we use in C is quite opaque and not very meaningful. Everyone just remembered and use it without asking questions, but I always felt like having it named as "btf_relocatable" or something along those lines ("relocatable" is the key here, IMO) would be best. Just my 2 cents on naming.

Oh, and I don't know if this is in scope or not, but it would be great to be able to express all of the libbpf/src/bpf_core_read.h at master · libbpf/libbpf · GitHub in Rust through whatever is the most natural way. Checks for type/field existence and enum value retrieval is critical for some real-world applications, not just relocatable field accesses.

1 Like