Skip to main content

godspeed_sdk/
dma.rs

1// SPDX-License-Identifier: Apache-2.0
2//! DMA arena access for userspace driver services (§12, §18).
3//!
4//! Part of the SDK's audited hardware/ABI layer (§18.1), alongside `mmio.rs` and
5//! the syscall ABI. A [`Dma`] wraps a kernel-granted, physically-contiguous
6//! arena: the driver builds device structures in it (via the read/write
7//! helpers) and hands the controller physical addresses (via [`Dma::phys_at`]).
8//! Driver services use this safe wrapper and never write `unsafe` themselves;
9//! `Dma` is only constructable inside this crate, from a kernel-granted region.
10
11/// A physically-contiguous DMA arena granted to a driver (e.g. via
12/// [`crate::ServiceContext::dma_region`]). The CPU accesses it through `base`, the device through
13/// `phys`. Both views cover the same `len` bytes one-to-one.
14///
15/// SEC-28 (SMP-port contract, `kernel/src/arch/CLAUDE.md`): this wrapper does NO cache maintenance.
16/// That is correct only because of how each port maps the arena (`DMA_ARENA_UNCACHED`): x86 maps it
17/// cacheable, its DMA being coherent; arm32 and aarch64, whose DMA is not, map it UNCACHED, which is
18/// their answer to SEC-28; riscv64 maps it cacheable. A port whose DMA is not coherent must map it
19/// uncached too, or add maintenance here - else the CPU and the device can see stale copies.
20#[derive(Clone, Copy)]
21pub struct Dma {
22    base: *mut u8,
23    phys: u64,
24    len: usize,
25}
26
27impl Dma {
28    /// Crate-internal: only the SDK constructs a `Dma`, from a kernel-granted
29    /// region, which is what makes the volatile accesses below sound.
30    pub(crate) fn new(base: *mut u8, phys: u64, len: usize) -> Self {
31        Self { base, phys, len }
32    }
33
34    /// Physical base address - program this (plus offsets) into the controller.
35    #[inline]
36    pub fn phys_base(&self) -> u64 {
37        self.phys
38    }
39
40    /// Length of the arena in bytes.
41    #[inline]
42    pub fn len(&self) -> usize {
43        self.len
44    }
45
46    /// Bounds-check an `off..off+size` access against the arena length (SEC-4). A driver bug that
47    /// lets a device-supplied value drive `off` past the arena is caught here as a loud panic
48    /// (killing only the one driver) instead of a silent out-of-arena CPU access - which, for an
49    /// IOMMU-passthrough driver, is a write ANYWHERE in RAM (§26.7). `checked_add` so a wrapping
50    /// `off` cannot slip past. The `unsafe` accessors below are only actually memory-safe with this.
51    #[inline]
52    fn check(&self, off: usize, size: usize) {
53        assert!(
54            off.checked_add(size).map_or(false, |end| end <= self.len),
55            "Dma access out of arena bounds",
56        );
57    }
58
59    /// Physical address of byte offset `off` within the arena.
60    #[inline]
61    pub fn phys_at(&self, off: usize) -> u64 {
62        self.check(off, 0);
63        self.phys + off as u64
64    }
65
66    /// Zero the whole arena.
67    ///
68    /// **Deliberately not `write_bytes`.** That lowers to `memset`, and a `memset` is free to reach
69    /// for whatever the ISA makes fastest - unaligned stores, cache-line zeroing hints (AArch64's
70    /// `DC ZVA`), non-temporal stores. Every one of those is legal on the Normal cacheable memory
71    /// x86 maps this arena as, and every one of them is a **fault** on the Device memory a
72    /// non-coherent port maps it as instead (`DMA_ARENA_UNCACHED`, the SEC-28 answer): Device-nGnRnE
73    /// forbids unaligned access outright and cache maintenance by address on it is not meaningful.
74    ///
75    /// So the clear is an explicit aligned 64-bit volatile loop, which is the one shape that is
76    /// correct on both. The arena is page-based (a whole number of 4 KiB frames at a page-aligned
77    /// VA), so the fast path always applies; the byte tail exists only so this function is total
78    /// rather than silently leaving a remainder, and it cannot run on a real grant.
79    ///
80    /// `volatile` matters as much as the alignment: an ordinary store loop over Device memory is
81    /// something the optimiser may merge back into a `memset` call, which would reintroduce exactly
82    /// what this avoids.
83    pub fn zero(&self) {
84        let words = self.len / 8;
85        for i in 0..words {
86            // SAFETY: base..base+len is the kernel-granted mapped arena (Dma is only constructed
87            // from one) and is page-aligned, so `base + i*8` is 8-byte aligned and in bounds for
88            // every i < len/8.
89            unsafe { core::ptr::write_volatile(self.base.add(i * 8) as *mut u64, 0) }
90        }
91        for off in words * 8..self.len {
92            // SAFETY: off < len, so this is in bounds; a byte store needs no alignment.
93            unsafe { core::ptr::write_volatile(self.base.add(off), 0) }
94        }
95    }
96
97    // ---------------------------------------------------------------------------------------------
98    // ALIGNMENT
99    //
100    // The multi-byte accessors below compose from BYTE accesses whenever the offset is not naturally
101    // aligned. That is not defensiveness; on a non-coherent arch this arena is mapped **Device**
102    // memory (AArch64 Device-nGnRnE, §SEC-28 - the mapping is what removes the need for cache
103    // maintenance), and Device memory does not permit unaligned access AT ALL. An unaligned 16- or
104    // 32-bit load there is not slow, it is an ALIGNMENT FAULT.
105    //
106    // The doc comments used to say "2-byte aligned" / "4-byte aligned" and leave it at that, which
107    // put the obligation on every caller. Drivers cannot honour it: parsing a USB configuration
108    // descriptor means walking to offsets the DEVICE chose, so `wMaxPacketSize` lands at an odd
109    // address whenever the descriptors before it summed to an odd length. The Pi 4 found this the
110    // first time the xhci service reached a real hub:
111    //
112    //     ESR_EL1 = 0x92000021 (data abort, DFSC 0b100001 = alignment fault)
113    //     FAR_EL1 = 0x200005003          <- arena base + 0x5003, an ODD offset
114    //     task    = xhci
115    //
116    // x86 hides this completely: unaligned loads just work there, so every such call site is correct
117    // on the arch it was written for and faults on the arch it was ported to. Putting the fix HERE
118    // rather than at the call sites means it cannot be forgotten by the next driver.
119    //
120    // Aligned offsets keep the single wide volatile access, so nothing on the fast path pays for it.
121
122    /// Read an 8-bit value at byte offset `off` (`off < len`).
123    #[inline]
124    pub fn read8(&self, off: usize) -> u8 {
125        self.check(off, 1);
126        // SAFETY: base is a valid kernel-granted mapping; check() bounded off in range.
127        unsafe { core::ptr::read_volatile(self.base.add(off)) }
128    }
129
130    /// Read a 16-bit value at byte offset `off` (`off + 2 <= len`; any alignment - see ALIGNMENT above).
131    #[inline]
132    pub fn read16(&self, off: usize) -> u16 {
133        self.check(off, 2);
134        if off & 1 != 0 {
135            return u16::from_le_bytes([self.read8(off), self.read8(off + 1)]);
136        }
137        // SAFETY: as read8; aligned 16-bit access in range.
138        unsafe { core::ptr::read_volatile(self.base.add(off) as *const u16) }
139    }
140
141    /// Read a 32-bit value at byte offset `off` (`off + 4 <= len`; any alignment - see ALIGNMENT above).
142    #[inline]
143    pub fn read32(&self, off: usize) -> u32 {
144        self.check(off, 4);
145        if off & 3 != 0 {
146            return u32::from_le_bytes([
147                self.read8(off), self.read8(off + 1), self.read8(off + 2), self.read8(off + 3),
148            ]);
149        }
150        // SAFETY: base is a valid kernel-granted mapping; check() bounded off in range.
151        unsafe { core::ptr::read_volatile(self.base.add(off) as *const u32) }
152    }
153
154    /// Write a 32-bit value at byte offset `off` (`off + 4 <= len`; any alignment - see ALIGNMENT above).
155    #[inline]
156    pub fn write32(&self, off: usize, val: u32) {
157        self.check(off, 4);
158        if off & 3 != 0 {
159            let b = val.to_le_bytes();
160            for (k, v) in b.iter().enumerate() { self.write8(off + k, *v); }
161            return;
162        }
163        // SAFETY: as read32; volatile so the device observes ordered writes.
164        unsafe { core::ptr::write_volatile(self.base.add(off) as *mut u32, val) }
165    }
166
167    /// Write an 8-bit value at byte offset `off` (`off < len`). For byte-granular
168    /// device structures (e.g. an e1000 TX descriptor's command byte, or frame bytes).
169    #[inline]
170    pub fn write8(&self, off: usize, val: u8) {
171        self.check(off, 1);
172        // SAFETY: as read8; volatile so the device observes ordered writes.
173        unsafe { core::ptr::write_volatile(self.base.add(off), val) }
174    }
175
176    /// Write a 16-bit value at byte offset `off` (`off + 2 <= len`; any alignment - see ALIGNMENT above).
177    #[inline]
178    pub fn write16(&self, off: usize, val: u16) {
179        self.check(off, 2);
180        if off & 1 != 0 {
181            let b = val.to_le_bytes();
182            self.write8(off, b[0]);
183            self.write8(off + 1, b[1]);
184            return;
185        }
186        // SAFETY: as read16; volatile 16-bit write in range.
187        unsafe { core::ptr::write_volatile(self.base.add(off) as *mut u16, val) }
188    }
189
190    /// Read a 64-bit value at byte offset `off` (`off + 8 <= len`; any alignment - see ALIGNMENT above).
191    #[inline]
192    pub fn read64(&self, off: usize) -> u64 {
193        self.check(off, 8);
194        if off & 7 != 0 {
195            return (self.read32(off) as u64) | ((self.read32(off + 4) as u64) << 32);
196        }
197        // SAFETY: as read32; 64-bit aligned access in range.
198        unsafe { core::ptr::read_volatile(self.base.add(off) as *const u64) }
199    }
200
201    /// Write a 64-bit value at byte offset `off` (`off + 8 <= len`; any alignment - see ALIGNMENT above).
202    #[inline]
203    pub fn write64(&self, off: usize, val: u64) {
204        self.check(off, 8);
205        if off & 7 != 0 {
206            self.write32(off, val as u32);
207            self.write32(off + 4, (val >> 32) as u32);
208            return;
209        }
210        // SAFETY: as read32; 64-bit aligned access in range.
211        unsafe { core::ptr::write_volatile(self.base.add(off) as *mut u64, val) }
212    }
213}