{
"$type": "site.standard.document",
"bskyPostRef": {
"cid": "bafyreih7mrmwl6mjs7cftml64b4jiqb33zrzg3gyiucowv3fiodoxta77e",
"uri": "at://did:plc:ivbknywyskln22er3nkssdhl/app.bsky.feed.post/3mlhd6ys4pcd2"
},
"path": "/t/include-racy-reads-in-rust-memory-model-with-maybeinvalid-t/24289#post_1",
"publishedAt": "2026-05-09T21:25:22.000Z",
"site": "https://internals.rust-lang.org",
"tags": [
"previous post",
"deliberate UB",
"RFC"
],
"textContent": "_Disclaimer: This post has nothing to do with previous post mentioning`MaybeInvalid`_.\n\nUnsafe code guidelines contains a deliberate UB section mentioning the SeqLock issue, i.e. SeqLock algorithm isn’t compatible with Rust memory model. In fact, SeqLock relies on a racy read, which is known to be valid only after a subsequent check.\n\nThe document mentions two possible solutions:\n\n * (a) adopt LLVM's handling of memory races (then the problematic read would merely return undef instead of UB due to a data race)\n * (b) add bytewise atomic memcpy and using that instead of the non-atomic volatile load.\n\n\n\nThere is currently a RFC opened about solution (b). I would like to explore a path closer to solution (a).\n\nIt would be materialized by the following types/functions:\n\n\n // in core::mem\n #[lang = \"maybe_invalid\"]\n #[derive(Copy)]\n #[repr(transparent)]\n pub union MaybeInvalid<T> {\n invalid: (),\n value: ManuallyDrop<T>,\n }\n\n impl<T> MaybeInvalid<T> {\n pub fn assume_valid(self) -> T { /* .. */ }\n }\n\n // in core::ptr\n pub unsafe fn read_maybe_invalid<T>(ptr: *const T) -> MaybeInvalid<T> { /* .. */ }\n\n\nConcretely, it would mean to defer the UB of a racy read to `MaybeInvalid::assume_valid` call. SeqLock implementation would then become:\n\n\n pub struct SeqLock<T> {\n seq: AtomicUsize,\n data: UnsafeCell<T>,\n }\n\n unsafe impl<T: Copy + Send> Sync for SeqLock<T> {}\n\n impl<T> SeqLock<T> {\n /// Safety: Only call from one thread.\n pub unsafe fn write(&self, value: T) {\n self.seq.fetch_add(1, Relaxed);\n fence(Release);\n unsafe { ptr::write(self.data.get(), value) }\n self.seq.fetch_add(1, Release);\n }\n\n pub fn read(&self) -> T {\n loop {\n let s1 = self.seq.load(Acquire);\n let data = unsafe { ptr::read_maybe_invalid(self.data.get()) };\n fence(Acquire);\n let s2 = self.seq.load(Relaxed);\n if s1 & 1 == 0 && s1 == s2 {\n return unsafe { data.assume_valid() };\n }\n }\n }\n }\n\n\nFor SeqLock, `MaybeInvalid::assume_valid` would be called after ensuring there was no data race. Compared to RFC 3301, I see the following advantages:\n\n * The API is simpler: one type, one method, one function.\n * It doesn't reuse `MaybeUninit`, as initialization is not the issue here, avoiding confusion.\n * Writes remain non-racing plain writes, so `assume_valid` cannot return torn values. This is in my opinion the biggest advantage, as it removes an entire class of problems (for example the `Drop` issue of RFC 3301).\n * The semantic is closer of what SeqLock algorithm is built on: a read which may be invalid, and is assumed valid after an atomic check.\n * This model is de facto already supported by LLVM, so there would be no change on this side.\n\n\n\nOf course, the drawbacks are significant:\n\n * It requires to modify the Rust memory model, which is, I assume, quite a blocker by itself.\n * Modifying Rust memory model might impact interoperability with C/C++, as Rust memory model is inherited from C++.\n * (I've realized it while writing SeqLock implementation) racy reads, but also plain writes should interact with fences as atomics do; the consequences of including plain writes here may be bigger than I would have expected.\n\n\n\nI'm not an expert in the domain, so I may be completely off the mark here. Moreover, the fence issue made me reconsider my will of having `MaybeInvalid` compared to RFC 3301. But this post has the merit of discussion option (a) of unsafe code guidelines.",
"title": "Include racy reads in Rust memory model with `MaybeInvalid<T>`"
}