MaybeUninit may cause a system reset if parity/ECC enabled

I'd like to note that on some processors that have parity-check or ECC enabled, reading an uninitialized value will cause a system reset.

This information seems to be not explicitly worked out on the documentation page: MaybeUninit in std::mem - Rust

E.g. on this Renesas CPU:
https://www.renesas.com/en/document/apn/using-ecc-memory-configuration-and-error-injection-and-detection-tsi107
search for "The consequence to skipping this step is the
immediate notification of a multi-bit error condition ..."

In case you use MaybeUninit as performance optimization, I wonder if the small performance optimization is worth the risk of causing a system reset by accidentally reading uninitialized memory.

1 Like

Why should it need to be stated explicitly? Reading an uninitialized value is already immediate UB.

15 Likes

You are right that "undefined" covers "system reset".
On the other hand, the page states for reading an integer type: " Reading the same uninitialized byte multiple times can give different results. This makes it undefined behavior to have uninitialized data in a variable even if that variable has an integer type, which otherwise can hold any fixed bit pattern:"
I think this does not cover all the possible behavior...

Note that Rust does very much permit reading uninit bytes... as uninit bytes. That is, you're allowed to copy, say, [MaybeUninit<u8>; 1024] from one place to another, even if its contents are uninitialized, without even needing to use unsafe.

So, it sounds like that CPU requires that something initializes its memory array with a bunch of writes, possibly just zeroing everything out for simplicity. Sounds like it's expected to happen fairly early on after it's powered up. But if it doesn't intend to mean that you need to initialize everything all at once, then presumably stack memory would need to be explicitly initialized before handing it over to a Rust program. Likewise, a global allocator (if provided) would need to initialize the memory it allocates before handing it over to Rust code. (Assuming that copying uninit bytes would otherwise not be allowed.)

If (as it sounds) that CPU expects that memory is initialized shortly after powering up, then it seems unsurprising that you can't run Rust code before the CPU has fully started up. There's all sorts of weird environments out there, and it isn't the Rust docs' job to explain what could happen on half-booted systems.

There certainly are a lot of useful details not mentioned on the docs, of course. It might be worth noting that uninit data that has genuinely unstable reads on the hardware level instead of the compiler level has blocked the mythical freeze intrinsic, IIRC. There's lots of information about Rust scattered across Zulip channels and GitHub issues that hasn't found its way to the docs yet.

12 Likes

I can't reconcile @jhpratt and @robofinch answers. If reading uninitialised memory is immediate UB why it is allowed to copy (i.e., read) [MaybeUninit<u8>; 1024] using only safe Rust? Shouldn't be impossible to have UB when using only safe Rust? I would expect a panic, not UB (that would also "fix" OP problem).

1 Like

There are two notions related to the idea of "initialized".

  1. An "initialized byte" is a byte that is set to some value from 0 to 255. (Technically there are also stuff about pointer provenance, but let's not talk about that.)
  2. A "valid value" of some type is a value whose byte pattern satisfies the validity requirements imposed by that type. For types such as u8 or i32, the validity requirement is "every byte must be an initialized byte". For the type MaybeUninit<T>, the validity requirement is "nothing".

Sometimes people inaccurately use the term "uninitialized value" to mean "an invalid value due to some byte being uninitialized even though it's supposed to be initialized".

It is UB to "read" or otherwise "produce" an invalid value.

6 Likes

The difference essentially comes down to colloquial speech throwing out details and making simplifying assumptions. In this case, reading an entirely uninitialized value is immediate UB for very, very nearly every type in Rust, with the exception of MaybeUninit (and unions similar to it), and, technically, zero-sized types. So, makes sense that people commonly elide a mention of that edge case.

To be precise, the all-uninit byte pattern is invalid for all of Rust's primitive/atomic types (aside from the zero-sized types ! and ()), as well as for structs, tuples, and arrays containing such types. Most enums also disallow the all-uninit byte pattern. Certain unions are the exception (as well as types composed of such unions, like [MaybeUninit<u8>; N]).

However, do note that I used the phrase "entirely uninitialized value" above. Partially uninitialized values are much, much more common. A type like (u8, u16) has one padding byte in it, which acts like a MaybeUninit<u8>; that is, padding bytes are allowed to be uninitialized. You can even define a type like

#[repr(align(8192))]
#[derive(Clone, Copy)]
struct Foo {
    field: u8,
}

which has 8191 padding bytes that are allowed to be uninitialized. Assuming that the OS and hardware might support "genuinely" uninitialized pages that do not have stable values when read, since a common page size is 4096 bytes, a (byte representation of a) value of that type might have a full page of "genuinely" uninitialized data in it. It is required to be sound to move around a value of type Foo in memory. If you just move around a Foo value normally, the compiler should be smart enough to only bother to move the single u8 field, but it must also be perfectly sound for Rust code to explicitly copy around all 8192 bytes of a Foo value, which would result in reading that uninitialized data.

10 Likes

I feel obliged to link to a classic post about uninitialised memory in Rust - "What The Hardware Does" is not What Your Program Does: Uninitialized Memory by Ralf Jung.

4 Likes

I'm guessing this is nothing to do with Rust. Any language, including assembler must trigger the same event on the same kind of read.

On power up your memory is full of random junk. Whatever state the RAM bits come up in. Which most likely do not satisfy the ECC checker. There is no way the ECC can know what state that is until you try and read it. Then the check fails.

That is what I expect, does it sound reasonable?

6 Likes

So, if I understand this correctly, reading the padding of a struct is UB (and in fact it is impossible in safe Rust) but reading bytes from a MaybeUninit<u8> array is OK because MaybeUninit is special: you don't know what you will get but you will get something and that's not UB.

UB in safe Rust is impossible and everybody is happy (except the OP but Rust is not supposed to maintain hardware invariants).

Thank you all for the clarifications.

You may write OS in Rust, too. Then you have to think about that corner-case.

It's different thing from what MaybeUninit is in Rust, though. Understandable why they use the same word, but also pretty unfortunate because semantic is different.

You need to consider that reading memory in Rust is a typed operation: you read some data as an instance of some type.

Reading the uninitialized memory (be it the padding of a struct, the contents of a MaybeUninit::uninit(), the memory of a fresh allocation, etc etc) as some type that does not allow that memory to be uninitalized (say, a u8) is UB. However doing the same with a type that allows that memory to be uninitialized (e.g. MaybeUninit<u8>) is safe.

So, re-reading your statement:

If you read it as a bunch of u8 or other must-be-initialized types, then yes. If you read them as MaybeUninit<u8> then no.

Reading bytes from a MaybeUninit<u8> array that's actually uninitialized (i.e. created with MaybeUninit::uninit() or similar) is no different from reading padding bytes.

Reading bytes as (as opposed to from) a MaybeUninit<u8> array is OK however.

10 Likes

That's an example of undefined behavior, intended to cover the common mistake of "it's just an integer". That doesn't mean that other (worse) things cannot happen.

2 Likes

The key point here is that what the CPU considers "uninitialized" and what the Rust memory model considers "uninitialized" do not need to be identical. Most of the time a Rust-"uninitialized" memory location (that is valid to be read/written to) is actually going to have a defined bit value as far as the CPU is concerned (even if trying to observe that value is UB).

So once every memory location is CPU-"initialized", which the manual you linked says should be done at boot, I doubt there's anything rust is likely to do to CPU-"uninitialize" them. Rust moving around MaybeUninits, or structs with uninitialized bits, is most likely going to be memcpy - propagating the actual CPU value instead of making the location CPU-"uninitialized". (Or otherwise it will leave the old CPU-valid value in place, or write some arbitrary but CPU-valid value.)

6 Likes

A solution could be a custom allocator who set the memory before giving it? (and the stack)

1 Like

You have a point here.
On the other hand, I guess that a simple reading of an uninitialized integer causes a system reset may be surprizing for most of the audience (at least for non-embedded developers)

You are right that you can cause such behavior in any language.
That's a fact.
Now my expectation:
In assembler, C or C++ i would have expected that.
In Rust, I thought that it is memory safe, there is no uninitialized data.
And here, the MaybeUninitialized is now the exception.
I understand that this is needed - you can possibly not write a device driver or other memory mapped io without that.

There are good comments above. Thank you for the discussion.

I'd like to first ask: has it been tested on an actual device, or a theoretical concern whose scope is to be determined?

Another thing that is often overlooked is that Rust Abstract Machine's uninit memory is not actually required to correspond to hardware/environment's uninit memory.

This is generally the thing to consider when defining a compilation target, since it needs to support Rust semantics; even moving the simplest Option<u64>'s None variant might copy some uninit memory. The OS (Redox, that is) developers also need to initialize all memory that they use. I don't see how it could be a problem of regular Rust programmers.

4 Likes

This is no different from reading from a non-existent address, or from a hardware register that is write-only, etc.

That's all hardware-level behavior, not Rust's concern.