This might be better on user's forum, please let me know if I should go there instead.
I'm trying to understand the unsafe coding guidelines better, and towards that end I've started writing some tests that are definitely unsafe, but may or may not cause undefined behavior. One of them can be found on the playground. The code is copied below for those that would prefer to just read.
The torture test is fairly simple; I have a struct that has an identifier value embedded within it. I create a raw pointer to this that I copy and pass into multiple threads. Each thread overwrites the identifier field with the same new identifier, without synchronizing this write with any other thread. After all threads have completed and I've joined all of them, I test that the header
and data
fields are unchanged, and the identifier
field is identical to the new identifier.
Now, this works on my machine in both debug and release modes, and it works on the playground, again in both debug and release modes. However, that just proves that the compiler currently has a particular behavior, it doesn't prove that this is defined behavior (opposite of UB). The real question is, is this a data race? On the one hand it definitely meets the requirements to be a data race per the definition given in the nomicon, but since every write has the same data, I don't know if I've landed in undefined behavior territory or not. Any insights would be much appreciated.
EDIT
Yeah, so, 5 more minutes of twiddling and I would have triggered the UB I was looking for... turns out what I was missing was #[repr(packed)]
on my struct; as soon as I added that, all kinds of UB came out (threads terminating instantly, threads never terminating, and in one case, spawning an unlimited number of threads). So, yay, I have found that this is definitely off the edge of the cliff...
The code
use rand::{distributions::Standard, prelude::*, Error, Fill};
use std::thread::{spawn, yield_now};
const HEADER_LENGTH: usize = 0usize;
const TRAILER_LENGTH: usize = 1024usize;
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct Example {
// Fake header to force unaligned access on packed structures. The contents
// are randomly generated, and should remain unchanged throughout the tests.
header: [u8; HEADER_LENGTH],
// Pseudo identifier
identifier: u128,
// Fake trailer to force unaligned access on packed structures. The contents
// are randomly generated, and should remain unchanged throughout the tests.
data: [u8; TRAILER_LENGTH],
}
impl Fill for Example {
fn try_fill<R: Rng + ?Sized>(&mut self, rng: &mut R) -> Result<(), Error> {
for i in 0..self.header.len() {
self.header[i] = rng.gen();
}
self.identifier = rng.gen();
for i in 0..self.data.len() {
self.data[i] = rng.gen();
}
Ok(())
}
}
impl Distribution<Example> for Standard {
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Example {
let mut ex = Example {
header: [0; HEADER_LENGTH],
identifier: 0,
data: [0; TRAILER_LENGTH],
};
ex.try_fill(rng).expect("This should never fail");
ex
}
}
fn unsafe_test() {
let mut rng = rand::thread_rng();
// First, create our test subject, and clone it as-is so we can verify that
// all and only the bits we expected to change actually did change.
let mut example: Example = rng.gen();
let pristine = example.clone();
// We're going to use a pointer to mutate the example.
let example_pointer: *mut Example = &mut example as *mut Example;
// Now create a new, shared identifier.
let new_identifier: u128 = rng.gen();
// Now things get interesting. We're going to create numerous threads and
// have them each try to write 'new_identifier' to shared.identifier. **WE
// DO NOT DO ANY SYNCHRONIZATION WHATSOEVER!** They are all writing the
// memory location as fast as they can, and if something goes south, so be
// it!
let mut join_handles = Vec::new();
for _ in 0..20 {
// Have to cast to a usize because pointers can't cross thread
// boundaries (rust's safety guarantees are AWESOME! Shooting yourself
// in the foot takes great skill here)
let p: usize = example_pointer as usize;
let t = spawn(move || {
let ep: *mut Example = p as *mut Example;
for _ in 0..200000 {
unsafe {
(*ep).identifier = new_identifier;
}
yield_now();
}
});
join_handles.push(t);
}
for jh in join_handles.drain(..) {
jh.join().expect("Why'd I crash on joining???");
}
// We've completed the threaded write tests. We should be able to write
// new_identifier into 'pristine', and it should be identical to 'example'.
// This test may fail, but it is very, very unlikely to. Good enough for
// the tests I'm doing.
assert!(pristine.identifier != new_identifier);
let mut merged: Example = pristine.clone();
merged.identifier = new_identifier;
assert_eq!(example, merged);
}
fn main() {
println!("This is a test of unsafe, but possibly not undefined, behavior.");
println!("If there aren't any crashes, then the test has likely passed.");
for _ in 0..1000 {
unsafe_test();
}
println!("If we're here, then there weren't any crashes.");
}