The actual problem with a safe freeze
for me is that it completely breaks the parse, don't validate pattern, which makes it possible to create types that ensures any invariant by marking its fields private and constructing it only through specific, carefully planned APIs that assert all required invariants. By following this pattern, we can guarantee that as long as we stick to safe code we will never encounter a value that violates one of its invariants. But with safe freeze
and enough luck (or maybe by scattering junk memory in a certain pattern), we can manufacture any value of a given type out of thin air. (Unsafe code already trivially breaks the parse-don't-validate thing, so we can only say that safe Rust respects it)
So, if we had safe freeze
, this means that trusting invariants in safe code now requires global analysis (we must verify that freeze
was not involved in the manufacturing the value itself or any input to its constructor functions, recursively), which is super uncool and also unfeasible. Marking freeze
as unsafe means that whenever you call it, you are telling the compiler "trust me, I got this right" (so much that I think that trustme { .. }
blocks would be much better than unsafe { .. }
). It wouldn't lead to memory unsafety / UB, but would lead to breaking invariants that otherwise can't be already broken with safe code. So a safe freeze
materially reduce what guarantees we have about safe code, and really shouldn't be done.
But of course it doesn't cause UB so it's hard to justify it being marked unsafe
. Maybe one way to think about it is that rather than unsafe fn freeze
, we should have something like unsafe(invariants) fn freeze
(or something like that; it has been proposed multiple times before, but I think that past proposals didn't have such a concrete use case). So freeze
is not unsafe because it causes UB, it's unsafe(invariants)
because it's a escape hatch that, if misused, can subvert guarantees that are generally afforded to safe code.
(We could even imagine that, during the leakapocalypse, rather than turning unsafe fn forget
into a safe function - justified by noting that it doesn't lead to actual UB - we would have something like unsafe(leak) fn forget
or something. Well as long as Rc
and Arc
were dealt with as well)
I probably just don't understand the issue at hand, but isn't this just a limitation of thread sanitizer itself, rather than evidence that atomic fences are somewhat bad? (a counterpoint is that Zig removed atomic fences due to this very issue which means that at least some people think they are bad)
But anyway - taking a look at the Arc
code, if swapping implementations with #[cfg]
just to appease the sanitizer overlords is deemed acceptable, then why can't this be done with freeze
as well?
We would need some compiler-implemented notion of a "default value" that arbitrarily picks a value of a given type (or panics, or fail to compile, for uninhabited types). This might be just a zeroed value, for types without a niche at zero - or the first valid bit pattern, skipping niches (so maybe the default value of a NonZeroUsize
is 1 or something). Then freeze
does it usual thing when called outside of sanitizerss, but call this default thing when running under MSan. Which of course doesn't perform an actual freeze at all, but Arc
isn't doing an atomic fence when running under TSan either.