{
"$type": "site.standard.document",
"bskyPostRef": {
"cid": "bafyreiflurxy7w534tnzbf3s6l2hutn4yf5okadkvkr3iwhjpgjpptphdu",
"uri": "at://did:plc:ivbknywyskln22er3nkssdhl/app.bsky.feed.post/3mjpcm3irlot2"
},
"path": "/t/pre-rfc-btf-relocations/24161#post_18",
"publishedAt": "2026-04-17T14:25:09.000Z",
"site": "https://internals.rust-lang.org",
"tags": [
"one of the posts you linked",
"Swift stable ABI",
"@ais523",
"struct task_struct",
"pt_regs",
"try_to_wake_up",
"@frozen"
],
"textContent": "Jules-Bertholet:\n\n> vad:\n>\n>> It's not strictly necessary, because matching container type, its name and matching field name are sufficient for the relocation to work.\n>\n> 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.)\n\nGood 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.\n\nUnsure 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.\n\nJules-Bertholet:\n\n> vad:\n>\n>> Jules-Bertholet:\n>>\n>>> Is there any connection to/similarity with Swift stable ABI?\n>>\n>> 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.\n>\n> 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.\n\nThanks for the clarification, I get your point now.\n\nThe syntax I initially proposed for BTF relocations was a new attribute (it's still in the pre-RFC text):\n\n\n #[btf_preserve_access_index]\n #[repr(C)]\n pub struct foo {\n [...]\n }\n\n\nHowever, @ais523 proposed a new type representation `repr(btf)`:\n\n\n #[repr(btf)]\n pub struct foo {\n [...]\n }\n\n\nIf 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:\n\n\n #[resilent]\n #[repr(C)]\n pub struct foo {\n [...]\n }\n\n\nAnd similarly for Swift resilient types, assuming there is `repr(swift)`:\n\n\n #[resilent]\n #[repr(swift)]\n pub struct foo {\n [...]\n }\n\n\nThe 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?\n\n * 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.\n * Should we default to resilient types and have an opt-out annotation `#[freeze]`? That would defeat the idea.\n\n\n\nGiven that ABI resilience is still opt-in in `swiftc`, I'm leaning towards the first option.\n\nTo sum it up, yes, I think there is a possibility to have the same annotations.\n\nzackw:\n\n> 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.\n\nI couldn't find any mechanism like that in Dart.\n\nBut yes, Swift's ABI resilience and BTF relocations are similar on the surface, but they have some differences as well.\n\n * 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.\n * Swift's ABI resilience changes the way types are accessed. Access to BTF types is still based on offsets.\n\n\n\nGiven 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.\n\nJules-Bertholet:\n\n> vad:\n>\n>> 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.\n>\n> Logic bug, or UB?\n\nPotentially 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.\n\nThat said, in Rust we could prevent that by forbidding the value initialization.\n\ncomex:\n\n> 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.)\n>\n> 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.\n\nThere 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:\n\n\n struct task_struct {\n [...]\n struct sched_entity\t\tse;\n struct sched_rt_entity\t\trt;\n struct sched_dl_entity\t\tdl;\n [...]\n }\n\n\nHowever, 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.\n\nThat 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.\n\nTo 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.\n\nThe 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:\n\n\n use core::c_ulong;\n\n // Representation of the registers stored on the stack during a system call.\n #[repr(C)]\n pub struct pt_regs {\n pub r15: c_ulong,\n pub r14: c_ulong,\n pub r13: c_ulong,\n pub r12: c_ulong,\n pub rbp: c_ulong,\n pub rbx: c_ulong,\n pub r11: c_ulong,\n pub r10: c_ulong,\n pub r9: c_ulong,\n pub r8: c_ulong,\n pub rax: c_ulong,\n pub rcx: c_ulong,\n pub rdx: c_ulong,\n pub rsi: c_ulong,\n pub rdi: c_ulong,\n pub orig_rax: c_ulong,\n pub rip: c_ulong,\n pub cs: c_ulong,\n pub eflags: c_ulong,\n pub rsp: c_ulong,\n pub ss: c_ulong,\n }\n\n\n #[repr(C)]\n struct task_struct {\n [...]\n pid: i32,\n tgid: i32,\n [...]\n }\n\n // BPF expects programs to be functions in specific sections with integer\n // return codes.\n #[unsafe(no_mangle)]\n #[unsafe(link_section = \"kprobe/try_to_wake_up\")]\n pub fn my_kprobe(ctx: *mut core::ffi::c_void) -> u32 {\n match try_my_kprobe(ctx) {\n Ok(ret) => ret,\n Err(_) => 0,\n }\n }\n\n // Convenience helper that allows us to return `Result`.\n fn try_my_kprobe(ctx: *mut core::ffi::c_void) -> Result<u32, i32> {\n let regs: *mut pt_regs = ctx.cast();\n\n // Retrieve the first argument of `try_to_wake_up` of type `task_struct`.\n let task: *const task_struct = unsafe { (*regs).rdi as *const _ };\n\n // Inspect the `task` - check the fields we are interested in, log them etc.\n let pid = unsafe { bpf_probe_read_kernel(&(*task).pid)? };\n let tgid = unsafe { bpf_probe_read_kernel(&(*task).tgid)? };\n info!(&ctx, \"kprobe called: pid: {}, tgid: {}\", pid, tgid);\n\n Ok(0)\n }\n\n\nWith 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)]`:\n\n\n #[repr(btf)]\n struct task_struct {\n pid: i32,\n tgid: i32,\n }\n\n\nThis 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.\n\nSo 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.\n\nais523:\n\n> 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.\n\nI think my current PoC implementation is quite aligned with this.",
"title": "[Pre-RFC] BTF relocations"
}