Re: C aggregate passing (Rust kernel policy)

From: Ralf Jung
Date: Thu Feb 27 2025 - 13:33:24 EST


Hi Linus,

On 27.02.25 00:16, Linus Torvalds wrote:
On Wed, 26 Feb 2025 at 14:27, Kent Overstreet <kent.overstreet@xxxxxxxxx> wrote:

This is another one that's entirely eliminated due to W^X references.

Are you saying rust cannot have global flags?

The way you do global flags in Rust is like this:

static FLAG: AtomicBool = AtomicBool::new(false);

// Thread A
FLAG.store(true, Ordering::SeqCst); // or release/acquire/relaxed

// Thread B
let val = FLAG.load(Ordering::SeqCst);
if val { // or release/acquire/relaxed
// ...
}
println!("{}", val);

If you do this, the TOCTOU issues you mention all disappear. The compiler is indeed *not* allowed to re-load `FLAG` a second time for the `println`.

If you try do to do this without atomics, the program has a data race, and that is considered UB in Rust just like in C and C++. So, you cannot do concurrency with "*ptr = val;" or "ptr2.copy_from(ptr1)" or anything like that. You can only do concurrency with atomics. That's how compilers reconcile "optimize sequential code where there's no concurrency concerns" with "give programmers the ability to reliably program concurrent systems": the programmer has to tell the compiler whenever concurrency concerns are in play. This may sound terribly hard, but the Rust type system is pretty good at tracking this, so in practice it is generally not a big problem to keep track of which data can be accessed concurrently and which cannot.

Just to be clear, since I know you don't like "atomic objects": Rust does not have atomic objects. The AtomicBool type is primarily a convenience so that you don't accidentally cause a data race by doing concurrent non-atomic accesses. But ultimately, the underlying model is based on the properties of individual memory accesses (non-atomic, atomic-seqcst, atomic-relaxed, ...).

By using the C++ memory model (in an access-based way, which is possible -- the "object-based" view is not fundamental to the model), we can have reliable concurrent programming (no TOCTOU introduced by the compiler) while also still considering (non-volatile) memory accesses to be entirely "not observable" as far as compiler guarantees go. The load and store in the example above are not "observable" in that sense. After all, it's not the loads and stores that matter, it's what the program does with the values it loads. However, the abstract description of the possible behaviors of the source program above *does* guarantee that `val` has the same value everywhere it is used, and therefore everything you do with `val` that you can actually see (like printing, or using it to cause MMIO accesses, or whatever) has to behave in a consistent way. That may sound round-about, but it does square the circle successfully, if one is willing to accept "the programmer has to tell the compiler whenever concurrency concerns are in play". As far as I understand, the kernel already effectively does this with a suite of macros, so this should not be a fundamentally new constraint.

Kind regards,
Ralf



That seems unlikely. And broken if so.

IOW: if you're writing code where rematerializing reads is even a
_concern_ in Rust, then you had to drop to unsafe {} to do it - and your
code is broken, and yes it will have UB.

If you need to drop to unsafe mode just to read a global flag that may
be set concurrently, you're doing something wrong as a language
designer.

And if your language then rematerializes reads, the language is shit.

Really.

Linus