Linux 7.4 will be the first kernel in which one field of a driver structure written in kernel Rust can safely hold a reference to another field of the same structure. A 20-patch extension to the pin-init crate, merged in October 2026, lets the compiler prove at build time what the Devres and Revocable types have checked at run time on every device access since Linux 6.14. The article starts from the basics, then covers the mechanism, its costs, and the current restrictions.
Rust code is now a normal part of the mainline Linux kernel: the Nova GPU driver, the Android Binder driver and a growing set of driver abstractions are written in it. To a C engineer reading this code, one pattern stands out on every path that touches device memory: the access can fail. The driver asks for its own MMIO region, and the bindings hand back a value that must be checked before use, even when the driver knows the device is present. This article explains why kernel Rust was built that way, what the pattern costs, and how a series merged for Linux 7.4 replaces the run-time check with a compile-time proof. No Rust experience is assumed; C drivers are unaffected throughout.
The starting point: what Rust promises inside the kernel
Rust was brought into the kernel because a large share of serious driver bugs are memory errors — use-after-free, out-of-bounds access, races on freed data. Rust’s compiler can rule out whole classes of these, under a contract: ordinary code stays inside the rules, and anything that cannot (talking to hardware, calling C) is marked unsafe and wrapped behind a safe interface. In the kernel that contract takes a concrete form: driver code that contains no unsafe block must be unable to cause a memory error at all. The burden falls on the bindings — the Rust layer in rust/kernel/ that wraps the C driver core: whatever a driver author does with the safe interface must end as a compile error or a safe, defined failure.
Two Rust words are needed before going further. A reference, written &x, is a pointer the compiler tracks: it refuses to build code that could use the pointer after the value it came from is gone. Rust calls this tracked pointer a borrow — the reference borrows the value. A lifetime, written 'a, is the compiler’s name for the span of time during which a reference is valid. C has neither: how long a C pointer stays valid is a comment and a code review; in Rust it is a type-checked fact.
A device can disappear while its driver still exists
A driver is bound to a device when the bus matches them and probe succeeds, and from that point it owns resources that only make sense while the binding holds: an ioremapped register window, an IRQ line, DMA buffers. The binding can end at any time — hot-unplug, a write to the unbind file in sysfs, module removal — and driver-private data can outlive that moment. Touching a device resource after unbind is a use-after-free against hardware state.
On the C side, the managed-resource API handles the release half: devm_ allocations are torn down automatically after the driver’s remove callback. What C cannot do is stop a leftover pointer from being used after that teardown; the discipline is the engineer’s, and the failure surfaces later as a KASAN report. We covered that machinery in how Devres-managed device resources work. Kernel Rust, because of the promise above, cannot leave it as discipline: the bindings must encode “valid only while bound” so that safe code cannot violate it. There are only two ways to do that — check at run time, every time, or prove at compile time. When the device bindings shipped in Linux 6.14, the compile-time option could not yet be expressed, so kernel Rust took the run-time one.
Devres and Revocable: the run-time answer in kernel Rust since 6.14
The run-time answer is a container called Devres<T>. It owns the resource, and inside it the value sits in a Revocable<T> — a holder with a switch. On creation, Devres registers a callback with the C devm_add_action() machinery; at unbind the callback flips the switch, and every later attempt to reach the resource comes back empty. The documented usage, from the Linux 6.14 source, where iomem is an ioremapped MMIO region:
let devres = Devres::new(&dev, iomem, GFP_KERNEL)?;
let res = devres.try_access().ok_or(ENXIO)?;
res.writel(0x42, 0x0);Read it line by line. Devres::new() ties the mapped region to the device dev. try_access() asks, at run time, whether the device is still bound: a guard around the region if yes, nothing if revoked, and ok_or(ENXIO)? turns the nothing-case into an error return. Only then may the driver write its register, and every access repeats the question.
Three costs follow. The first is structural: every caller carries an error arm, including callers that run only while the device is certainly bound — probe, for instance, where the device is bound by definition. The error path exists to satisfy the type, and driver code accumulates failure handling for failures that cannot occur. The second is a run-time cost: the guard returned by try_access() is an RCU-style read guard, cheap but paid on each access, and on an MMIO-heavy path the accesses are where the driver spends its time.
The third cost forced the issue: some contexts cannot fail. Rust runs a destructor, called Drop, when a value goes out of scope — the teardown half of a C remove path — and Drop returns nothing, so a teardown that must write a device register has nowhere to put the error arm. An in-progress patch needed device access from inside Drop, and under the revocation model that meant a large refactor or error paths that can never run.
Mainline has been narrowing the gap — since 2025, Devres::access() returns the resource directly when the caller holds a &Device<Bound>, a device reference the type system knows is bound — but that bound context does not follow the data into long-lived structures. The structure a driver actually wants stores the device reference and the BAR side by side, dying together — and that structure is the one thing Rust has never allowed safe code to write.
Why Rust rejects the structure a C engineer writes first
In C the natural private-data struct holds a pointer to the device and the mapped BAR, and the engineer maintains the validity rules in their head. Writing the same shape in Rust means one field referencing another field of the same structure — a self-referential structure:
struct Example<'a> {
owner: Foo,
borrower: &'a Foo, // meant to point at the owner field above
}Safe Rust rejects wiring borrower to owner, for two concrete reasons. First, Rust moves values freely — returning a structure or pushing it into a collection copies it to a new address — after which borrower would still point at the old location of owner. Second, no lifetime in the language means “exactly as long as that sibling field”, so 'a has nothing valid to name. The userspace escapes are heap indirection — everything behind reference-counted allocations, which is what Devres itself did in 6.14 — or the self-reference helper crates; the kernel evaluated moveit, rental, yoke and ouroboros and rejected them, because they assume allocation is freely available, which kernel Rust forbids as an implicit operation, and several are unsound.
The kernel already owns the answer to the first problem. Kernel objects routinely contain locks and list heads that C code keeps pointers into, so the pin-init crate initializes kernel structures in place and pins them — guarantees they never move after initialization. That removes the move hazard. What remained missing was a way to tell the type system which field borrows which, in what order the fields must die, and how flexible each lifetime may be. That is what Gary Guo’s series adds.
What the merged series changes
The series — 20 patches, all inside rust/pin-init — was posted as v2 on 8 October 2026 and applied to mainline three days later, after a Kangrejos presentation and an upstreaming plan settled at the Linux Plumbers Conference. The visible change for a kernel Rust author is one piece of new syntax: under the #[pin_data] attribute that pin-init already uses, a lifetime that shares its name with a field now refers to that field. The minimal case looks like this:
#[pin_data]
struct Example {
borrower: &'owner Foo,
owner: Foo,
}There is no declared lifetime parameter: 'owner names the owner field below, and the macro arranges everything the compiler needs for borrower to reference it. The example from the series cover letter shows the shape a real driver takes:
#[pin_data]
struct MyDriver<'bound> {
dev: &'bound Device,
#[pin]
irq: irq::Registration<'bound, MyIrqHandler<'bound, 'bar>>,
bar: Bar<'bound, BAR_SIZE>,
}Here 'bound is an ordinary lifetime parameter — the period during which the device is bound — while 'bar names the sibling field bar: the IRQ handler borrows from the BAR of the same structure. The driver stores plain references where it previously stored Arc<Devres<...>>, and an access is a field read the compiler has already proven valid — no try_access(), no guard, no error arm. The Drop problem disappears too, because holding the reference is itself the proof.
Most of the series’ 2,246 lines generate checking, and two pieces of it are worth understanding.
The first is drop order. If borrower references owner, borrower must be destroyed first — an IRQ handler that can still fire must not outlive the mapping it reads. The macro therefore requires fields to be declared in drop order, borrowers before owners, and enforces it by generating a function that is never called, whose body declares locals of the field types in that order. The compiler takes types in signatures on trust but fully checks function bodies; a wrong order becomes a build failure.
The second is variance — the rule for when a longer-lived reference may stand in where a shorter-lived one is expected. Plain shared references allow it; types with interior mutability, such as a Mutex, do not, and the stricter rule spreads upward through every structure containing one. The series checks this for self-referential fields, and a field that is legitimately stricter must say so: #[uses('bar: invariant)], a spelling settled at Plumbers. The hardest case came from Nova’s command queue, where a Mutex made the queue’s lifetime strict and the strictness spread upward until it conflicted with the registration above it. Guo’s first workaround compiled, and was later shown to be unsound; the durable fix is the series’ last patch, existential lifetimes — a bound like exists<'cmdq>: 'a that fences a strict inner lifetime off. That unsound-but-compiling intermediate explains why the series took this shape: in macro code that manufactures safety guarantees, compiling has never meant sound.
The restrictions: who may use it, and what could break it
The merge comes with explicit fences, agreed at Plumbers. The basic form — a named-field lifetime like the examples above — is generally available. The advanced forms are not: the variance annotation and existential lifetimes are limited to pre-agreed users, named in the cover letter as the DRM jobqueue work and Nova’s command queue, so that if the design needs rework the dependent code stays small. This is unusual for kernel Rust: documentation is deferred to the following cycle, and adopting the advanced forms anywhere else means arguing on the mailing list first, not copying a pattern from Nova.
The maintenance risks are stated plainly by the author. The implementation grows the pin_data macro by roughly 1,500 lines of procedural-macro code, in a crate with few reviewers, and Guo acknowledged at Kangrejos that the result is demanding to review. The soundness argument also depends on Rust’s current pinning semantics: a future move to per-byte pinning would make the implementation unsound. This is the price of building, with today’s Rust plus macros, a feature the language cannot yet express directly.
Version position: the series is in mainline and first appears in a release as Linux 7.4. Linux 7.3 does not carry it, and no stable or longterm series does. It is a feature, not a fix, so expect no stable backports; a product on a 6.6 or 6.12 longterm kernel will not see this mechanism on its own branch at all.
Check what your own tree carries
Two checks place any tree you work with on this kernel Rust timeline. First, whether the runtime-revocation bindings are present at all:
raghu@techveda.org:~$ ls rust/kernel/devres.rsThe file exists from Linux 6.14 onwards; on 6.13 and earlier there are no Rust driver-core bindings to speak of. Second, whether the self-reference support is present:
raghu@techveda.org:~$ ls rust/pin-init/examples/selfref.rs
raghu@techveda.org:~$ git log --oneline -1 -- rust/pin-init/examples/selfref.rsThe example file ships with the series, so it exists only on mainline after the 7.4 merge window; on 7.3 or any stable tree the ls reports no such file. Where it exists, the log shows the series commits from October 2026, with 8f6607d1e51d as the enabling patch. A third measure: grep -rn "try_access" rust/ drivers/ counts the run-time revocation checks a tree still contains; watch it fall across releases.
What this means for an embedded product team
If your product ships a 6.6 or 6.12 longterm kernel, nothing changes on your branch; what changes is the code you will read next. A driver written against the 6.14-era bindings — Arc<Devres<Bar>> fields, try_access() everywhere — will not resemble its 7.4-era descendant, which holds plain borrowed fields. Forward-porting across that boundary is an idiom change, not a line-by-line update, and in-kernel Rust gives the same stability promise as the rest of the in-kernel API: none.
For reviewers, the hard part of this design is not Rust. It is the device lifetime model — bind, unbind, what remains valid in between — which every experienced C driver engineer already carries from devm_ resources and remove callbacks. Kernel Rust is making explicit the rules C left as discipline, and an engineer who understands why a pointer saved at probe must not be used after remove already understands what 'bound means. That device lifetime model is part of what TECH VEDA’s Linux training tracks build in course work.
Key takeaways
- Kernel Rust must guarantee that driver code without
unsafeblocks cannot cause memory errors, so device resources need protection the bindings enforce — not engineer discipline, as in C. - Since Linux 6.14 that protection has been run-time revocation:
Devres/Revocablemake every device access fallible, costing error arms that cannot fire, a guard per access, and a hard conflict withDrop, which cannot fail. - A 20-patch pin-init series by Gary Guo, merged for Linux 7.4, lets one field of a pinned structure safely reference a sibling field: the bound relationship is proven at compile time and the per-access check disappears.
- The mechanism names a sibling field as a lifetime under
#[pin_data]; soundness rests on pinning plus macro-generated drop-order and variance checks. - The advanced forms are restricted to pre-agreed users (DRM jobqueue, Nova command queue) and documentation lands next cycle; no released kernel carries it yet, and no stable backports are expected.
Related reading on TECH VEDA
- Devres: How Managed Device Resources Work in the Linux Kernel — the C-side
devm_machinery behind the same unbind problem. - Rust in the Linux Kernel: What It Means for Embedded Engineers — where Rust fits in kernel work and what to learn first.
Frequently asked questions
What is a self-referential structure in Rust?
A structure in which one field holds a reference to another field of the same structure. Safe Rust normally rejects this because moving the structure would leave the internal reference pointing at the old memory location. The pin-init mechanism makes it safe by guaranteeing the structure never moves after initialization and by checking field drop order and variance at compile time.
Which kernel release first ships safe self-referential structs in kernel Rust?
Linux 7.4. The 20-patch pin-init series was applied to mainline in October 2026. Linux 7.3 and all current stable and longterm series use the Devres/Revocable runtime model, and no stable backport is expected because this is a feature, not a fix.
Does this remove Devres from the kernel?
No. Devres remains in place and correct, and its access() method already avoids the runtime check where a bound-device context is available. The self-reference mechanism is an alternative that drivers adopt as their bindings migrate, starting with the users named in the series.
Does anything change for C drivers?
No. The devm_ managed-resource API and the driver model are untouched. The change affects how Rust drivers encode the same device lifetime rules that C leaves to engineering discipline.
Further reading
- Safe self-referential data structures in kernel Rust — LWN.net coverage of Gary Guo’s Kangrejos 2026 talk
- [PATCH v2 00/20] rust: pin-init: create self references safely — series cover letter
- Commit enabling self-referential support in pin_data
- rust: add devres abstraction — the introducing commit (Linux 6.14)
- rust: devres: implement Devres::access()
- Devres — kernel Rust documentation (v7.4-rc2)
- The pin_init crate — kernel Rust documentation
- Self-referential pin-init — Kangrejos 2026 slides, Gary Guo
- pin-init pull request #181 — the out-of-tree test suites for the series
- The Nova GPU driver — Rust for Linux project page



