Skip to content

BufferViewMut is unsound due to WC memory #8897

@human-0

Description

@human-0

Description
When mapping a buffer, wgpu can give a slice to memory that does not behave correctly with Rust atomics on x86-64, probably because it uses write-combining memory.

Repro steps

use std::sync::atomic::{AtomicPtr, AtomicU64, Ordering};

fn test(mem: &mut u8) {
    const ITERS: u64 = 10_000;

    let counter = AtomicU64::new(0);
    let ptr = AtomicPtr::new(mem);

    std::thread::scope(|s| {
        s.spawn(|| {
            let ptr = ptr.load(Ordering::Relaxed);
            let mut value = 0;

            while value < ITERS {
                value += 1;

                unsafe {
                    ptr.write(value as u8);
                }

                counter.store(value, Ordering::Release);

                while !counter.load(Ordering::Acquire).is_multiple_of(2) {}
                value += 1;
            }
        });

        s.spawn(|| {
            let ptr = ptr.load(Ordering::Relaxed);

            let mut value = 0;
            while value < ITERS {
                while counter.load(Ordering::Acquire).is_multiple_of(2) {}
                value += 1;

                unsafe {
                    assert_eq!(ptr.read(), value as u8);
                }

                value += 1;
                counter.store(value, Ordering::Release);
            }
        });
    })
}

fn main() {
    // This works
    test(&mut 0);

    let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::default());

    let adapter_options = wgpu::RequestAdapterOptions {
        power_preference: wgpu::PowerPreference::HighPerformance,
        ..Default::default()
    };
    let adapter = pollster::block_on(instance.request_adapter(&adapter_options)).unwrap();

    let (device, _queue) =
        pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor::default())).unwrap();

    let buffer = device.create_buffer(&wgpu::BufferDescriptor {
        label: None,
        size: wgpu::COPY_BUFFER_ALIGNMENT,
        usage: wgpu::BufferUsages::MAP_WRITE,
        mapped_at_creation: true,
    });

    let mut buffer = buffer.get_mapped_range_mut(..);

    // This doesn't
    test(&mut buffer[0]);
}

The first thread writes sucessive odd numbers to the memory, and the second thread reads them. The raw pointer operations should be synchronised by the acquire/release operations, and the test sequence passes miri with regular memory.

Note that the sequence can be rewritten using relaxed atomic operations using AtomicU8::from_ptr, or without any unsafe code using AtomicU8::from_mut (unstable).

Expected vs observed behavior
The test function should exit successfully for both regular and wgpu mapped memory. However, with wgpu memory the test panics with:

assertion `left == right` failed
  left: 0
 right: 1

Platform
wgpu 28.0.0 on x86-64 Linux with Vulkan backend, Nvidia Pascal GPU with proprietary drivers. I suspect that this will reproduce on any discrete GPU with mappable device-local memory.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions