[PATCH 2/3] rust: dma: add abstraction for the single-buffer streaming DMA API

From: Maurice Hieronymus

Date: Wed Aug 05 2026 - 17:56:50 EST


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 <mhi@xxxxxxxxxxx>
---
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