Skip to content

Conversation

joshlf
Copy link
Contributor

@joshlf joshlf commented Apr 29, 2025

Partially addresses rust-lang/unsafe-code-guidelines#555 by clarifying that it is sound to write any byte values (initialized or uninitialized) to any MaybeUninit<T> regardless of T.

r? @RalfJung

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-libs Relevant to the library team, which will review and decide on the PR/issue. labels Apr 29, 2025
/// }
/// ```
///
/// # Validity
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving this discussion here:

The MaybeUninit docs probably make sense for this. We now do have a definition of "byte" in the reference that this can link to.

Okay, awesome. And what wording would you recommend? Would it be accurate to say something like the following?

The value of a [MaybeUninit<u8>; N] may contain pointer provenance, and so p: P -> [MaybeUninit<u8>; N] -> P preserves the value of p, including provenance

@RalfJung would you like me to add language like this to this PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update: I've added the following as a more concrete and fleshed out draft. I can edit or remove as preferred.

/// # Provenance
///
/// `MaybeUninit` values may contain [pointer provenance][provenance]. Concretely, for any
/// pointer type, `P`, which contains provenance, transmuting `p: P` to
/// `MaybeUninit<[u8; size_of::<P>]>` and then back to `P` will produce a value identical to
/// `p`, including provenance.
///
/// [provenance]: ../ptr/index.html#provenance

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to reference the definition of a byte.

@rust-log-analyzer

This comment has been minimized.

@RalfJung
Copy link
Member

RalfJung commented May 7, 2025

Cc @rust-lang/opsem

Comment on lines 277 to 279
/// If `T` contains initialized bytes at byte offsets where `U` contains padding bytes, these
/// may not be preserved in `MaybeUninit<U>`, and so `transmute(u)` may produce a `T` with
/// uninitialized bytes in these positions. This is an active area of discussion, and this code
/// may become sound in the future.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it makes sense to say that a type "contains initialized bytes" at some offset. That's a property of a representation.

The typical term for representation bytes that are lost here is "padding". I don't think we have rigorously defined padding anywhere yet, but the term is sufficiently widely-used (and generally with a consistent meaning) that we may just be able to use it here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC, you're making two points:

  • We should speak about a type's representation containing bytes, not about the type itself containing bytes
  • In a representation, we should speak about padding bytes rather than uninitialized bytes

Is that right?

One thing that's probably worth distinguishing here is between values and layouts. In my mental model, an uninit byte is one of the possible values that a byte can have (e.g., it's the 257th value that can legally appear in a MaybeUninit<u8>). By contrast, padding is a property of a layout - namely, it's a sequence of bytes in a type's layout that happen to have the validity [MaybeUninit<u8>; PADDING_LEN].

Based on this, maybe it's best to say:

If byte offsets exists at which T's representation does not permit uninitialized bytes but U's representation does (e.g. due to padding), then the bytes in T at these offsets may not be preserved in u, and so transmute(u) may produce a T with uninitialized bytes at these offsets. This is an active area of discussion, and this code may become sound in the future.

Copy link
Member

@RalfJung RalfJung May 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that right?

No. I think both of the following concepts make sense:

  • The representation of a particular value at a particular type contains uninitialized bytes.
  • A type contains padding bytes. (These are bytes which are always ignored by the representation relation.)

But it makes less sense to talk about padding of a representation, or to talk about uninitialized bytes in a type.

So for this PR, the two key points (and they are separate points) are:

  • If U has padding, those bytes may be reset to "uninitialized" as part of the round-trip. If those same bytes are not padding in T, this can therefore mean some of the information of the original T value is lost.
  • If T does not permit uninitialized bytes on those positions, the round-trip is UB.

The second point is just a logical consequence of the first, it does not add any new information. Not sure if it is worth mentioning.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • The representation of a particular value at a particular type contains uninitialized bytes.
  • A type contains padding bytes. (These are bytes which are always ignored by the representation relation.)

Does this imply that a type contains padding bytes, not a type's representation?

I'm thinking through the implications of what you said, and I think I understand something new that I didn't before, and I want to run it by you: In my existing mental model, a padding byte is a location in a type's layout such that every byte value at that location (including uninit) is valid (enums complicate this model, but I don't think that complication is relevant for this discussion - we can just stick to thinking about structs). The problem with this mental model is that, interpreted naively, it implies that different byte values in a padding byte could correspond to different logical values of the type. So e.g. in the type #[repr(C)] struct T(u8, u16), [0, 0, 0, 0] and [0, 1, 0, 0] would correspond to different values of the type since we're treating the padding byte itself as part of the representation relation. Of course, that is not something we want.

IIUC, by contrast your model is that the representation relation simply doesn't include padding bytes at all. So it'd be more accurate to describe the representation of T as consisting of three bytes - at offsets 0, 2, and 3. Every representation of T has a "hole" at offset 1 which is not part of the representation. This ensures that there's a 1:1 mapping between logical values and representations. Is that right?

Copy link
Member

@RalfJung RalfJung May 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this imply that a type contains padding bytes, not a type's representation?

That's how I think about it. We can't tell which byte is a padding byte by looking at one representation -- it's a property of the type.

In my existing mental model, a padding byte is a location in a type's layout such that every byte value at that location (including uninit) is valid

That would make the only byte of MaybeUninit<u8> a padding byte, so I don't think this is the right definition.
That's why I said above: a padding byte is a byte that is ignored by the representation relation. Slightly more formally: if r is some representation valid for type T, and r' is equal to r everywhere except for padding bytes, then r and r' represent the same value.

So it'd be more accurate to describe the representation of T as consisting of three bytes

The representation has 4 bytes. But only 3 of them actually affect the represented value (which is a tuple of two [mathematical] integers).


We seem to be using the term "representation" slightly differently. For me, that's list a List<Byte> of appropriate length. You may be using that term to refer to what I call "representation relation"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We seem to be using the term "representation" slightly differently. For me, that's list a List<Byte> of appropriate length. You may be using that term to refer to what I call "representation relation"?

That's helpful, thank you!

To avoid rabbit holing too much on the definitions (although it's interesting and useful – just maybe a bit of a distraction here), maybe you could propose language you'd prefer to see in place of what I've written here?

@RalfJung
Copy link
Member

@rustbot ready

@RalfJung
Copy link
Member

@rustbot author

@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels May 30, 2025
@rustbot
Copy link
Collaborator

rustbot commented May 30, 2025

Reminder, once the PR becomes ready for a review, use @rustbot ready.

@RalfJung
Copy link
Member

RalfJung commented Jun 1, 2025

I raised the question on Zulip whether it is wise to make a guarantee here that isn't, strictly speaking, documented in the LLVM LangRef. Nikita says he thinks that that's fine -- we may have to adjust how exactly we compile MaybeUninit in the future, but LLVM currently intends do support this case in a somewhat roundabout and incomplete way that seems to work well enough in practice, and LLVM can't more aggressively exploit the fuzziness along the edges of that approximation until a proper alternative exists.

@karolzwolak
Copy link
Member

Thanks for your contribution @joshlf from wg-triage.
Could you address the comments above?

@joshlf
Copy link
Contributor Author

joshlf commented Aug 27, 2025

Thanks for your contribution @joshlf from wg-triage. Could you address the comments above?

I likely won't have time to move this forward until mid-September or October, but I'll follow up at that point.

@rustbot

This comment has been minimized.

@joshlf
Copy link
Contributor Author

joshlf commented Aug 30, 2025

Thanks for your contribution @joshlf from wg-triage. Could you address the comments above?

I likely won't have time to move this forward until mid-September or October, but I'll follow up at that point.

Nvm, found some time 🙂

I've responded to various comment threads.

@karolzwolak
Copy link
Member

Awesome, you should also rebase your changes onto master, and do @rustbot ready when you're ready for review.

@rustbot

This comment has been minimized.

@joshlf
Copy link
Contributor Author

joshlf commented Aug 31, 2025

@rustbot ready

@rustbot rustbot removed the S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. label Aug 31, 2025
@rustbot rustbot added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Aug 31, 2025
/// be undefined behavior or yield a value different from `t` due to those bytes being lost. This is an active area of discussion, and this code
/// may become sound in the future.
///
/// Note that, so long as no such byte offsets exist, then the preceding `identity` example *is* sound.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Note that, so long as no such byte offsets exist, then the preceding `identity` example *is* sound.
/// However, so long as no such byte offsets exist, then the preceding `identity` example *is* sound.
/// In particular, since `[u8; N]` has no padding bytes, transmuting `t` to `MaybeUninit<[u8; size_of::<T>]>`
/// and back will always produce the original value `t` again. This is true even if `t` contains [provenance]:
/// the resulting value will have the same provenance as the original `t`.
///
/// [provenance]: crate::ptr#provenance

And then we don't need the dedicated provenance section at all, do we?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And then we don't need the dedicated provenance section at all, do we?

Done, and removed the provenance section.

@RalfJung
Copy link
Member

RalfJung commented Sep 8, 2025

Thanks, I like it! However, more eyes can't hurt -- @rust-lang/opsem @traviscross (since this is basically Reference-like material).

I also wonder what process we need to invoke, if any. This does document a new guarantee, after all. Nominating for t-lang.

@RalfJung RalfJung added the I-lang-nominated Nominated for discussion during a lang team meeting. label Sep 8, 2025
/// However, so long as no such byte offsets exist, then the preceding `identity` example *is* sound.
/// In particular, since `[u8; N]` has no padding bytes, transmuting `t` to `MaybeUninit<[u8; size_of::<T>]>`
/// and back will always produce the original value `t` again. This is true even if `t` contains [provenance]:
/// the resulting value will have the same provenance as the original `t`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a bit of a footgun for potential misunderstandings here but maybe I am being too nitpicky -- and I don't know what else we could say, anyway:

if T is &mut _ or &_, the resulting value will actually not be identical. The transmute itself is a perfect identity, but when a function return a reference (and this includes transmute returning a reference), that can influence the aliasing model. Tree Borrows will consider the resulting reference a child of the original reference.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe the core issue here is how we use the term "value"? E.g. I might say something like: "let y = x produces a y with the same value as x", but it's semantically relevant that there are now two copies of this value instead of one (e.g. image that x: &mut T). So maybe I'm using "value" in a way that doesn't capture everything about the value? More precisely, maybe I'm using "value" in a way that captures everything local about the value, but doesn't capture everything about the relationship between that value and other values (which is relevant when it comes to aliasing). Of course, provenance complicates this story because we talk about provenance "living inside" a particular value, but it's also a non-local property.

That doesn't really resolve your concern, but some random thoughts. Maybe it'll prompt you to think of better language we could use here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another possibility: Instead of saying that the value is fully preserved, maybe we could say that the following contents of the value are preserved?

  • Bit pattern
  • Provenance

...and then explicitly disclaim any other value contents that we add to the AM in the future?

Copy link
Member

@RalfJung RalfJung Sep 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's more a problem of abstraction: the Rust code let y = x; becomes something like y = x; Retag(y) in MIR, and that Retag does change the value. Specifically it changes the provenance, making y a child of x rather than wholly identical to x.

(In fact we sometimes even insert reborrows, making this more like y = &mut *x. I don't know the exact conditions for that.)

So maybe I'm using "value" in a way that doesn't capture everything about the value? More precisely, maybe I'm using "value" in a way that captures everything local about the value, but doesn't capture everything about the relationship between that value and other values (which is relevant when it comes to aliasing). Of course, provenance complicates this story because we talk about provenance "living inside" a particular value, but it's also a non-local property.

Alias tracking is based on provenance. Provenance is also just data. It may reference other data, such as indicating a position in a tree.

Copy link
Contributor Author

@joshlf joshlf Sep 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another possibility: Instead of saying that the value is fully preserved, maybe we could say that the following contents of the value are preserved?

  • Bit pattern
  • Provenance

...and then explicitly disclaim any other value contents that we add to the AM in the future?

In that case, I'm leaning towards this option unless you have thoughts about language we could use that captures specifically the subset of "value" that we want to address here. How does that sound?

Copy link
Member

@RalfJung RalfJung Sep 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's the best I can come up with so far. But I feel like this may actually be easier to write for someone who's less deeply entrenched in these discussions. ;)

Note: if t contains a reference, then there may be implicit reborrows of the reference any time it is copied, which may alter its provenance. In that case, the value returned by identity may not be exactly the same as its argument. However, even in this case, it remains true that identity behaves the same as a function that just returns t immediately.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay here's my version of this (which is in the PR now):

Note a potential footgun: if t contains a reference, then there may be implicit reborrows of the reference any time it is copied, which may alter its provenance. In that case, while the value returned by identity is exactly the same as its argument, that value may immediately be reborrowed upon return, altering its provenance. This may make this call to identity behave as though it does not exactly preserve provenance.

Copy link
Member

@RalfJung RalfJung Sep 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

while the value returned by identity is exactly the same as its argument

That part isn't given (there can be reborrows inside identity).

In fact my comment explicitly stated "the value returned by identity may not be exactly the same as its argument" so not sure how you got from there to your version.

It also seems very useful to state that it is still equivalent to a "boring" identity function.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact my comment explicitly stated "the value returned by identity may not be exactly the same as its argument" so not sure how you got from there to your version.

I thought you were being overly-conservative in your wording, which I realize now isn't the case.

Changed to be closer to your wording. Better?

Note a potential footgun: if t contains a reference, then there may be implicit reborrows of the reference any time it is copied, which may alter its provenance. In that case, the value returned by identity may not be exactly the same as its argument. However, even in this case, it remains true that identity behaves the same as a function that just returns t immediately (i.e., fn identity<T>(t: T) -> T { t }).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That works for me :)

@traviscross traviscross added I-lang-radar Items that are on lang's radar and will need eventual work or consideration. P-lang-drag-1 Lang team prioritization drag level 1. https://rust-lang.zulipchat.com/#narrow/channel/410516-t-lang T-lang Relevant to the language team labels Sep 9, 2025
/// ```
///
/// If the representation of `t` contains initialized bytes at byte offsets where `U` contains padding bytes, these
/// may not be preserved in `MaybeUninit<U>`. Interpreting the representation of `u` at type `T` again (i.e., `transmute(u)` above) may thus
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// may not be preserved in `MaybeUninit<U>`. Interpreting the representation of `u` at type `T` again (i.e., `transmute(u)` above) may thus
/// may not be preserved in `MaybeUninit<U>`. Interpreting the representation of `u` as type `T` again (i.e., `transmute(u)` above) may thus

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC "at" is intentional language that @RalfJung uses – it's nomenclature used by language theorists.

Copy link
Member

@RalfJung RalfJung Sep 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"interpret u as type T" sounds wrong. We don't interpret u as a type, it's not a type. We use the type to tell us how to interpret u.

But I am not sure if there is widely-used standard terminology here, "interpret x at type y" just sounded most natural to me.

Copy link
Contributor

@traviscross traviscross Sep 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. Reading more closely, to use "as" here I'd instead use "as of", e.g. "interpret the bytes of x as being of the type T". I.e., a value "has a type", "is of a type", or is treated or interpreted as "having" or "being of" a type.

I'm pretty happy to use PLT jargon in general, but for that to work we need to use the piece of jargon pervasively enough that people pick up on it (and probably discuss the jargon in our notation guide or elsewhere). If we just use it in one place, people will just think it's a typo.

Copy link
Member

@RalfJung RalfJung Sep 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't even know if this is PLT jargon or just Ralf jargon. ;)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we instead say this?

Transmuting u to T (i.e., transmute(u) above) may thus...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That also works for me. I'd rather say "Transmuting u back to T" (to preserve the "again" in the current version).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Sep 16, 2025
@traviscross
Copy link
Contributor

We briefly reviewed this is the last lang triage meeting, but it's the sort of thing that's hard to really look through in a meeting, so we deferred it to async review.

@ehuss and I are looking at it now, on the @rust-lang/lang-docs side, as it is Reference-like material, as @RalfJung said. We agreed it'd be good clarify the bit raised in #140463 (comment) regarding provenance and transmute. As discussed in that thread, it'd probably be best to just describe the situation here, in terms of how the surrounding code effects a reborrow, and give an example or demonstration of code that's equivalent in this model.

When that's done, the typo I noted is fixed, and the text is wrapped more consistently, I'll r+ it.

Process-wise, thanks for pinging lang here; that was correct. In fact, let's plan to ping @rust-lang/lang and @rust-lang/lang-docs both on this sort of thing.

@traviscross traviscross removed I-lang-nominated Nominated for discussion during a lang team meeting. P-lang-drag-1 Lang team prioritization drag level 1. https://rust-lang.zulipchat.com/#narrow/channel/410516-t-lang labels Sep 16, 2025
Co-authored-by: Ralf Jung <[email protected]>
Edited-by: TC
@rustbot
Copy link
Collaborator

rustbot commented Sep 21, 2025

This PR was rebased onto a different master commit. Here's a range-diff highlighting what actually changed.

Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers.

@traviscross
Copy link
Contributor

traviscross commented Sep 21, 2025

I've revised the text for hopefully better clarity. In particular, as @RalfJung had suggested above, I've stated the guarantees upfront in positive form and then followed with the various caveats. This seemed desirable to me, and will hopefully ease lang review.

@RalfJung, @joshlf, does this look correct? (Did I break anything?)

Let's rewrite this for better clarity.  In particular, let's document
our language guarantees upfront and in positive form.  We'll then list
the caveats and the non-guarantees after.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
I-lang-radar Items that are on lang's radar and will need eventual work or consideration. S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. T-lang Relevant to the language team T-libs Relevant to the library team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants