Before you start#
Review kernel C and lifetime, atomics and ordering, and arm64 boot. This lesson concerns upstream Linux v6.12, whose Rust support is optional and limited by configuration, toolchain and architecture. It does not imply that arbitrary Rust crates, drivers or target architectures are supported. No kernel or Rust toolchain build is claimed.
Learning goals#
Explain why safe Rust abstractions can contain unsafe implementations, trace a kernel Arc reference through clone and drop, and identify architectural assumptions that must be checked before porting a driver.
Source walkthrough#
- Rust Arc
- Refcount gives shared allocation lifetime, not mutual exclusion
- Unsafe FFI
- Wrapper must preserve C lifetime, errno and callback invariants
- Architecture
- Ordering, page size, MMIO and DMA mapping depend on target
- Toolchain check
- rustavailable diagnoses prerequisites, not runtime support
The kernel's Rust crate is no_std: it does not use the ordinary hosted Rust standard library as if kernel code were a userspace program. rust/kernel/lib.rs exposes kernel-specific wrappers, and its configuration guards conditionally compile some modules. The crate's introductory comment says that a needed C API should first be ported or wrapped there rather than bypassing the crate. Rust is an additional language within the same kernel, not a separate privilege boundary: an unsafe wrapper with a broken invariant can corrupt kernel memory.
An Arc<T> in rust/kernel/sync/arc.rs represents shared reference-counted ownership. It allows shared lifetime across owners; it does not automatically make arbitrary mutation race-free. The implementation's Clone increments a kernel refcount_t; Drop decrements it and frees the allocation when the count reaches zero. This short exact v6.12 excerpt from Drop is GPL-2.0-only; surrounding comments and closing braces are omitted:
let is_zero = unsafe { bindings::refcount_dec_and_test(refcount) };
if is_zero {
// The count reached zero, we must free the memory.
The unsafe call reaches a C binding; its safety relies on the stated invariant that the existing Arc owns a live reference. The boolean tests whether this decrement reached zero. Only that path reconstructs and drops the Box from the allocation in the following lines. The code comments also warn that after a nonzero decrement another CPU may drop the last reference, so this code must not dereference the object after that point. The Clone implementation relies on refcount_inc saturating rather than wrapping to zero; its new Arc owns the newly acquired reference. Safe callers rely on these internal unsafe obligations being correct.
The matching increment is the Clone implementation. In rust/kernel/sync/arc.rs, v6.12, SPDX GPL-2.0 (exact, unabridged):
impl<T: ?Sized> Clone for Arc<T> {
fn clone(&self) -> Self {
// INVARIANT: C `refcount_inc` saturates the refcount, so it cannot overflow to zero.
// SAFETY: By the type invariant, there is necessarily a reference to the object, so it is
// safe to increment the refcount.
unsafe { bindings::refcount_inc(self.ptr.as_ref().refcount.get()) };
// SAFETY: We just incremented the refcount. This increment is now owned by the new `Arc`.
unsafe { Self::from_inner(self.ptr) }
}
}
The comments state the invariant: the existing Arc supplies the live reference, and the increment is then owned by the new handle. The comment also says refcount_inc saturates, but the excerpt does not show that C function, its memory ordering or any access to the stored data.
A shared lifetime example#
Imagine one owner A with count one. A clones its Arc, creating B and incrementing to two. A drops its handle, decrementing to one; B can still use its reference under the type's rules. When B drops, the count reaches zero and the allocation is released. If A and B drop concurrently, an atomic reference-count operation determines which decrement sees zero. Neither caller may use its own moved or dropped handle afterward. The scenario describes the implementation's ownership model, not an observed kernel execution or a complete model of saturation and all memory ordering.
- Clone
refcount_inc on the live allocation; the new Arc owns that reference
- Drop, nonzero
refcount_dec_and_test returns false; this owner must not touch the object again
- Drop, zero
This owner was last; Box::from_raw rebuilds the allocation and drops it
- Mutable contents
The count keeps memory alive but does not synchronize field writes
A reference count keeps an object allocated; it is not a lock around fields inside the object. If two contexts need to mutate shared state, an appropriate synchronization abstraction and its context rules are still necessary. For DMA, foreign devices are not Rust borrowers checked by the compiler; a wrapper must model mapping, completion and ownership correctly. Likewise, unsafe is not permission to ignore bounds, lifetime, alignment or concurrency. A safe API must make its preconditions enforceable or expose the obligations explicitly to its unsafe caller.
Architecture is part of the contract#
An x86-64 QEMU build and an arm64 board may differ in boot protocol, interrupt controller, device enumeration, memory coherence, page size and available instructions. Neither a successful x86 build nor pointer-width similarity proves an arm64 driver correct. A portable driver uses kernel interfaces instead of assuming CPU physical addresses equal DMA addresses, treating MMIO as ordinary RAM, or depending on x86 ordering. Endianness and alignment are properties to check explicitly when interpreting device formats. Compare arm64 setup with the chosen x86 path, and check architecture-specific Kconfig dependencies and device-tree or ACPI descriptions.
- Borrow checker
- Checks Rust references and moves inside safe code
- Arc refcount
- Keeps the allocation alive until the last owner drops
- Device DMA
- Needs mapping, completion and unmapping per the hardware contract
- Architecture
- Needs ordering, alignment and I/O accessors checked for each target
The v6.12 Rust build requires a supported Rust toolchain and kernel configuration. Check the version-specific Rust quick start and build diagnostics; do not assume a modern distribution's default toolchain matches this historical tag. Source reading and the following ownership model work without Rust, a kernel build, or a device.
A bridge from C ownership to Rust#
A Rust value has an owner, and moving the value transfers ownership unless the type has copy semantics. After an ordinary move, the original binding cannot be used as though it still owned the value. A shared borrow, written conceptually &T, gives temporary shared access subject to the type's rules; a mutable borrow &mut T gives exclusive access for its lifetime. A borrow is not a physical lock and says nothing by itself about DMA from an external device. These rules make certain use-after-free and aliasing mistakes harder to express in safe Rust, provided every unsafe boundary upholds its contract.
Result<T, E> represents success carrying a value or failure carrying an error. The ? operator propagates an error to the caller after the appropriate conversion; it is not a silent success and does not bypass cleanup of values already owned by the current scope. Drop provides destruction behavior when an owned value goes out of scope or is explicitly dropped. In a kernel wrapper, that destruction may release a reference, registration or allocation; exactly what it does comes from the wrapper's implementation. A trait describes operations and associated items a type implements; a generic function or structure is parameterized over types and can require traits. Neither generics nor a trait guarantee a hardware operation succeeds.
Pin expresses that a value whose invariants rely on its address must not be moved out of its pinned location through safe operations. Pinning does not itself allocate storage, lock the object, or keep a device from DMA. Kernel intrusive lists and callbacks can require stable addresses, so the v6.12 kernel uses pin-aware initialization facilities to establish fields and their invariants in place. A partially initialized object needs correct unwind if a later initialization step fails. unsafe marks code that must uphold extra obligations not checked by the compiler; it does not disable every check or make the safe API's callers responsible for undocumented invariants.
Arc sharing, type bounds, and ordering#
The kernel Arc stores a pointer to an inner allocation containing a refcount_t and the data. Its Clone increments the reference count, then constructs the new owning handle; Drop decrements and destroys the allocation on the zero result. The implementation explicitly describes saturation of the underlying reference count to prevent overflow wrapping to zero. Saturation is a defensive property, not permission to leak references or an unlimited-resource guarantee. The final decrement has ordering requirements supplied by the kernel refcount API; ordinary increment of a known-live reference does not by itself publish separately initialized data or replace the synchronization needed for mutable contents. The kernel's refcount ordering documentation specifies the relevant operations; do not generalize from atomic_t operations with the same apparent arithmetic.
The type-level cross-thread contract is explicit in arc.rs. This exact v6.12 GPL-2.0 excerpt omits the safety explanation immediately above each line:
unsafe impl<T: ?Sized + Sync + Send> Send for Arc<T> {}
T: ?Sized permits appropriate dynamically sized values; Sync + Send are constraints on the underlying type. The unsafe implementation asserts that moving an Arc<T> between threads is sound only under those conditions. The neighboring Sync implementation has the same bounds: a shared reference to an Arc can be cloned on another thread, and the final owner can drop the inner value there. The source explains why T needs both properties. These bounds are evidence of a carefully specified abstraction, not proof that every external C callback or DMA operation follows Rust's aliasing rules.
In the existing A/B example, cloning from a live A acquires a new reference for B. Dropping A after the clone leaves B's reference. If a writer wants to mutate data visible to both, it needs a synchronization mechanism appropriate to the data and execution context; the count alone protects allocation lifetime. A race between final drops is handled by the refcount operation, but a stale raw pointer recovered after all references are gone is invalid even if it once pointed into an Arc. An unsafe conversion from a raw pointer must satisfy the exact into_raw/from_raw contract and balance ownership; guessing that a pointer is valid because its numeric address is nonzero is insufficient.
A real v6.12 Rust PHY driver#
drivers/net/phy/ax88796b_rust.rs is an upstream Rust PHY driver for specific Asix devices, not a generic Rust network driver. Its module_phy_driver! declaration registers several driver types and device IDs through kernel abstractions. A PHY handles physical-link functions, and is not the same object as the network interface, packet queue, or entire Ethernet controller. A compatible hardware ID, build configuration and supporting abstractions are required for binding; reading the source does not prove a machine has that PHY.
The following short v6.12 excerpt is from PhyAX88772A::read_status in ax88796b_rust.rs, SPDX GPL-2.0, copyright FUJITA Tomonori; surrounding code is omitted:
dev.genphy_update_link()?;
if !dev.is_link_up() {
return Ok(0);
}
The update call runs first, and ? returns its error to the caller if it fails. The condition then checks the updated link state; an inactive link returns a successful zero status without attempting the later register read. This short-circuit does not prove that an active link has working packet delivery.
In PhyAX88772A::read_status, the driver first invokes genphy_update_link()?. An error propagates rather than being treated as an up link. If the link is down, it returns Ok(0) without reading the speed fallback. For a link that is up, it reads the Clause 22 basic mode control register, derives speed and duplex from bits, then reads link partner ability and may resolve auto-negotiated link mode when enabled and complete. A failed register read propagates through ?. The source comments describe a specific older-switch workaround; do not copy its assumptions to another PHY or interpret a valid register read as proof that packets traverse the whole network.
Suppose the driver enters read_status with the link down. The update succeeds, the link test returns false, and the function returns success without the later BMCR read. If the link is up but the register read fails, the error returns before speed and duplex can be established from that read. If the read succeeds, the chosen speed/duplex fields describe the source branch and can later be refined by auto-negotiation resolution. The mutable &mut phy::Device expresses the abstraction's access rules; the wrapper and underlying C/driver implementation must still uphold its lifetime, locking and hardware contracts. A Rust reference alone cannot validate a broken device or firmware response.
Toolchain, configuration and kernel FFI#
The v6.12 minimum-version script reports Rust 1.78.0 and bindgen 0.65.1; the versioned changes documentation lists the same minimums. bindgen depends on libclang. These are historical minimums, not a promise that arbitrary newer combinations, targets or installed standard-library source components will work. The v6.12 quick start describes the additional components and make rustavailable, which diagnoses availability in the selected build environment. A positive availability check does not enable CONFIG_RUST, guarantee the desired architecture and Kconfig dependencies are supported, build a driver, or execute it.
The kernel crate uses #![no_std] and kernel allocation and synchronization facilities. Kernel allocations can fail and use allocation flags; the minimal sample's Vec::push(..., GFP_KERNEL)? illustrates a fallible allocation and error propagation. GFP_KERNEL may sleep and is therefore inappropriate in contexts where sleeping is forbidden. The exact choice of flags and locking must follow the API and calling context. Safe wrappers around generated C bindings are responsible for translating error returns, raw pointers, ownership and callback lifetime into an enforceable Rust interface. A negative C errno, an ERR_PTR and a null pointer are not interchangeable representations; the wrapper must interpret the particular C API contract. A Rust Result makes the error path explicit to its caller but cannot make a falsely specified FFI contract correct.
x86-64, arm64 and RISC-V portability#
The main lab's x86-64 guest, an arm64 machine and a RISC-V system have distinct boot, interrupt and memory-ordering contracts. For portable shared-memory code, use Linux locking, atomics and barriers according to the Linux kernel memory model; a test that happens to pass on x86-64 cannot establish ordering on arm64 or RISC-V. Ordering is not merely compiler optimization: other CPUs and devices may observe operations in a different order without the required synchronization. Conversely, inserting an arbitrary barrier cannot repair a missing lifetime reference or a wrong DMA mapping.
Endianness and page configuration are also explicit contracts. Common deployments of all three architectures can be little-endian, but supported configurations and device formats must be checked rather than inferred from the CPU name. Linux arm64 can be built with different page sizes; RISC-V and x86 configurations have their own constraints. A descriptor's fixed little-endian field is decoded with the relevant conversion regardless of host CPU. A pointer crossing a page boundary, alignment-sensitive register access, and a device's DMA segment limits require testing under the selected configuration. Misaligned ordinary loads have architecture- and context-dependent behavior and performance; MMIO registers can impose stricter access widths and must use the kernel's I/O accessors rather than an unaligned Rust reference.
A device address returned by the DMA API is not a CPU virtual address and need not equal a physical address. An IOMMU or non-coherent system can make a shortcut that appeared to work on an x86-64 virtual device fail on an arm64 or RISC-V platform. Rust ownership can describe who is allowed to hand a buffer to a device, but unsafe glue must ensure mappings, cache synchronization, completion and unmapping follow the hardware contract. A portable claim requires the actual architecture, kernel config, driver and test evidence, not merely successful compilation on one machine.
Read-only toolchain observation lab#
On your authorized Linux host or in a disposable guest, inspect installed tools without installing anything or changing the running kernel:
uname -m
uname -r
command -v rustc >/dev/null 2>&1 && rustc --version || echo 'rustc unavailable'
command -v bindgen >/dev/null 2>&1 && bindgen --version || echo 'bindgen unavailable'
command -v clang >/dev/null 2>&1 && clang --version | head -n 1 || echo 'clang unavailable'
The output identifies host tools and architecture, not the configuration of a v6.12 target kernel. A missing tool is a valid outcome; do not install or modify anything for this exercise. Separately, if you have a clean disposable v6.12 source checkout and wish to run the build system's diagnostic, use a temporary output directory. The following writes only its newly created temporary directory and removes it on exit; it does not load a module or build a kernel:
: "${LINUX_SRC:?Set LINUX_SRC to a clean disposable Linux v6.12 checkout}"
case "$(git -C "$LINUX_SRC" describe --tags --exact-match HEAD 2>/dev/null)" in v6.12) ;; *) echo 'checkout is not exactly v6.12'; exit 1;; esac
rust_lab_out=$(mktemp -d) || exit 1
trap 'rm -rf -- "$rust_lab_out"' EXIT
make -C "$LINUX_SRC" O="$rust_lab_out" rustavailable
A diagnostic error identifies a prerequisite or configuration problem in that environment, not a kernel runtime defect. An available toolchain does not demonstrate that the PHY driver is enabled or hardware is present. Source-only alternative: compare the pinned minimum-version script with the PHY's read_status branches described above. Deliver either a tool/version table or a source table with link-down, register-error and successful-read outcomes; separately name one architecture assumption that requires a real target test.
Practice#
Run this userland reference-count model with Python 3. It represents the ordinary non-saturated sequence only; Python integers and this sequential program do not implement the kernel's concurrency or refcount semantics:
python3 - <<'PY'
count = 1
for event in ('clone B', 'drop A', 'drop B'):
count += 1 if event.startswith('clone') else -1
assert count >= 0
print(event, 'count', count, 'free' if count == 0 else 'live')
PY
Expected counts are 2, 1 and 0; free occurs only on the last step. Add an extra drop as a counterexample: the assertion detects a negative count, illustrating an invalid model operation, not kernel refcount behavior. Deliver a table listing owner A, owner B, count and allocation lifetime, and add a column naming the lock or other synchronization you would need for mutable shared contents. The program creates no files.
Knowledge check#
- What invariant permits
Arc::cloneto increment the counter? - Why must the drop implementation avoid accessing the object after a nonfinal decrement?
- Does
Arcmake device DMA or mutable shared fields automatically safe? - Why does the PHY driver return before its BMCR read on a down link, and what happens on a failed register read?
- What does
rustavailableestablish about a v6.12 build?
Answers#
- The existing
Arcowns a valid reference to the allocation. The implementation increments the counter before giving ownership of the new reference to the clone. - Another CPU may concurrently drop the final reference and free the allocation after this decrement. The implementation's comments explicitly identify that race.
- No. Shared allocation lifetime is only one invariant. Mutation needs appropriate synchronization, and DMA requires device mapping, completion and ownership rules that the Rust borrow checker cannot infer for hardware.
- The source checks link state after updating it and returns
Ok(0)when down; if the later read fails on an up link,?propagates that error before deriving speed and duplex. - It checks toolchain prerequisites in the chosen environment. It does not prove that CONFIG_RUST or the PHY driver is enabled, that a kernel was built, or that a device ran.
Checkpoint#
Submit the ownership table and one architecture assumption to investigate in a particular driver. Ready means the last drop alone frees in the ordinary model, unsafe implementation invariants are named, and lifetime is distinguished from mutual exclusion. Re-read the Clone and Drop implementations if any owner can access freed storage. This check is not certification.