The Rust Kernel's Rejection of READ_ONCE() and WRITE_ONCE() Macros
#Rust

The Rust Kernel's Rejection of READ_ONCE() and WRITE_ONCE() Macros

Tech Essays Reporter
4 min read

A recent patch series proposed adding Rust equivalents of the kernel's READ_ONCE() and WRITE_ONCE() macros, but the Rust kernel developers rejected the idea, favoring a more explicit approach using the Rust Atomic crate. This decision highlights a fundamental philosophical divergence in how concurrency is handled between C and Rust in the kernel, with implications for code clarity, developer understanding, and the future evolution of the kernel's memory model.

The Linux kernel's READ_ONCE() and WRITE_ONCE() macros are foundational tools for lockless algorithms and safe concurrent data access. With nearly 8,000 call sites for READ_ONCE() alone, they are deeply embedded in the kernel's C codebase. Given the ongoing integration of Rust into the kernel, one might expect corresponding Rust versions to emerge. However, a recent discussion reveals that the Rust kernel community is deliberately choosing a different path, one that prioritizes explicit semantics over mimicking C's historical patterns.

The READ_ONCE() and WRITE_ONCE() macros, introduced in kernel version 3.18, serve a specific purpose: they prevent the compiler from merging or refetching reads and writes, ensuring that a memory location is accessed exactly once per macro call. They also guarantee atomicity for the access, meaning a concurrent write won't result in a torn read. Crucially, these macros impose no ordering constraints on the CPU or compiler beyond what's necessary for the single access; they are not memory barriers. Their implementation in C often involves volatile casts, a tool that predates and exists somewhat outside the C11 memory model.

In late 2025, Alice Ryhl posted a patch series adding READ_ONCE() and WRITE_ONCE() to the kernel's Rust code. The implementation relied on Rust's read_volatile() and write_volatile() functions. The motivation was clear: there are places in the existing Rust code where volatile reads could be replaced with these more semantically precise macros, such as accessing the f_flags field of a struct file.

However, this proposal met with significant resistance from other key Rust kernel developers. Gary Guo suggested avoiding READ_ONCE() and WRITE_ONCE() entirely, advocating instead for the use of relaxed operations from the Rust Atomic crate. Boqun Feng expanded on this objection, arguing that the semantics of READ_ONCE() and WRITE_ONCE() are inherently ambiguous. They are sometimes used for atomicity and sometimes for preventing data races, but the intent isn't always clear. Using Atomic::from_ptr().load(Relaxed) forces the developer to be explicit about the desired guarantees, leading to clearer, more maintainable code. Feng described READ_ONCE()/WRITE_ONCE() as a "band-aid" solution that could hinder the development of a more principled approach to concurrent programming in Rust.

This perspective appears to have prevailed. Ryhl has paused her effort to add these macros, signaling a shift in the kernel's Rust philosophy. The implications of this decision are multifaceted. First, it creates a divergence in how concurrent data access is implemented in the kernel. As more Rust code is written for core kernel subsystems, developers will need to understand two distinct APIs for accessing shared data: the C READ_ONCE()/WRITE_ONCE() model and the Rust Atomic crate model. This could increase the cognitive load for developers working across the language boundary.

Second, the discussion has already spurred improvements in the existing C code. As Feng pointed out, some C code in the kernel assumes that plain writes are atomic in situations where the C standard makes no such guarantee. Peter Zijlstra acknowledged that such code should be updated to use WRITE_ONCE() properly, and the conversation even identified a missing READ_ONCE() call in the high-resolution timer code. This demonstrates a positive feedback loop where the design of Rust abstractions prompts a re-evaluation and refinement of the underlying C code.

Historically, there has been resistance to creating Rust interfaces that look substantially different from their C counterparts. The prevailing thought was that if Rust developers devised a better design, the C side should eventually be updated to match. In this case, if the Rust approach of using explicit Atomic operations is superior to the ambiguous READ_ONCE()/WRITE_ONCE() pattern, one could argue that the C side should follow suit. However, changing thousands of low-level concurrency primitives across the kernel's C codebase would be a monumental task, likely beyond practical feasibility.

This situation may represent a new normal for the kernel: a future where code in the two languages handles concurrency in fundamentally different ways. The Rust community's rejection of READ_ONCE() and WRITE_ONCE() is not merely a technical choice but a philosophical one, favoring explicit, type-safe, and unambiguous APIs over the more implicit and historically contingent patterns of C. This divergence will require kernel developers to cultivate a deeper understanding of both models to navigate the increasingly bilingual codebase effectively.

For those interested in the technical details, the discussion highlights the nuances of concurrent programming in low-level environments. The comments on the original LWN article delve into related topics, such as the unsoundness of using Rust's read_volatile() and write_volatile() for inter-thread communication without additional synchronization, and the performance implications of relaxed atomics on various architectures. These conversations underscore the complexity of the problem space and the careful consideration required when designing concurrency primitives for a system as critical as the Linux kernel.

Comments

Loading comments...