[Pre-RFC] BTF relocations
Sorry for the silence. I took some time to re-think the entire approach and at the end, together with tamird, we came up with way simpler solution that will not require any changes to the field projection, and aligns with what @kornel proposes.
kornel:
Does it have to be a feature of structs and arrays?
Could it instead be expressed as a macro or intrinsic function that gives an offset to use with
ptr::read? (which could be sufficient to make getters and setters for something struct-like that isn't an actual struct)Or maybe it could be implemented as getters and setters on extern opaque types?
It wouldn't be as neat as 1st class structs that work with literals and pattern matching, but might be sufficient to interact with kernel structs treating them as an API.
Yes, after experimenting with that idea, I'm convinced this might be a good direction.
For reference, clang has the following intrinsic that allows to request information about BTF-relocatable type:
uint32_t __builtin_bpf_preserve_field_info(void *member_ptr, uint64_t info_kind);
where the info_kind can be:
FIELD_EXISTENCE(0) - returns a0or1indicating whether a field exists.FIELD_OFFSET(1) - returns an offset of the field, and emits a BTF relocation, so the loader can patch the offset.FIELD_SIZE(2) - returns a size of the field.
We could expose the same functionality in Rust through three intrinsics (each of them having a return type appropriate in Rust's context):
pub fn relocatable_field_byte_offset<T: PointeeSized>(variant: usize, field: usize) -> usize
pub fn relocatable_field_byte_size<T: PointeeSized>(variant: usize, field: usize) -> usize
pub fn relocatable_field_exists<T: PointeeSized>(variant: usize, field: usize) -> bool
And... that would be it on Rust compiler's side. No changes to MIR and the field projection. No direct exposure of preserve_access LLVM intrinsics (at least to the users).
Why?
With BTF-relocatable types, we are not sure whether a field exists during a runtime. Therefore, a field access does not provide any guarantees of soundess. That goes against Rust's guarantees of safety.
What goes in line with Rust's safety guarantees, however, is a way to access a relocatable field only if we meet certain circumstances - that the field exists, and that the size of the field is as we expect.
The mentioned intrinsics would provide building blocks for safe abstraction for accessing relocatable fields. To be precise - such safe abstraction could be provided outside of Rust standard library, it could be done in Aya or a dedicated third-party library. The first step towards safety would be the following function with an error type:
#[derive(Debug, Eq, PartialEq)]
pub enum RelocatableFieldAccessError {
SizeMismatch { expected: usize, actual: usize },
ProbeRead(i32),
}
#[inline(always)]
pub fn relocatable_field_read<T>(ptr: *const T) -> Result<Option<T>, RelocatableFieldAccessError> {
if relocatable_field_exists(ptr) {
let expected = core::mem::size_of::<T>();
let actual = relocatable_field_byte_size(ptr);
if expected == actual {
let offset = relocatable_field_byte_offset(ptr);
// SAFETY: We trust LLVM to emit a correct BTF relocation for
// that offset, and we trust BPF loader libraries to patch it
// accordingly.
let field = unsafe {
bpf_probe_read_kernel(&*ptr.add(offset))
.map_err(|code| RelocatableFieldAccessError::ProbeRead(code))?
};
Ok(Some(field))
} else {
Err(RelocatableFieldAccessError::SizeMismatch { expected, actual })
}
} else {
Ok(None)
}
}
And then a BTF-relocatable structure with getters that use the function:
#[repr(C)]
struct task_struct {
pid: i32,
tgid: i32,
}
impl task_struct {
pub fn pid(&self) -> Result<Option<i32>, RelocatableFieldAccessError> {
relocatable_field_read(::core::ptr::addr_of!(self.pid))
}
pub fn tgid(&self) -> Result<Option<i32>, RelocatableFieldAccessError> {
relocatable_field_read(::core::ptr::addr_of!(self.pid))
}
}
Generation of such getters could be hidden behind a proc macro.
That would also solve the problem of different kernel versions removing or renaming fields, without having to rely on version suffixes mechanism from clang that was described by @anakryiko and mentioned by @Jules-Bertholet.
The only point from @kornel's proposition which I'm unsure about is using extern opaque types. We would need some way of declaring its layout and letting Rust (and a backend, like LLVM) to generate the debug info for it. The weakness of the code example I provided above is that the task_struct type is a sized struct. An open question is how can it be marked as !Sized, perhaps that brings us back to the #[repr(btf)] idea.
Updated experimental branch: GitHub - vadorovsky/rust at btf-relocations-v2 · GitHub
Discussion in the ATmosphere