Re: C aggregate passing (Rust kernel policy)

Ralf Jung
2025-02-27T18:33:09Z

Hi Linus,

On 27.02.25 00:16, Linus Torvalds wrote:
> On Wed, 26 Feb 2025 at 14:27, Kent Overstreet <kent.overstreet@linux.dev> 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