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.