Is it UB to modify an array created with std::slice::from_raw_parts_mut if the underlying array is immutable ref type?

I have the following message bus with a buffer for writing.

pub struct MemoryBus {
    buffer: std::sync::Arc<SharedBuffer>,
    ptr: ConstPtr<u8>,
}

struct SharedBuffer {
    _buffer: std::sync::Mutex<PinnedBuffer>,
    write_head: std::sync::atomic::AtomicUsize,
    read_head: std::sync::atomic::AtomicUsize,
}

struct PinnedBuffer {
    _buffer: std::pin::Pin<std::boxed::Box<[u8; BUFFER_SIZE]>>,
    _pin: std::marker::PhantomPinned,
}

struct ConstPtr<T: ?Sized> {
    ptr: usize,
    _phantom: std::marker::PhantomData<T>,
}

I return a reference to one part of the buffer in the read implementation using
std::slice::from_raw_parts:

impl traits::Reader for MemoryBus {
    fn read(&self, position: usize) -> Option<&[u8]> {
        let pos = self
            .buffer
            .read_head
            .load(std::sync::atomic::Ordering::Acquire);

        if position >= pos {
            return None;
        }

        let wrapped_pos = position % WRAP_SIZE;

        unsafe {
            let ptr_offset = self.ptr.as_ptr().add(wrapped_pos);
            let buffer = std::slice::from_raw_parts(ptr_offset, messenger::HEADER_SIZE);

            let hdr = bincode::deserialize::<messenger::Header>(buffer).unwrap();

            let len = messenger::HEADER_SIZE + hdr.size as usize;

            let buffer = std::slice::from_raw_parts(ptr_offset, len);
            _print_buffer(&buffer);

            Some(buffer)
        }
    }
}

While holding this reference I'm writing new items to the message bus which also takes a
&self as first argument and a std::slice::from_raw_parts_mut


impl traits::Writer for MemoryBus {
    fn write<M: traits::Message, H: traits::Handler>(&self, message: M) {
        let size = messenger::align_to_usize(bincode::serialized_size(&message).unwrap() as usize);

        let position = self.buffer.write_head.fetch_add(
            messenger::HEADER_SIZE + size as usize,
            std::sync::atomic::Ordering::Relaxed,
        );

        let wrapped_pos = position % WRAP_SIZE;

        unsafe {
            let ptr_offset = self.ptr.as_ptr_mut().add(wrapped_pos);
            let len = messenger::HEADER_SIZE + size;
            let buffer = std::slice::from_raw_parts_mut(ptr_offset, len);

            buffer.fill(0);

            bincode::serialize_into(
                &mut buffer[..messenger::HEADER_SIZE],
                &messenger::Header {
                    source: H::ID.into(),
                    message_id: M::ID.into(),
                    size: size as u16,
                },
            )
            .unwrap();

            bincode::serialize_into(&mut buffer[messenger::HEADER_SIZE..], &message).unwrap();
        }

        let new_read_head = position + messenger::HEADER_SIZE + size;
        loop {
            match self.buffer.read_head.compare_exchange_weak(
                position,
                new_read_head,
                std::sync::atomic::Ordering::Release,
                std::sync::atomic::Ordering::Relaxed,
            ) {
                Ok(_) => break,
                _ => {}
            }
        }
    }
}

Given that I'm sure that the write and read function will not operate on the same part of the array.
Can this implementation still cause Undefined Behavior?
Is it safe to have write take &self instead of &mut self in this case?

FYI the goal is to have zero copy deserialization.

Answering the title question: Transmuting (or otherwise casting) from & to &mut is always Undefined Behavior. No you can't do it. No you're not special. — The Rustonomicon

However, it's valid to have & to one part of a slice and &mut to another part of a slice. Given access originates from a mut-capable raw pointer, doing such can be acceptable. The important trick is ensuring that MemoryBus::write cannot be called concurrently with itself.

Separately, there's the issue that even just moving Pin<Box<_>> around invalidates pointers to the payload, especially because the buffer type is Unpin (so doesn't get included in the broad hacky exception that allows that to not break async futures). I have a hunch your ConstPtr type is storing usize instead of being *const T because Miri complained when using *const T, but that doesn't mean that using usize means it's okay, just that Miri fails to track the pointer casts precisely enough to diagnose the issue. There's no reason to not be using *const (or ptr::NonNull) here.

7 Likes

Thanks for the reply!

You're right, ConstPtr is a usize, MemoryBus is initialized as

impl MemoryBus {
    pub fn new() -> MemoryBus {
        let pinned_buffer = std::boxed::Box::pin([0u8; BUFFER_SIZE]);
        let ptr = ConstPtr::new(pinned_buffer.as_ptr());

        MemoryBus {
            buffer: std::sync::Arc::new(SharedBuffer {
                _buffer: std::sync::Mutex::new(PinnedBuffer {
                    _buffer: pinned_buffer,
                    _pin: std::marker::PhantomPinned,
                }),
                read_head: std::sync::atomic::AtomicUsize::new(0),
                write_head: std::sync::atomic::AtomicUsize::new(0),
            }),
            ptr: ptr,
        }
    }
}

I was running into the issue that a const* is not sync or send. I will change it. This means i implement Sync and Send traits on the ConstPtr wrapper type?

"The important trick is ensuring that MemoryBus::write cannot be called concurrently with itself."
Is it possible to write concurrently to non-overlapping parts of the array?

" Separately, there's the issue that even just moving Pin<Box<_>> around invalidates pointers to the payload, especially because the buffer type is Unpin (so doesn't get included in the broad hacky exception that allows that to not break async futures)"

The root object is a messenger object initialized like

impl messenger::Messenger<MemoryBus> {
    pub fn new() -> messenger::Messenger<MemoryBus> {
        messenger::Messenger {
            message_bus: MemoryBus::new(),
        }
    }
    pub fn run(&mut self) {
        let mut handles = Vec::<std::thread::JoinHandle<()>>::new();
        let mb = self.message_bus.clone();
        handles.push(std::thread::spawn(|| WorkerA::run_task(mb)));
        let mb = self.message_bus.clone();
        handles.push(std::thread::spawn(|| WorkerB::run_task(mb)));
        for handle in handles {
            handle.join().unwrap();
        }
    }
}
struct WorkerA {
    position: usize,
    handler_a: handlers::HandlerA,
    handler_b: handlers::HandlerB,
}
struct WorkerB {
    position: usize,
    handler_c: handlers::HandlerC,
}
impl WorkerA {
    fn run_task<MB: traits::MessageBus>(mut message_bus: MB) {
        let mut worker = WorkerA {
            position: 0,
            handler_a: handlers::HandlerA::new(),
            handler_b: handlers::HandlerB::new(),
        };
        worker.run(&mut message_bus)
    }
    fn run<MB: traits::MessageBus>(&mut self, message_bus: &mut MB) {
        self.handler_a.on_start(message_bus);
        self.handler_b.on_start(message_bus);
        loop {
            if let Some(buffer) = message_bus.read(self.position) {
                self.position += buffer.len();
                self.route(&buffer, message_bus);
            }
            self.handler_a.on_loop(message_bus);
            self.handler_b.on_loop(message_bus);
        }
    }
}

is the move into the worker thread already invalidating the pointer to the payload?

You can do it, and it would technically be sound, but I would recommend against it, for the same reason that *const T is not Send/Sync, even though it technically could be made so. The reason is auto trait inference: anything which consists of Send/Sync types is itself Send/Sync. But raw pointers are commonly used to create data structures which may or may not actually be Send/Sync, so having such automatic implementations would be a major footgun rather than help.

In fact, even types composed entirely of Send/Sync innocuously looking types may turn out not to be Send/Sync, and the auto trait implementations would introduce an error. A famous example is MutexGuard, whose implementation would cause it to automatically be Send and looks innocuous enough. But MutexGuard can't be Send, because you must unlock a mutex on the same thread that locked it!

There is also a subtle issue with MutexGuard<T>: Sync, which was also caused by auto trait inference. This impl is only sound when T: Sync, while auto trait inference would implement it unconditionally. This was a soundness bug in Rust 1.0!

Yes, but:

  • You must ensure that no &mut reference exists at any time simultaneously with any other & or &mut reference (except when you explicitly reborrow a reference with a smaller lifetime and covered memory region). That may be tricky, so it's generally recommended to avoid references entirely, and only work with raw pointers in unsafe code.
  • Remember that you must still ensure proper synchronization between the writes and any possible subsequent reads.
2 Likes

I put my source code in a public repo GitHub - berendjan/messenger

when i try to run my example cargo miri run --bin two_workers_example i get the following error:

HandlerA started
error: Undefined Behavior: trying to retag from <2912> for Unique permission at alloc1358[0x0], but that tag does not exist in the borrow stack for this location
   --> /Users/usr/.rustup/toolchains/nightly-x86_64-apple-darwin/lib/rustlib/src/rust/library/core/src/slice/raw.rs:162:9
    |
162 |         &mut *ptr::slice_from_raw_parts_mut(data, len)
    |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |         |
    |         trying to retag from <2912> for Unique permission at alloc1358[0x0], but that tag does not exist in the borrow stack for this location
    |         this error occurs as part of retag at alloc1358[0x0..0x10]
    |
    = help: this indicates a potential bug in the program: it performed an invalid operation, but the Stacked Borrows rules it violated are still experimental
    = help: see https://github.com/rust-lang/unsafe-code-guidelines/blob/master/wip/stacked-borrows.md for further information
help: <2912> was created by a SharedReadWrite retag at offsets [0x0..0x8000]
   --> src/bin/two_workers_example/main.rs:8:1
    |
8   | / messenger! {
9   | |     memory_bus::MemoryBus,
10  | |     WorkerA:
11  | |         handlers: [
...   |
25  | |         ]
26  | | }
    | |_^
help: <2912> was later invalidated at offsets [0x0..0x8000] by a Unique retag
   --> src/bin/two_workers_example/main.rs:8:1
    |
8   | / messenger! {
9   | |     memory_bus::MemoryBus,
10  | |     WorkerA:
11  | |         handlers: [
...   |
25  | |         ]
26  | | }
    | |_^
    = note: BACKTRACE (of the first span) on thread `unnamed-1`:
    = note: inside `std::slice::from_raw_parts_mut::<'_, u8>` at /Users/usr/.rustup/toolchains/nightly-x86_64-apple-darwin/lib/rustlib/src/rust/library/core/src/slice/raw.rs:162:9: 162:55
    = note: inside `<messenger::message_bus::memory_bus::MemoryBus as messenger::traits::Writer>::write::<messages::MessageB, handlers::HandlerA>` at /Users/usr/Developer/messenger/src/message_bus/memory_bus.rs:84:31: 84:78
    = note: inside `<handlers::HandlerA as messenger::traits::Sender>::send::<messages::MessageB, messenger::message_bus::memory_bus::MemoryBus>` at /Users/usr/Developer/messenger/src/traits.rs:39:9: 39:41
note: inside `<handlers::HandlerA as messenger::traits::Handler>::on_start::<messenger::message_bus::memory_bus::MemoryBus>`
   --> src/bin/two_workers_example/handlers.rs:42:9
    |
42  |         Self::send(messages::MessageB { other_val: 0 }, _writer);
    |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
note: inside `WorkerA::run::<messenger::message_bus::memory_bus::MemoryBus>`
   --> src/bin/two_workers_example/main.rs:8:1
    |
8   | / messenger! {
9   | |     memory_bus::MemoryBus,
10  | |     WorkerA:
11  | |         handlers: [
...   |
25  | |         ]
26  | | }
    | |_^
note: inside `WorkerA::run_task::<messenger::message_bus::memory_bus::MemoryBus>`
   --> src/bin/two_workers_example/main.rs:8:1
    |
8   | / messenger! {
9   | |     memory_bus::MemoryBus,
10  | |     WorkerA:
11  | |         handlers: [
...   |
25  | |         ]
26  | | }
    | |_^
note: inside closure
   --> src/bin/two_workers_example/main.rs:8:1
    |
8   | / messenger! {
9   | |     memory_bus::MemoryBus,
10  | |     WorkerA:
11  | |         handlers: [
...   |
25  | |         ]
26  | | }
    | |_^
    = note: this error originates in the macro `messenger` (in Nightly builds, run with -Z macro-backtrace for more info)

note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace

error: aborting due to 1 previous error

This has to do with the setup of my MemoryBus in src/message_bus/memory_bus.rs

impl MemoryBus {
    pub fn new() -> MemoryBus {
        let mut boxed = Box::new([0u8; BUFFER_SIZE]);
        let ptr = UnsafePtr::new(boxed.as_mut_ptr());
        let pinned_buffer = Box::into_pin(boxed);

        MemoryBus {
            buffer: std::sync::Arc::new(SharedBuffer {
                _buffer: std::sync::Mutex::new(PinnedBuffer {
                    _buffer: pinned_buffer,
                    _pin: std::marker::PhantomPinned,
                }),
                read_head: std::sync::atomic::AtomicUsize::new(0),
                write_head: std::sync::atomic::AtomicUsize::new(0),
            }),
            ptr: ptr,
        }
    }
}

How can i fix this issue?
Do i need a UnsafeCell?

"You can do it, and it would technically be sound, but I would recommend against it"
What other approach would you recommend?

The way you're using Pin/PhantomPinned isn't doing anything for you here, because [u8; BUFFER_SIZE] (your box pointee type) is still Unpin. If you just want Miri to accept the code, moving the PhantomPinned inside the Box should probably be sufficient. But that's not actually sufficient to be properly guaranteed sound.

Instead, use Box::into_raw to turn your Box into *mut [u8; BUFFER_SIZE] and store that instead, alongside a Drop implementation for MemoryBus which calls Box::from_raw to unleak the box and ensure the allocation is cleaned up. Thus you'll only be dealing with raw pointer semantics while the MemoryBus owns the buffer, which is a lot more straightforward to work with.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.