BTF relocations - #3966
Conversation
BTF, the BPF Type Format, encodes type information for both the running
Linux kernel and compiled eBPF programs. An eBPF object can carry
relocation records that describe field and aggregate accesses in terms
of BTF types instead of fixed offsets; at load time, the loader compares
the program's BTF with the kernel's BTF and rewrites those accesses to
the correct offsets for the target kernel. This mechanism is often
referred to as "CO-RE relocations" or "BTF relocations".
`offset_of` always folds to a plain layout constant and does not
preserve enough information for BTF CO-RE relocation emission. As a
result, it is not suitable for relocatable field queries on BPF targets.
Add three explicit intrinsics for BTF field metadata queries:
* `btf_field_byte_offset`
* `btf_field_byte_size`
* `btf_field_exists`
The user-facing BTF relocatable type support remains behind the
`btf_relocations` feature gate, so use of this experimental CO-RE
surface requires an explicit nightly opt-in.
These intrinsics provide a frontend surface for CO-RE relocations.
Unlike `offset_of`, they remain visible to backend codegen and can lower
to relocatable field-info queries instead of immediate layout constants.
For LLVM, lower these intrinsics through `@llvm.bpf.preserve.field.info`
with the corresponding query kind. The necessary
`@llvm.preserve.{struct,array,union}.access.index` chain is constructed
internally during lowering, but it is not exposed as part of the
user-facing API.
On targets or backends without BTF relocation support, fall back to the
ordinary layout-computed result: the field offset for
`btf_field_byte_offset`, the field size for `btf_field_byte_size`, and
`true` for `btf_field_exists`.
The language-level design for this feature is proposed in
rust-lang/rfcs#3966.
BTF, the BPF Type Format, encodes type information for both the running
Linux kernel and compiled eBPF programs. An eBPF object can carry
relocation records that describe field and aggregate accesses in terms
of BTF types instead of fixed offsets; at load time, the loader compares
the program's BTF with the kernel's BTF and rewrites those accesses to
the correct offsets for the target kernel. This mechanism is often
referred to as "CO-RE relocations" or "BTF relocations".
`offset_of` always folds to a plain layout constant and does not
preserve enough information for BTF CO-RE relocation emission. As a
result, it is not suitable for relocatable field queries on BPF targets.
Add three intrinsics for BTF field metadata queries:
* `btf_field_byte_offset`
* `btf_field_byte_size`
* `btf_field_exists`
Their availability is hidden behind the `btf_relocations` feature gate.
Unlike `offset_of`, they remain visible to backend codegen and can lower
to relocatable field-info queries instead of immediate layout constants.
For LLVM, lower these intrinsics through `@llvm.bpf.preserve.field.info`
with the corresponding query kind. The necessary
`@llvm.preserve.{struct,array,union}.access.index` chain is constructed
internally during lowering, but it is not exposed as part of the
user-facing API.
On targets or backends without BTF relocation support, fall back to:
* The field offset for `btf_field_byte_offset`.
* The field size for `btf_field_byte_size`.
* `true` for `btf_field_exists`.
The language-level design for this feature is proposed in
rust-lang/rfcs#3966.
0a822d7 to
2ec6577
Compare
2ec6577 to
54a1461
Compare
| #[inline] | ||
| pub fn pid(&self) -> Option<&i32> { | ||
| self.has_pid().then(|| { | ||
| let offset = core::btf::field_byte_offset!(task_struct, pid); | ||
| let ptr = self as *const task_struct as *const u8; | ||
|
|
||
| // SAFETY: the BTF relocation says that `se.vruntime` exists in the | ||
| // target layout, and the returned offset is relative to `task_struct`. | ||
| Some(unsafe { &*(ptr.add(offset) as *const i32) }) | ||
| }) |
There was a problem hiding this comment.
So a typo in a field name can result in simply turning this into a None and never taking the "active" code path, right? Since the relocation is functionally a set of runtime checks, and we would have to assume we take the field name for granted, and the entire reason to use these relocations is the struct definition per se is actually external to the program...?
There was a problem hiding this comment.
Yes, that's right. Although instead of saying "is actually external", I would say that the struct definition is a reference point for the runtime checks.
To be fair, I proposed a field projection (like in clang) in my pre-RFC, but given the complexity, I'm not pursuing it for now.
|
Syntax questions and my nits aside, this will want a sponsor from T-lang for proceeding as a nightly experiment (which will later come back to re-informing this RFC). I have nominated it so that if you do not find a sponsor in your own time they eventually see it appear on their agenda and one of them can either take up the gauntlet or they can decline it. And this comment as explanation for why I nominated it, of course. This may be a very eventual eventually, so I do suggest finding a sponsor in your own time. |
|
We talked about this in the lang call today. We were interested to hear what the people working on @rust-lang/rust-for-linux think about this, as well as what those working on reflection (@oli-obk, @scottmcm, rust-lang/rust-project-goals#406) and projection (@BennoLossin, @dingxiangfei2009, @tmandry, rust-lang/rust-project-goals#390) think. If this generally makes sense to people, I'll be happy to champion a lang experiment here. cc @lqd @Nadrieril |
|
Also needs input from sized hierarchy side. cc @davidtwco |
| This overlaps with the accepted [Field Projections project goal][field-projections], | ||
| which is exploring virtual places as a general mechanism for custom field | ||
| projection. This RFC deliberately does not depend on that work: it provides the | ||
| low-level BTF field metadata queries needed for CO-RE today, while leaving a | ||
| future field-projection-based ergonomic surface open. | ||
|
|
||
| Providing the field-info queries proposed in this RFC does not rule out | ||
| exploring this alternative in the future. On the contrary, Clang provides both | ||
| explicit field-info builtins and field projection. It makes sense to treat these | ||
| as separate RFCs. |
There was a problem hiding this comment.
I think this RFC could be written to make it more clear that we expect field access and offset_of! to work in the future. For example, the offset_of! section is phrased as if we could never support offset_of! with btf relocations.
There was a problem hiding this comment.
Good point, I'm going to rephrase this (while still making it clear it's out of scope of this RFC).
|
|
||
| `offset_of!` is intentionally a constant layout query. It does not preserve the | ||
| field identity needed to emit a BTF relocation. Reusing it would either silently | ||
| produce non-relocatable code or require changing the meaning of an existing |
There was a problem hiding this comment.
How is the new macro with the same API change anything? Why isn't that also silently producing wrong things when e.g. a field doesn't exist at all in the relocation table?
There was a problem hiding this comment.
How is the new macro with the same API change anything?
When I submitted the pre-RFC, where I initially proposed emission of BTF relocations in regular field projections and offset_of! , the main feedback was that it goes against the principle that Sized types should have layout known at compile time, which is not the case with BTF-relocatable types.
Then the discussion went towards the hierarchy of Sized types RFC #3729 which would eliminate this problem. But then we also agreed treating BTF-relocatable types as !Sized and allowing access to them only with dedicated macros/intrinsics could be a good way to keep the feature rolling without hard dependency on #3729.
Why isn't that also silently producing wrong things when e.g. a field doesn't exist at all in the relocation table?
Not sure if I got this question right - are you asking about what field_byte_offset! and field_byte_size! macros produce when a field does not exist? That's a great question and actually made me realize that they should probably return an Option:
core::btf::field_byte_offset!(Carrier, field.path) -> Option<usize>
core::btf::field_byte_size!(Carrier, field.path) -> Option<usize>
There was a problem hiding this comment.
Jup, that makes sense now, thx
There was a problem hiding this comment.
Then the discussion went towards the hierarchy of Sized types RFC #3729 which would eliminate this problem
Just checking, as I'm not familiar with how BTF-relocatable types are used in practice: it seems like the property of having a size that is not known at compile time but known to be constant at runtime (depending on the hopefully future differentiation between const Sized vs Sized types from a follow-up to #3729) would be enough for you to model field offsets on BTF-relocatable types that are only known at runtime, and you won't need to model additional, separate, properties (e.g. with your own traits), right?
There was a problem hiding this comment.
@lqd I'm so sorry for missing this question!
Just checking, as I'm not familiar with how BTF-relocatable types are used in practice: it seems like the property of having a size that is not known at compile time but known to be constant at runtime (depending on the hopefully future differentiation between
const SizedvsSizedtypes from a follow-up to #3729) would be enough for you to model field offsets on BTF-relocatable types that are only known at runtime and you won't need to model additional, separate, properties (e.g. with your own traits), right?
That's right, under the Sized hierarchy RFC, BTF-relocatable types could implement SizeOfVal, I don't see any need for any additional trait.
One open question, which would remain even after the Sized hierarchy is accepted, is how do we handle direct field accesses to fields that don't exist during runtime. Do we panic? If so, are we going to generalize the macros (e.g. field_byte_offset! -> Option<usize>), that I'm proposing in this RFC, to become a fallible way of accessing potentially non-existing fields, that would apply to all of SizeOfVal but not Sized types?
Thank you, @traviscross, for the discussion and for offering to champion this. I appreciate the lang team's interest. I'm going to modify the RFC according to the comments later today. |
The RFC proposes the `btf_relocations` feature gate, which provides a `#[repr(Btf)]` representation and `core::btf` field-info macros that emit BTF (BPF Type Format)[0] CO-RE (Compile Once, Run Everywhere)[1] relocations. It also documents the relationship to `offset_of!`, ordinary field projection, and LLVM BPF lowering. This feature was originally proposed as a pre-RFC[2]. [0] https://docs.kernel.org/bpf/btf.html [1] https://nakryiko.com/posts/bpf-portability-and-co-re/ [2] https://internals.rust-lang.org/t/pre-rfc-btf-relocations/24161/25
It's better to emit a compile error.
54a1461 to
6292c33
Compare
|
This PR was rebased onto a different master commit. Here's a range-diff highlighting what actually changed. Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers. |
Move it under `#relocatable-field-access` section. Make it clear that we don't rule the `offset_of!` support out in the future.
Make the other macros sound by returning `None` when a field does not exist. That makes `field_exists!` redundant, so remove it.
Keep code examples only in the guide-level explanation.
|
I believe I’ve addressed all the review feedback. I have an implementation ready on my branch (https://github.com/vadorovsky/rust/tree/vad/btf-relocations). @traviscross, would you be comfortable championing this as a lang experiment, or do we need more feedback and explicit approvals? In case we can proceed, should I create a tracking issue, or should rust-lang/rust#158412 be converted into one? |
|
I'll champion it. Go ahead and create a tracking issue if you would. |
|
Given that the long-term plan is to implement support for transparent operation with ordinary rust field projections, how terrible would it be to keep the |
Thank you! I created the tracking issue and the libs-team ACP:
I'm going to send a PR later today.
It probably depends on how long
LLVM 23 support just got merged and it unblocks all of these points. The first two points should be fixed automatically, the last one will require some work on supporting linking with binutils (so far the only ld-type of linker supporting BPF). It would be also great to add BPF support to lld, mold, wild etc. If the work on ld linking gets done faster than dynamic I could imagine, that in situation when:
I would probably be in favor of trying to stabilize Of course it would be great to avoid such scenario by making all three done in similar time. I'll try to see how I can be helpful in
Yes, there is one difference between macros and a plain field projection. Macros return an It's not uncommon for Linux kernel code to remove, rename or add fields in important structs, and for BPF programs to want to handle layouts of such structs for different kernel versions. Even if the field projection is implemented, the macros will be useful for handling such cases. Also, some kernel structs have fields that depend on kernel configuration. For example, there is a field called https://elixir.bootlin.com/linux/v7.1.5/source/include/linux/sched.h#L1329-L1331 Writing a BPF program that would panic on absence of this field wouldn't be great, and it would be preferable to write code like: // This would be generated by a proc-macro in Aya.
impl task_struct {
pub fn maybe_cg_dead_lnode(&self) -> Option<&llist_node> {
core::btf::field_byte_offset!(task_struct, cg_dead_lnode).map(|offset| {
let ptr = self as *const task_struct as *const u8;
Some(unsafe { &*(ptr.add(offset) as *const llist_node) })
})
}
}
// User's code that doesn't panic.
if let Some(cg_dead_lnode) = task_struct.maybe_cg_dead_lnode() {
// handle `PREEMPT_RT`
} else {
// no `PREEMPT_RT`
} |
|
A'ight. In that case the only other suggestion I have is that this module should be under |
View all comments
The RFC proposes the
btf_relocatable_typesfeature gate, that provides a#[repr(Btf)]representation and low-level field-info intrinsics that emit BTF (BPF TypeFormat) CO-RE (Compile Once, Run Everywhere) relocations.It also documents the relationship to
offset_of!, ordinary field projection and LLVM BPF lowering.This feature was originally proposed as pre-RFC.
Rendered