The Unsafe Rust an AI Wrote Me Last Week
I told a model to write a lock-free SPSC ring buffer in Rust. The result compiled first time, passed my unit tests, and looked utterly reasonable. It also contained a torn read that would only appear at scale. Here is the interesting part, snipped down a bit: pub fn push(&self, value: T) -> bool { let head = self.head.load(Ordering::Relaxed); let next = (head + 1) % self.capacity; if next == self.tail.load(Ordering::Relaxed) { return false; // full } unsafe { (*self.buffer.get()).as_mut_ptr().add(head).write(value); } self.head.store(next, Ordering::Relaxed); true } Note the orderings: all Relaxed . On the producer side: write data to slot head , then store the updated head. On the consumer side: read the current head, then read data from that slot. The problem is that with Relaxed ordering, nothing guarantees those two producer operations become visible in that order to the consumer. The consumer can observe the updated head pointer before the data write actually becomes visible to it. So the consumer reads head, sees "yes there's data", goes to read it, and instead reads whatever garbage is currently sitting in that memory slot. That is the torn read. Under x86 with its relatively strong memory ordering it might work for a long time. On ARM, or simply under enough load, that ordering will break. The fix is simple: // producer publishes the data with Release self.head.store(next, Ordering::Release); // consumer reads the index with Acquire let head = self.head.load(Ordering::Acquire); The Release store and Acquire load order the data write before the index update the consumer synchronizes on, which closes the race. The point, the actual point, is this. The bug did not exist because the model is stupid. It exists because the model does not understand what this code is for. It looked at many ring buffers in its training data, most of them using Relaxed , and saw Relaxed scattered around in contexts where it happened to be safe for other reasons. It cannot tell the difference between "Relaxed happened to work here because of some higher-level guarantee" and "Relaxed causes a data race in this pattern". That difference lives entirely in the memory model and the actual concurrency of the program. It is not visible in the shape of the code. This is why you should not let these things generate unsafe . Safe Rust is no problem, the compiler says no. But unsafe is exactly where invariants the compiler cannot see and the model has no concept of become critical. You get something that compiles and passes your basic tests, with the bug merely scheduled for production to unearth. I have written enough of these by hand that I tend to catch them in review. That is the real job now. Not writing the code, but knowing what questions the code has to answer, and noticing when the answers aren't there. Top comments (0)
Comments
No comments yet. Start the discussion.