[PATCH 2/3] rust: dma: add abstraction for the single-buffer streaming DMA API
Maurice Hieronymus <[email protected]> Wed, 05 Aug 2026 23:54:42 +0200
| Newsgroups | org.kernel.vger.rust-for-linux,dev.linux.lists.driver-core,dev.linux.lists.nova-gpu,org.freedesktop.lists.dri-devel,org.kernel.vger.linux-kernel |
|---|---|
| Message-ID | <[email protected]> |
Add `Streaming`, a safe abstraction around `dma_map_single_attrs()`. Between map and unmap the buffer belongs to the device, and the CPU may only access it in between a `dma_sync_single_for_cpu()` / `dma_sync_single_for_device()` pair. The types encode that protocol: - `Streaming` owns the backing storage, so no other CPU-side reference to the region exists. - `submit()` consumes it and returns a `StreamingInFlight`, the only source of the `DmaAddress`. It owns the buffer, so the contents stay unreachable while a transfer may be in flight, no matter where the driver stores the address. - `for_cpu()` is therefore safe: no transfer can have been started from a `Streaming`. It syncs for the CPU, returns a guard dereferencing to the contents, and syncs for the device again on drop. - `complete()` turns a `StreamingInFlight` back into a `Streaming`. Whether the device has finished cannot be checked by any abstraction, so this is the one `unsafe` operation. Dropping a `StreamingInFlight` leaks the mapping and the backing storage, with a warning. A transfer may still be in flight: freeing the storage would leave the device writing through a dangling handle, and even unmapping could recycle a SWIOTLB bounce slot mid-transfer. Reclaiming either takes the assertion only `complete()` can make, so an early `?` return between `submit()` and `complete()` costs a leak instead of a device-side use-after-free. The mapping is torn down on drop of a `Streaming`, or by `into_inner()`, which returns the backing storage. Unmapping already hands the buffer back to the CPU, so `into_inner()` skips a synchronization nothing would consume. The `'a` lifetime binds the mapping to a `Device<Bound>`: the DMA API may only be called while a driver is bound, and `Drop` unmaps. `DataDirection::None` (a `BUG_ON()` in the DMA core) and empty buffers (not representable by `dma_map_single()`) are rejected with `EINVAL`. So are `DMA_ATTR_SKIP_CPU_SYNC`, which disables the implicit CPU cache maintenance the type invariants are built on with no way to compensate through this API, and `DMA_ATTR_MMIO`, which describes memory a `ContiguousBuffer` cannot represent. Signed-off-by: Maurice Hieronymus <[email protected]> --- rust/helpers/dma.c | 35 +++++ rust/kernel/dma.rs | 392 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 427 insertions(+) diff --git a/rust/helpers/dma.c b/rust/helpers/dma.c index 9fbeb507b08c..7e1e5c67431a 100644 --- a/rust/helpers/dma.c +++ b/rust/helpers/dma.c @@ -49,3 +49,38 @@ __rust_helper void rust_helper_dma_set_max_seg_size(struct device *dev, { dma_set_max_seg_size(dev, size); } + +__rust_helper dma_addr_t rust_helper_dma_map_single_attrs(struct device *dev, + void *ptr, size_t size, + enum dma_data_direction dir, + unsigned long attrs) +{ + return dma_map_single_attrs(dev, ptr, size, dir, attrs); +} + +__rust_helper void rust_helper_dma_unmap_single_attrs(struct device *dev, + dma_addr_t addr, size_t size, + enum dma_data_direction dir, + unsigned long attrs) +{ + dma_unmap_single_attrs(dev, addr, size, dir, attrs); +} + +__rust_helper int rust_helper_dma_mapping_error(struct device *dev, dma_addr_t addr) +{ + return dma_mapping_error(dev, addr); +} + +__rust_helper void rust_helper_dma_sync_single_for_cpu(struct device *dev, + dma_addr_t addr, size_t size, + enum dma_data_direction dir) +{ + dma_sync_single_for_cpu(dev, addr, size, dir); +} + +__rust_helper void rust_helper_dma_sync_single_for_device(struct device *dev, + dma_addr_t addr, size_t size, + enum dma_data_direction dir) +{ + dma_sync_single_for_device(dev, addr, size, dir); +} diff --git a/rust/kernel/dma.rs b/rust/kernel/dma.rs index 8a8af5ab7feb..cbaf30a2de86 100644 --- a/rust/kernel/dma.rs +++ b/rust/kernel/dma.rs @@ -24,6 +24,7 @@ uaccess::UserSliceWriter, }; use core::{ + mem::ManuallyDrop, ops::{ Deref, DerefMut, // @@ -345,6 +346,14 @@ const fn const_cast(val: bindings::dma_data_direction) -> u32 { // is within the representable range of `u32`. wide_val as u32 } + + /// Returns whether this direction may be passed to a mapping or synchronization primitive. + /// + /// Equivalent to `valid_dma_direction()`; [`Self::None`] is a debugging aid the DMA core + /// rejects with a `BUG_ON()`. + const fn is_valid(self) -> bool { + !matches!(self, Self::None) + } } impl From<DataDirection> for bindings::dma_data_direction { @@ -621,6 +630,389 @@ fn data(&mut self) -> &mut Self::Data { } } +/// An abstraction of the `dma_map_single` API. +/// +/// Unlike [`Coherent`], a streaming mapping is a temporary lease on memory the caller already +/// owns: between mapping and unmapping the buffer belongs to the device, and the CPU may only +/// access it in between a `dma_sync_single_for_cpu()` / `dma_sync_single_for_device()` pair. +/// +/// [`Streaming`] owns the backing storage and is one of two states: [`submit`](Self::submit) +/// consumes it and returns a [`StreamingInFlight`], the only source of the [`DmaAddress`]; +/// [`complete`](StreamingInFlight::complete) turns that back into a [`Streaming`], whose +/// [`for_cpu`](Self::for_cpu) yields a [`StreamingCpuGuard`] dereferencing to the contents. +/// +/// The mapping is torn down on drop of a [`Streaming`], or by [`into_inner`](Self::into_inner), +/// which returns the backing storage. Dropping a [`StreamingInFlight`] instead leaks the mapping +/// and the storage: safe code cannot prove the device is done, so it cannot be allowed to reclaim +/// either. +/// +/// The `'a` lifetime keeps the device bound for the life of the mapping. +/// +/// # Examples +/// +/// ``` +/// # use kernel::device::{Bound, Device}; +/// use kernel::dma::{ +/// DataDirection, +/// Streaming, // +/// }; +/// +/// # fn test(dev: &Device<Bound>) -> Result { +/// let buf = KBox::new(0u64, GFP_KERNEL)?; +/// let mut dma = Streaming::new(dev, buf, DataDirection::Bidirectional)?; +/// +/// // The CPU prepares the buffer, and hands it back to the device by dropping the guard. +/// *dma.for_cpu() = 42; +/// +/// // Hand the buffer to the device; `dma` is consumed, so its contents are now unreachable. +/// let dma = dma.submit(); +/// +/// // Program `dma.dma_handle()` and `dma.size()` into the device. +/// +/// // SAFETY: For the sake of the example, assume the transfer has been waited for. +/// let mut dma = unsafe { dma.complete() }; +/// +/// assert_eq!(*dma.for_cpu(), 42); +/// # Ok::<(), Error>(()) } +/// ``` +/// +/// # Invariants +/// +/// * `dma_addr` denotes a live mapping of `container` for the lifetime of the instance, and +/// `container`, `direction` and `dma_attrs` are unchanged since it was established. +/// * `direction` is not [`DataDirection::None`]. +/// * The buffer is synchronized for the device whenever no [`StreamingCpuGuard`] borrowed from +/// this instance is alive. +pub struct Streaming<'a, C: ContiguousBuffer> { + container: C, + direction: DataDirection, + dma_addr: DmaAddress, + dma_attrs: Attrs, + dev: &'a device::Device<Bound>, +} + +impl<'a, C: ContiguousBuffer> Streaming<'a, C> { + /// Maps `container` for streaming DMA in `direction`. + /// + /// Ownership of `container` is moved into the returned [`Streaming`]; the buffer belongs to + /// the device until [`for_cpu`](Self::for_cpu) is called. + /// + /// Returns [`EINVAL`] for an empty buffer, for [`DataDirection::None`], or if `dma_attrs` + /// contains [`DMA_ATTR_SKIP_CPU_SYNC`](attrs::DMA_ATTR_SKIP_CPU_SYNC) or + /// [`DMA_ATTR_MMIO`](attrs::DMA_ATTR_MMIO). + /// + /// # Examples + /// + /// ``` + /// # use kernel::device::{Bound, Device}; + /// use kernel::dma::{ + /// attrs::*, + /// DataDirection, + /// Streaming, // + /// }; + /// + /// # fn test(dev: &Device<Bound>) -> Result { + /// let buf = KBox::new(0u64, GFP_KERNEL)?; + /// let dma = Streaming::new_with_attrs( + /// dev, + /// buf, + /// DataDirection::ToDevice, + /// DMA_ATTR_WEAK_ORDERING, + /// )?; + /// # Ok::<(), Error>(()) } + /// ``` + pub fn new_with_attrs( + dev: &'a device::Device<Bound>, + mut container: C, + direction: DataDirection, + dma_attrs: Attrs, + ) -> Result<Self> { + // The DMA core `BUG_ON()`s on `DMA_NONE`, bail early. + if !direction.is_valid() { + return Err(EINVAL); + } + + // The type invariants are built on the implicit CPU cache maintenance performed by + // `dma_map_single_attrs()` and `dma_unmap_single_attrs()`; `DMA_ATTR_SKIP_CPU_SYNC` + // disables it, with no way to compensate through this API. `DMA_ATTR_MMIO` describes + // memory a `ContiguousBuffer` cannot represent. + if dma_attrs.contains(attrs::DMA_ATTR_SKIP_CPU_SYNC) + || dma_attrs.contains(attrs::DMA_ATTR_MMIO) + { + return Err(EINVAL); + } + + let size = container.size(); + + // `dma_map_single_attrs` cannot handle zero-length mappings, bail early. + if size == 0 { + return Err(EINVAL); + } + + // SAFETY: + // - Device pointer is guaranteed as valid by the type invariant on `Device`. + // - By the safety requirements of `ContiguousBuffer`, `container.ptr()` points to a single + // physically contiguous region of `size` bytes in the kernel's linear mapping. + // - `container` is moved into `Self` below, so the region stays alive and at a stable + // address until the mapping is torn down in `Drop`. + let dma_addr = unsafe { + bindings::dma_map_single_attrs( + dev.as_raw(), + container.ptr(), + size, + direction.into(), + dma_attrs.as_raw(), + ) + }; + + // SAFETY: Device pointer is valid per the above, and `dma_addr` was just returned by + // `dma_map_single_attrs()` for this device. + to_result(unsafe { bindings::dma_mapping_error(dev.as_raw(), dma_addr) })?; + + // INVARIANT: + // - The mapping was just established with these exact parameters, none of which is + // mutated afterwards. + // - `direction` was checked above. + Ok(Streaming { + container, + direction, + dma_addr, + dma_attrs, + dev, + }) + } + + /// Performs the same functionality as [`Streaming::new_with_attrs`], except the `dma_attrs` + /// is 0 by default. + #[inline] + pub fn new( + dev: &'a device::Device<Bound>, + container: C, + direction: DataDirection, + ) -> Result<Self> { + Self::new_with_attrs(dev, container, direction, Attrs(0)) + } + + /// Returns the size of the mapping in bytes. + #[inline] + pub fn size(&self) -> usize { + self.container.size() + } + + /// Returns the direction this buffer was mapped with. + #[inline] + pub fn direction(&self) -> DataDirection { + self.direction + } + + /// Hands the buffer to the device. + /// + /// This performs no synchronization: by the type invariants the buffer is already + /// synchronized for the device. + #[inline] + pub fn submit(self) -> StreamingInFlight<'a, C> { + StreamingInFlight(ManuallyDrop::new(self)) + } + + /// Transfers ownership of the buffer back to the CPU and returns a guard granting access to + /// its contents. + /// + /// Dropping the guard transfers ownership back to the device. If the buffer is not handed to + /// the device again, prefer [`into_inner`](Self::into_inner), which unmaps instead. + pub fn for_cpu(&mut self) -> StreamingCpuGuard<'_, C::Data> { + let dev = self.dev; + let dma_addr = self.dma_addr; + let direction = self.direction; + let size = self.container.size(); + + // SAFETY: By the type invariants, `dev` is bound and `dma_addr` denotes a live mapping of + // `size` bytes established with `direction`, which is the range synced here. + unsafe { + bindings::dma_sync_single_for_cpu(dev.as_raw(), dma_addr, size, direction.into()) + }; + + // INVARIANT: The buffer is now owned by the CPU, and dropping the guard hands it back. + StreamingCpuGuard { + data: self.container.data(), + dev, + dma_addr, + size, + direction, + } + } + + /// Tears the mapping down and returns the backing storage. + /// + /// Unmapping transfers ownership of the buffer back to the CPU, so no separate + /// [`for_cpu`](Self::for_cpu) is needed. + /// + /// # Examples + /// + /// ``` + /// # use kernel::device::{Bound, Device}; + /// use kernel::dma::{ + /// DataDirection, + /// Streaming, // + /// }; + /// + /// # fn test(dev: &Device<Bound>) -> Result { + /// let dma = Streaming::new( + /// dev, + /// KBox::new(0u64, GFP_KERNEL)?, + /// DataDirection::FromDevice, + /// )? + /// .submit(); + /// + /// // Program `dma.dma_handle()` into the device. + /// + /// // SAFETY: For the sake of the example, assume the transfer has been waited for. + /// let dma = unsafe { dma.complete() }; + /// + /// // Take the buffer back; the mapping is gone once this returns. + /// let buf: KBox<u64> = dma.into_inner(); + /// # Ok::<(), Error>(()) } + /// ``` + pub fn into_inner(self) -> C { + let mut this = ManuallyDrop::new(self); + + this.unmap(); + + // SAFETY: `this` is wrapped in a `ManuallyDrop`, so `Streaming::drop()` never runs and + // `this.container` is never read again. The remaining fields are all `Copy`. + unsafe { core::ptr::read(&this.container) } + } + + /// Tears the mapping down. + /// + /// Shared by [`Drop`] and [`into_inner`](Self::into_inner), both of which run it exactly once. + fn unmap(&mut self) { + // SAFETY: By the type invariants, `self.dev` is bound and the mapping is still live, with + // exactly the address, size, direction and attributes it was created with. Both callers + // run this at most once, so the mapping cannot be torn down twice. + unsafe { + bindings::dma_unmap_single_attrs( + self.dev.as_raw(), + self.dma_addr, + self.container.size(), + self.direction.into(), + self.dma_attrs.as_raw(), + ) + }; + } +} + +impl<C: ContiguousBuffer> Drop for Streaming<'_, C> { + fn drop(&mut self) { + self.unmap(); + } +} + +/// A [`Streaming`] mapping whose [`DmaAddress`] has been handed out. +/// +/// Returned by [`Streaming::submit`]. It owns the buffer, so the contents are unreachable while +/// it exists. [`complete`](Self::complete) is the only way back: it is the caller's assertion +/// that the device has finished, which nothing else can establish. Dropping this instead leaks +/// the mapping and the backing storage, since reclaiming either while the device may still +/// access the buffer would be a use-after-free. +pub struct StreamingInFlight<'a, C: ContiguousBuffer>(ManuallyDrop<Streaming<'a, C>>); + +impl<'a, C: ContiguousBuffer> StreamingInFlight<'a, C> { + /// Returns the DMA address to program into the device. + #[inline] + pub fn dma_handle(&self) -> DmaAddress { + self.0.dma_addr + } + + /// Returns the size of the mapping in bytes. + #[inline] + pub fn size(&self) -> usize { + self.0.size() + } + + /// Returns the direction this buffer was mapped with. + #[inline] + pub fn direction(&self) -> DataDirection { + self.0.direction() + } + + /// Takes the buffer back from the device. + /// + /// This performs no synchronization; [`Streaming::for_cpu`] does that. + /// + /// # Safety + /// + /// The device must have finished accessing the buffer. + #[inline] + pub unsafe fn complete(self) -> Streaming<'a, C> { + let mut this = ManuallyDrop::new(self); + + // SAFETY: `this` is wrapped in a `ManuallyDrop`, so `StreamingInFlight::drop()` never + // runs and `this.0` is never touched again. + unsafe { ManuallyDrop::take(&mut this.0) } + } +} + +impl<C: ContiguousBuffer> Drop for StreamingInFlight<'_, C> { + fn drop(&mut self) { + // A transfer may still be in flight: freeing the storage would leave the device writing + // through a dangling handle, and unmapping could recycle a SWIOTLB bounce slot + // mid-transfer. Reclaiming either requires the assertion only `complete()` can make, so + // leak both. + dev_warn!( + self.0.dev, + "StreamingInFlight dropped without complete(); leaking the mapping and its storage\n" + ); + } +} + +/// A guard granting the CPU access to the contents of a [`Streaming`] buffer. +/// +/// Returned by [`Streaming::for_cpu`]. Dropping it issues a `dma_sync_single_for_device()`, which +/// hands the buffer back to the device. +/// +/// # Invariants +/// +/// * `dev`, `dma_addr`, `size` and `direction` describe the live mapping of the [`Streaming`] this +/// guard borrows, and are unchanged for the lifetime of the guard. +/// * `data` refers to exactly the mapped region. +pub struct StreamingCpuGuard<'a, T: ?Sized> { + data: &'a mut T, + dev: &'a device::Device<Bound>, + dma_addr: DmaAddress, + size: usize, + direction: DataDirection, +} + +impl<T: ?Sized> Drop for StreamingCpuGuard<'_, T> { + fn drop(&mut self) { + // SAFETY: By the type invariants, `self.dev` is bound and `self.dma_addr` denotes a live + // mapping of `self.size` bytes established with `self.direction`, which is the range + // synced here. + unsafe { + bindings::dma_sync_single_for_device( + self.dev.as_raw(), + self.dma_addr, + self.size, + self.direction.into(), + ) + }; + } +} + +impl<T: ?Sized> Deref for StreamingCpuGuard<'_, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.data + } +} + +impl<T: ?Sized> DerefMut for StreamingCpuGuard<'_, T> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.data + } +} + /// An abstraction of the `dma_alloc_coherent` API. /// /// This is an abstraction around the `dma_alloc_coherent` API which is used to allocate and map -- 2.54.0