Skip to content

RFC: add freeze operation - #4001

Open
honzasp wants to merge 17 commits into
rust-lang:masterfrom
honzasp:freeze
Open

honzasp wants to merge 17 commits into
rust-lang:masterfrom
honzasp:freeze

Conversation

@honzasp

@honzasp honzasp commented Aug 19, 2026 •

Copy link
Copy Markdown

View all comments

Introduce an operation similar to the LLVM freeze instruction, which converts uninitialized values into initialized but arbitrary values:

impl<T> MaybeUninit<T> {
    const fn freeze(self) -> MaybeUninit<T>;
}

The biggest disadvantage of adding the freeze operation is that a Rust program can leak a secret that was previously stored in the uninitialized memory without triggering UB.

Important

Since RFCs involve many conversations at once that can be difficult to follow, please use review comment threads on the text changes instead of direct comments on the RFC.

If you don't have a particular section of the RFC to comment on, you can click on the "Comment on this file" button on the top-right corner of the diff, to the right of the "Viewed" checkbox. This will create a separate thread even if others have commented on the file too.

Rendered

Comment thread text/4001-freeze.md
}
```

However, this would require a new trait in the standard library, so this might

@Lokathor Lokathor Aug 19, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hello. Yes, I'd merge this PR for sure.

View changes since the review

@RalfJung RalfJung left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This sounds very good overall, thanks a lot :)
Cc @rust-lang/opsem

View changes since this review

Comment thread text/4001-freeze.md Outdated
Comment thread text/4001-freeze.md Outdated

However, until LLVM adds support for this intrinsic, the compiler can generate a
call to a function that performs the copy, or a call to `memcpy()` (if LLVM can
be persuaded not to treat this as equivalent to the `llvm.memcpy` intrinsic).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Cc @nikic -- do you think it makes sense to add a flag to LLVM's memcpy that indicates a freeze? Ideally we'd not have to hand-roll our own freezing memcpy in Rust, that seems a bit silly. We want to use the regular highly-optimized memcpy primitive, just in a way that LLVM considers to be freezing all undef/poison.

Comment thread text/4001-freeze.md Outdated
Comment thread text/4001-freeze.md Outdated
Comment thread text/4001-freeze.md
would also be consistent with the existing `zeroed()` method.

However, the name `freeze` for this operation is already well established by the
LLVM instruction, so this RFC proposes to use `freeze` instead of `frozen`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't find this very convincing. We don't usually name things after the LLVM IR operation they compile to.

I think it should be called frozen.

Comment thread text/4001-freeze.md Outdated
Comment thread text/4001-freeze.md Outdated
Comment thread text/4001-freeze.md Outdated
Comment thread text/4001-freeze.md
Comment thread text/4001-freeze.md Outdated
Comment thread text/4001-freeze.md Outdated
Comment thread text/4001-freeze.md
Comment thread text/4001-freeze.md
Comment thread text/4001-freeze.md
Comment thread text/4001-freeze.md
Comment on lines +467 to +469
The biggest disadvantage of adding the `freeze` operation is that **a Rust
program can leak a secret that was previously stored in the uninitialized memory
without triggering UB**.

@hanna-kruppe hanna-kruppe Aug 20, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

In general, we don't have to make things UB to consider them wrong and try to prevent them. We also have the concept of erroneous behavior (EB), where the program is considered buggy and can be aborted if e.g. run under Miri or a sanitizer, but otherwise has well-defined behavior (or at least not UB).

Could we have EB for at least some subset of Rust programs that leak uninitialized memory? It makes no sense to consider every use of freeze that sees an uninit byte to be EB, if we want that then we shouldn't add freeze to begin with. But tools like Valgrind and MemorySanitizer can diagnose specific uses of uninitialized memory that are likely bugs, e.g., branching on a condition or dereferencing a pointer derived from uninitialized memory.

Is there a way to specify EB that blesses (roughly) the kind of checks those tools perform? That seems like it would provide a decent compromise (it rules out use case 1 but still allows other use cases). Unfortunately I don't see an easy way to do it:

  • A simple operational semantics would be a naive "taint tracking" model where bytes that were uninit and got frozen are still recorded as being "tainted" by uninit-ness, and this is propagated though essentially every operation on values, and certain operations on tainted values/memory (e.g., branching or outputting) are EB. However, this disallows use case 2 where the offending bits are masked out, and likely other use cases as well.
  • To be smarter about when the "taint" of uninit-ness can be safely considered defused, one could try to do "possible values of non-det choice" reasoning like LLVM's undef (e.g., undef & 1 is either 0 or 1 and (undef & 1) >> 1 is always 0). However, this seems very hard to reason about and and possibly makes some desirable compiler optimizations illegal (w.r.t. not introducing EB).
  • A more teleological definition would be that there is EB if the observable behavior of the program depends on the non-deterministic choices made by freeze operations. However, this is impossible to implement, and allows some programs that sanitizers will flag as using uninitialized memory.

View changes since the review

@RalfJung RalfJung Aug 20, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

"program leaks secrets" is not something that you can define as a property of an AM execution, so I don't think we can have Miri detect this or call it EB. (Formally it's a hyperproperty, you need to define a notion of "public"/"secret" data and then compare two runs of the program to determine that a secret was leaked.)

A simple operational semantics would be a naive "taint tracking" model where bytes that were uninit and got frozen are still recorded as being "tainted" by uninit-ness,

I think you are inventing provenance for integers. Please, let's not.

@hanna-kruppe hanna-kruppe Aug 20, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I'm well aware that "leaks secrets" can't be operationalized at AM level, and of other challenges. I'm wondering whether there is some property of an AM execution that we can define, which is somewhat related to improper use of uninitialized memory, and useful as EB: doesn't rule out any important use cases, but diagnoses some obviously buggy programs. Tools like Valgrind and MemorySanitizer are useful and already don't complain about some of the things that freeze would allow doing. It would be a shame if we had to essentially turn them off completely around any use of freeze.

Maybe the fact that freeze is opt-in is good enough to still catch all the same bugs in practice. But it's not obvious to me.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It would be a shame if we had to essentially turn them off completely around any use of freeze.

I can't think of anything better. Any way of distinguishing the result of freeze from a normal integer amounts to essentially a form of provenance on integers, and that's too big a hammer for this IMO.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

imo whatever we pick should support stuff like Atomic::<(u8, u16)>::compare_exchange which needs to be able to freeze the padding bytes (probably using MaybeUninit<[u8; 4]>) and have them turn into normal bytes that don't report errors because you had to compare them in your cmpxchg loop.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please let's not scope creep. Atomic on types with padding has a bunch of extra complications.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm not saying Rust should implement Atomic for types with padding, but that with freeze a user library could.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ah I see. Yeah that may be possible, but one has to be careful in from_mut (it probably has to freeze padding).

Comment thread text/4001-freeze.md Outdated
@RalfJung

Copy link
Copy Markdown
Member

Looks like all the first-round feedback was handled. Let's nominate this for t-lang.

@RalfJung RalfJung added the I-lang-nominated Indicates that an issue has been nominated for prioritizing at the next lang team meeting. label Aug 21, 2026
@honzasp

honzasp commented Aug 21, 2026

Copy link
Copy Markdown
Author

To be honest, I am now even less convinced that adding freeze is a good idea. If we look at the use cases from the RFC:

  1. memcpy() serialization: doing this is almost always a very bad idea, because it can leak content of uninitialized memory, including secrets.
  2. Masked reads: as @SkiFire13 pointed out, this can already be solved by overwriting the uninitialized padding bytes with zeros, and LLVM seems to be able to optimize this out to the exact same code as with freeze + mask.
  3. C bitfields: this is an edge case that's only relevant if we access C bitfields and compile with LTO, and according to @RalfJung we can't guarantee that the freeze() function will do the right thing anyway. (And even though C bitfields may cause UB on the LLVM level, this doesn't seem to cause any problems in practice.)
  4. Sparse set and other clever data structures: this is a fairly niche application, and it can be implemented safely with inline assembly or FFI.

On the other hand, we lose the property that safe Rust can never leak values of uninitialized memory without invoking UB or using inline assembly or FFI. This is not a theoretical concern, this opens a whole class of security issues.

(It might look a bit weird that I'm arguing against an RFC that I wrote, but my main aim was to resolve this question: either decide to add freeze, or decide that this operation will never be added to the language.)

@programmerjake

Copy link
Copy Markdown
Member

another use-case for freeze: allow optimizing Simd operations -- when LLVM gains llvm.speculative.load and Rust gains a function that uses that, it can be used to load a Simd even if it might go beyond the end of the input buffer, freezing that allows you to do your simd operations without needing masking until the end, potentially running faster that way.

@RalfJung

RalfJung commented Aug 21, 2026 •

Copy link
Copy Markdown
Member

Possible motivations, some were already mentioned:

  • SIMD where some lanes are "garbage"
  • atomics on arbitrary types of small size, including types that have padding and even unions
  • More general version of the previous point: Soundly deal with padding of data of arbitrary type where we cannot just manually reset the padding to 0. (E.g., implement a memcmp that's never UB, even if it may produce odd behavior for some data.)
  • Data structures such as the sparse set
  • As a "story" for in-memory freeze via inline asm

I feel like we have seen more use cases over the years. Cc @chorman0773 @rust-lang/opsem

None of these are Earth-shattering on their own but it adds up.

Comment thread text/4001-freeze.md
@traviscross traviscross added the T-lang Relevant to the language team, which will review and decide on the RFC. label Aug 26, 2026
@programmerjake

Copy link
Copy Markdown
Member

Regarding use cases, would this allow implementing seqlock w/o inline assembly? I vaguely remember that it has similar problems as the mentioned Atomic<T> use case, but maybe not the exact same.

I don't think so.

if you want a slower seqlock that works on arbitrary types including padding, but not including anything containing pointers, you can use freeze to do that without needing inline assembly. A demo doing that (though it uses inline assembly to implement freeze since freeze isn't implemented yet): https://rust.godbolt.org/z/zd6jTec3e

you'll want atomic bytewise memcpy for full speed seqlocks, since the compiler can do much larger loads and stores with that.

@RalfJung

Copy link
Copy Markdown
Member

Ah right, you can do "atomic bytewise memcpy at home". There's also code somewhere that uses a derive for that, to do such a copy for a specific type. So yeah that would all work with just this RFC There are even variants of that that don't need this RFC I think.

@RalfJung

RalfJung commented Sep 7, 2026

Copy link
Copy Markdown
Member

Cc @thomcc as another freeze supporter, maybe you can help gather more usecases. :)

@veluca93

Copy link
Copy Markdown

My two cents: to my understanding, this RFC would allow soundly implementing a safe function along the lines of read_some_arbitrary_process_memory(len: usize) -> &'static [u8].

If that's the case, this undermines what I consider to be a big part of the "memory safety" promise, which I think is extremely undesirable. If it's not the case, then sorry for my misunderstanding :-)

If I did not misunderstand, I would be much more comfortable if freeze() / frozen() were an unsafe method, with the requirement that observable program behaviour should not depend on the resulting frozen value (I believe this would allow the "sparse set" usecase and probably also the SIMD lanes one, but not the "serialize over the network" one -- but I also think that should not be allowed at all :-)). No idea how we could ever detect such UB though...

For the case with atomics, is there a reason I am missing that would make a wrapper that copies the type into a zeroed u64 (or similar) before atomic operations not work?

@RalfJung

Copy link
Copy Markdown
Member

My two cents: to my understanding, this RFC would allow soundly implementing a safe function along the lines of read_some_arbitrary_process_memory(len: usize) -> &'static [u8].

I don't think so.
How do you imagine that implementation to look like?

@veluca93

veluca93 commented Sep 20, 2026 •

Copy link
Copy Markdown

My two cents: to my understanding, this RFC would allow soundly implementing a safe function along the lines of read_some_arbitrary_process_memory(len: usize) -> &'static [u8].

I don't think so. How do you imagine that implementation to look like?

Something like this:

fn read_some_arbitrary_process_memory(len: usize) -> &'static [u8] {
    assert_ne!(len, 0);
    let layout = Layout::array::<u8>(len).unwrap();
    // SAFETY: `layout` is not 0-sized.
    let uninit: *mut MaybeUninit<u8> = unsafe {std::alloc::alloc(layout)}.cast();
    let ret = Box::leak(vec![0u8; len].into_boxed_slice());
    for i in 0..len {
        // SAFETY: i <= len and we own the memory
        let val = unsafe { uninit.add(i).read() };
        let val = freeze(val);
        // SAFETY: we just `freeze`'d the MaybeUninit, and every u8 bit pattern is valid
        ret[i] = unsafe { val.assume_init() };
    }
    ret
}

If there's UB here under the proposed semantics of freeze, I am not sure what it would be.

@RalfJung

RalfJung commented Sep 20, 2026 •

Copy link
Copy Markdown
Member

Oh, len is not the address. Yeah that is sound.

I don't think declaring it UB is a meaningful defense, and we have years of accumulated requests to allow freezing, so the benefits outweigh the cost. Also note that if you replace freeze there with an inline asm blob, then even under the strictest model we have for inline asm, the code is already sound today.

People have different ideas about what "memory safety" means. To me it means "no UB". Including non-interference properties such as the one you are alluding to would move the goalpost by miles. The entire stack from LLVM to rustc is not equipped to deal with any kind of reasoning about "not leaking secrets". This is not a new observation, and not a new problem introduced by this RFC. It is a problem, and we know the basic tools needed to tackle it, but none of the players in the ecosystem that have the resources to turn those tools into reality seem sufficiently interested in making progress here so nothing happens. That should not stall progress for other useful features such as freeze, IMO.

@veluca93

Copy link
Copy Markdown

Well, if that'd be sound to do with inline asm, then so be it :-) (I was under the impression that inline asm was also disallowed from reading uninit memory, but I can see how that would be hard to formulate)
Given that I don't see how one would use freeze without some unsafe code around it, I guess at least standard auditing of unsafe code would let one find such implementations, so it'd be at least somewhat detectable.

@RalfJung

Copy link
Copy Markdown
Member

The inline asm situation is complicated -- but tl;dr: if you have an inline asm blob that reads any mapped memory (initialized or not), and you specify it as "just returns arbitrary data", that pretty much has to be okay. People use inline asm to read all sorts of stuff that's not memory in the AM, like stack pointers, or to read memory that they don't have the right to read as per the aliasing rules, and that's all fine as long as they don't make any assumptions about the bytes they see. Given that we want to allow many of these patterns, there's not really any way I know that we could say "oh but specifically for uninit memory this is not allowed".

Crucially, such inline asm is different from freeze in that as far as reasoning about the code goes, it always returns arbitrary data, even if the memory happens to be initialized. But in practice it can still return the actual data that sits there, so if you are thinking in terms of leaking secrets, that makes no difference.

@oskgo

oskgo commented Sep 20, 2026

Copy link
Copy Markdown

I understand that the formal models Rust currently uses cannot even formulate guarantees about nondeterministic code not returning your cryptographic keys, but in practice some sources of nondeterminism are less likely to do that than others. I think that's worth considering. (I agree that asm blocks can be just as bad as freeze here, but asm blocks are scary)

The number of requests for freeze is an indication that this is something people want, not that it's useful for a lot of people. How many of these requests are made for the purpose of serialization? I personally first thought about freeze because of that, and asked about it on the Rust discord before realizing the risk associated with this.

I think that freeze opens up a massive footgun. We better make sure that we can warn about the pitfalls loudly enough to make the benefits outweigh the downsides. I doubt we can match the scare factor of asm blocks.

@RalfJung

Copy link
Copy Markdown
Member

The number of requests for freeze is an indication that this is something people want, not that it's useful for a lot of people. How many of these requests are made for the purpose of serialization?

Basically none, as far as I can recall. That's why it is not in my list.

@honzasp

honzasp commented Sep 22, 2026

Copy link
Copy Markdown
Author

@RalfJung Just a question about the procedure, what are going to be next steps for the RFC? Is there still anything that needs to be resolved (other than the question of whether we want this operation at all), or anything else that I can do to move this forward?

@RalfJung

Copy link
Copy Markdown
Member

My understanding of the current status is that you got a bunch of feedback that should be incorporated into the text, especially for the motivation section. I haven't followed to what extent you stayed on top of the various subthreads here in terms of updating the RFC to resolve their concerns.

@honzasp

honzasp commented Sep 23, 2026

Copy link
Copy Markdown
Author

Here are all the unresolved threads and conversations:

Do you think that some of the points above should be incorporated into the text?

Comment thread text/4001-freeze.md Outdated
@RalfJung

Copy link
Copy Markdown
Member

The motivation section definitely needs to be updated and extended. Even you yourself didn't seem very convinced by the original motivation any more for a while. ;)

#4001 (comment): I don't disagree, but I'm leaving this open to discussion. I think we can postpone the naming decision until the lang team meeting

Then please put this under "unresolved questions".

#4001 (comment): I don't see how this could work without introducing a lot of complexity, so I don't think it's something we want to add to the RFC at the moment. However, I also don't want to just dimiss this by marking the thread as "resolved".

You could add an unresolved question along the lines of

  • Is it possible to declare some misuses of freeze as Erroneous Behavior so that it can still be flagged by e.g. valgrind, as a means to detect code that might otherwise leak secrets? There is no concrete proposal for how that could be done, so the answer is likely "no", but maybe good ideas will surface during the unstable experimentation phase.

@joshtriplett

Copy link
Copy Markdown
Member

Should this be an unsafe operation?

Also, should this require a separate assume_init, or should it directly give you a &mut [u8]?

@ChayimFriedman2

Copy link
Copy Markdown

@joshtriplett This is available on any type, not just bytes. So if this assume_init()s it must be unsafe. But IMO it makes sense to not - it's more general since you might want to write some bytes, and for it to be safe (what is the safety precondition? What UB is possible here?)

@RalfJung

Copy link
Copy Markdown
Member

As proposed, with signature fn(MaybeUninit<T>) -> MaybeUninit<T>, there's no way to cause UB with it so by our usual standards it should not be unsafe.

@chorman0773

chorman0773 commented Sep 23, 2026 •

Copy link
Copy Markdown

The &mut [u8] would need to take &mut self, and that's deceptively expensive (as well documented on the original RFC and I believe this one, it's basically replace_with(self, |v| v.freeze())). External crates (like bytemuck) can wrap freeze().assume_init() into various levels of safe and unsafe apis.

It doesn't need to be unsafe because it doesn't cause undefined behaviour at all. There are security implications, but we don't consider unsafe a security boundary.
Though, if we're considering EB... does EB need to be unsafe? I'm not sure we have precedent at all here.

@RalfJung

Copy link
Copy Markdown
Member

For now it's not EB because we have no idea how we could make it EB.
If we do find a way to make it EB -- well it's not UB so arguably it doesn't have to be unsafe. But we can cross that bridge when we get there.

@chorman0773

Copy link
Copy Markdown

The easiest way to make it EB, I think, would just be to add Frozen(u8) to the Byte enum, that is the non-deterministically chosen value, then reading from these bytes at a scalar type throws EB then if the program didn't crash, acts as though Frozen(n) was Init(n, None).

In any case, do we have precendent on EB? It could be reasonable to say that operations that can cause EB should be unsafe as well.

@thomcc

thomcc commented Sep 24, 2026

Copy link
Copy Markdown
Member

Why would we make it EB? It can be useful to do this, so I don't really see the benefit of allowing it if we're going to insist that you still shouldn't ever do it.

@chorman0773

Copy link
Copy Markdown

Well, I don't think we have a way of stopping valgrind from trapping (though I think valgrind can trap on MaybeUninit<scalar> passed by value, so). And there's the security argument where it can be desirable to allow trapping to warn people about potential secret leekage.

@honzasp

honzasp commented Sep 24, 2026

Copy link
Copy Markdown
Author

The motivation section definitely needs to be updated and extended. Even you yourself didn't seem very convinced by the original motivation any more for a while. ;)

Do you have something in particular that you'd like to add to the motivation? My aim in the RFC was to present the advantages and disadvantages of freeze as accurately as possible; if the motivation is not compelling enough, then perhaps we should decide not to add freeze :)

Then please put this under "unresolved questions".

[...]

You could add an unresolved question along the lines of

I added these two unresolved questions to the text.

@RalfJung

RalfJung commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

The easiest way to make it EB, I think, would just be to add Frozen(u8) to the Byte enum, that is the non-deterministically chosen value, then reading from these bytes at a scalar type throws EB then if the program didn't crash, acts as though Frozen(n) was Init(n, None).

In any case, do we have precendent on EB? It could be reasonable to say that operations that can cause EB should be unsafe as well.

That would wholly defeat the purpose of this RFC. All programs that make use of the new functionality would be called "buggy". Remember that Miri will stop execution on EB, at least by default.

Do you have something in particular that you'd like to add to the motivation? My aim in the RFC was to present the advantages and disadvantages of freeze as accurately as possible; if the motivation is not compelling enough, then perhaps we should decide not to add freeze :)

We discussed this above, didn't we? Also see the messages after that.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

I-lang-nominated Indicates that an issue has been nominated for prioritizing at the next lang team meeting. P-lang-drag-1 Lang team prioritization drag level 1. T-lang Relevant to the language team, which will review and decide on the RFC.

Projects

None yet

Development

Successfully merging this pull request may close these issues.