Skip to main content

storage_device/
pinned_buffer_allocator.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::buffer::{BufferAllocator as BufferAllocatorTrait, BufferImpl, OwnedBuffer};
6use crate::buffer_allocator::{BufferAllocator, BufferFuture, BufferSource, TryAllocateBuffer};
7use event_listener::{EventListener, Listener as _};
8use fuchsia_sync::Mutex;
9use std::cell::UnsafeCell;
10use std::fmt::Debug;
11use std::ops::Range;
12use std::sync::Arc;
13use zx::sys::zx_paddr_t;
14
15/// Default chunk size for pinning DMA buffers (1 MiB).
16pub const DEFAULT_PIN_CHUNK_SIZE: usize = 1024 * 1024;
17
18#[derive(Debug)]
19struct Chunk {
20    pmt: Option<zx::Pmt>,
21    // Number of active buffer allocations spanning this chunk.
22    ref_count: usize,
23}
24
25#[derive(Debug)]
26struct PinnedInner {
27    chunks: Vec<Chunk>,
28}
29
30/// A specialized buffer allocator that pins memory in coarse chunks (e.g. 1 MiB) for DMA.
31///
32/// The first chunk (chunk 0) is pinned at initialization and remains pinned for the lifetime of
33/// the allocator to eliminate pinning latency on requests. Additional chunks are pinned
34/// dynamically on demand when buffers spill over into them, and unpinned when all buffers within
35/// those chunks are dropped.
36///
37/// This allocator relies on [`BufferAllocator`]'s lowest-offset-first allocation strategy so that
38/// allocations pack into the permanently pinned first chunk before spilling over into dynamically
39/// pinned chunks.
40pub struct PinnedBufferAllocator {
41    allocator: BufferAllocator,
42    bti: zx::Bti,
43    contiguity: u64,
44    chunk_size: usize,
45    inner: Mutex<PinnedInner>,
46    paddrs: Box<[UnsafeCell<zx_paddr_t>]>,
47}
48
49// SAFETY: Synchronization of writes to `paddrs` is guarded by `inner: Mutex<PinnedInner>`:
50// - A chunk's entries in `paddrs` are only written when `pmt.is_none()` under the lock.
51// - While `pmt.is_some()`, the chunk's entries are immutable and never modified or unpinned.
52// - Readers calling `paddrs()` hold a `Buffer` whose range is in that chunk, guaranteeing
53//   `pmt.is_some()` and exclusive immutable access to those entries for the buffer's lifetime.
54unsafe impl Send for PinnedBufferAllocator {}
55unsafe impl Sync for PinnedBufferAllocator {}
56
57impl Debug for PinnedBufferAllocator {
58    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59        f.debug_struct("PinnedBufferAllocator")
60            .field("block_size", &self.block_size())
61            .field("chunk_size", &self.chunk_size)
62            .field("contiguity", &self.contiguity)
63            .finish_non_exhaustive()
64    }
65}
66
67pub type PinnedBuffer<'a> = BufferImpl<'a, &'a PinnedBufferAllocator, PinnedBufferAllocator>;
68
69pub type PinnedBufferFuture<'a> = BufferFuture<'a, PinnedBufferAllocator>;
70
71impl<'a> TryAllocateBuffer<'a> for PinnedBufferAllocator {
72    type Buffer = PinnedBuffer<'a>;
73
74    fn try_allocate_buffer(&'a self, size: usize) -> Result<PinnedBuffer<'a>, EventListener> {
75        self.try_allocate_buffer(size)
76    }
77}
78
79impl PinnedBufferAllocator {
80    /// Creates a new `PinnedBufferAllocator` with default chunk size (1 MiB).
81    ///
82    /// `contiguity` specifies the minimum physical address contiguity (in bytes) required by the
83    /// hardware/BTI, which determines the granularity of entries in [`paddrs`](Self::paddrs).
84    pub fn new(block_size: usize, source: BufferSource, bti: zx::Bti, contiguity: u64) -> Self {
85        Self::with_chunk_size(block_size, source, bti, contiguity, DEFAULT_PIN_CHUNK_SIZE)
86    }
87
88    /// Creates a new `PinnedBufferAllocator` with a custom `chunk_size`.
89    ///
90    /// `contiguity` specifies the minimum physical address contiguity (in bytes) required by the
91    /// hardware/BTI, which determines the granularity of entries in [`paddrs`](Self::paddrs).
92    pub fn with_chunk_size(
93        block_size: usize,
94        source: BufferSource,
95        bti: zx::Bti,
96        contiguity: u64,
97        chunk_size: usize,
98    ) -> Self {
99        assert!(!source.is_trusted(), "PinnedBufferAllocator cannot use a trusted buffer source");
100        assert!(chunk_size.is_power_of_two());
101        assert!(chunk_size >= contiguity as usize);
102        assert!(chunk_size % contiguity as usize == 0);
103        let num_chunks = source.size().div_ceil(chunk_size);
104        let total_paddrs = source.size().div_ceil(contiguity as usize);
105        let mut paddrs = Vec::with_capacity(total_paddrs);
106        for _ in 0..total_paddrs {
107            paddrs.push(UnsafeCell::new(0));
108        }
109        let mut chunks = Vec::with_capacity(num_chunks);
110        for _ in 0..num_chunks {
111            chunks.push(Chunk { pmt: None, ref_count: 0 });
112        }
113        let allocator = BufferAllocator::new(block_size, source);
114        let this = Self {
115            allocator,
116            bti,
117            contiguity,
118            chunk_size,
119            inner: Mutex::new(PinnedInner { chunks }),
120            paddrs: paddrs.into_boxed_slice(),
121        };
122        if num_chunks > 0 {
123            let mut inner = this.inner.lock();
124            this.pin_chunk_locked(&mut inner, 0);
125        }
126        this
127    }
128
129    pub fn block_size(&self) -> usize {
130        self.allocator.block_size()
131    }
132
133    pub fn chunk_size(&self) -> usize {
134        self.chunk_size
135    }
136
137    pub fn contiguity(&self) -> u64 {
138        self.contiguity
139    }
140
141    pub fn buffer_source(&self) -> &BufferSource {
142        self.allocator.buffer_source()
143    }
144
145    /// Returns the underlying VMO for DMA operations.
146    pub fn vmo(&self) -> Option<Arc<zx::Vmo>> {
147        self.allocator.vmo()
148    }
149
150    pub fn is_trusted(&self) -> bool {
151        false
152    }
153
154    /// Decommits unallocated pages in the buffer so the kernel can reclaim memory.
155    pub fn clean_transfer_buffer(&self) {
156        self.allocator.clean_transfer_buffer();
157    }
158
159    fn pin_chunk_locked(&self, inner: &mut PinnedInner, chunk_idx: usize) {
160        let chunk = &mut inner.chunks[chunk_idx];
161        if chunk.pmt.is_some() {
162            return;
163        }
164        let chunk_offset = chunk_idx * self.chunk_size;
165        let chunk_len =
166            std::cmp::min(self.chunk_size, self.allocator.buffer_source().size() - chunk_offset);
167        let contiguity_usize = self.contiguity as usize;
168        let paddr_start = chunk_offset / contiguity_usize;
169        let num_paddrs = chunk_len.div_ceil(contiguity_usize);
170        // SAFETY: We hold `inner` lock and `chunk.pmt.is_none()`, so no other thread is
171        // reading or writing to this slice of `paddrs`.
172        let paddr_slice = unsafe {
173            let ptr = self.paddrs[paddr_start].get();
174            std::slice::from_raw_parts_mut(ptr, num_paddrs)
175        };
176        let options =
177            zx::BtiOptions::PERM_READ | zx::BtiOptions::PERM_WRITE | zx::BtiOptions::COMPRESS;
178        let vmo = self.allocator.vmo().expect("PinnedBufferAllocator requires an untrusted VMO");
179        let pmt = self
180            .bti
181            .pin(options, &vmo, chunk_offset as u64, chunk_len as u64, paddr_slice)
182            .unwrap_or_else(|status| {
183                panic!("Failed to pin chunk {chunk_idx}: {status:?}");
184            });
185        chunk.pmt = Some(pmt);
186    }
187
188    fn pin_range(&self, range: &Range<usize>) {
189        let start_chunk = range.start / self.chunk_size;
190        let end_chunk = (range.end + self.chunk_size - 1) / self.chunk_size;
191        let mut inner = self.inner.lock();
192        for chunk_idx in start_chunk..end_chunk {
193            self.pin_chunk_locked(&mut inner, chunk_idx);
194            inner.chunks[chunk_idx].ref_count += 1;
195        }
196    }
197
198    pub(crate) fn free_buffer(&self, range: Range<usize>) {
199        let start_chunk = range.start / self.chunk_size;
200        let end_chunk = (range.end + self.chunk_size - 1) / self.chunk_size;
201        let mut inner = self.inner.lock();
202        for chunk_idx in start_chunk..end_chunk {
203            let chunk = &mut inner.chunks[chunk_idx];
204            assert!(chunk.ref_count > 0);
205            chunk.ref_count -= 1;
206            // The first chunk (chunk 0) remains pinned for the lifetime of the allocator.
207            if chunk.ref_count == 0 && chunk_idx > 0 {
208                if let Some(pmt) = chunk.pmt.take() {
209                    // SAFETY: All buffers allocated in this chunk have been dropped.
210                    let _ = unsafe { pmt.unpin() };
211                }
212                let chunk_offset = chunk_idx * self.chunk_size;
213                let chunk_len = std::cmp::min(
214                    self.chunk_size,
215                    self.allocator.buffer_source().size() - chunk_offset,
216                );
217                // Zero/decommit the unpinned chunk so the kernel reclaims physical memory.
218                unsafe {
219                    self.allocator
220                        .buffer_source()
221                        .clean_range(chunk_offset..chunk_offset + chunk_len);
222                }
223            }
224        }
225        drop(inner);
226        self.allocator.free_buffer(range);
227    }
228
229    /// Returns a slice of physical addresses covering `range` and the contiguity granularity (in
230    /// bytes) represented by each address in the slice.
231    pub fn paddrs(&self, range: &Range<usize>) -> Option<(&[zx_paddr_t], u64)> {
232        let contiguity_usize = self.contiguity as usize;
233        let start_page = range.start / contiguity_usize;
234        let end_page = (range.end + contiguity_usize - 1) / contiguity_usize;
235        let num_paddrs = end_page - start_page;
236        // SAFETY: The caller holds an active Buffer for `range`, which guarantees that the
237        // chunks covering `range` have `ref_count > 0` (or `chunk_idx == 0`)
238        // and are pinned with immutable paddrs.
239        let slice = unsafe {
240            let ptr = self.paddrs[start_page].get();
241            std::slice::from_raw_parts(ptr, num_paddrs)
242        };
243        Some((slice, self.contiguity))
244    }
245
246    pub fn try_allocate_buffer(&self, size: usize) -> Result<PinnedBuffer<'_>, EventListener> {
247        let buffer = self.allocator.try_allocate_buffer(size)?;
248        let range = buffer.range();
249        self.pin_range(&range);
250        let slice = unsafe { self.allocator.buffer_source().subslice_ptr(&range) };
251        // Forget the inner buffer so its Drop doesn't free the allocation prematurely;
252        // ownership is transferred to the returned PinnedBuffer.
253        std::mem::forget(buffer);
254        Ok(BufferImpl::new(slice, range, self))
255    }
256
257    pub fn allocate_buffer_sync(&self, size: usize) -> PinnedBuffer<'_> {
258        <Self as TryAllocateBuffer>::allocate_buffer_sync(self, size)
259    }
260
261    pub fn allocate_buffer(&self, size: usize) -> PinnedBufferFuture<'_> {
262        BufferFuture::new(self, size)
263    }
264
265    pub fn try_allocate_buffer_owned(
266        self: &Arc<Self>,
267        size: usize,
268    ) -> Result<OwnedBuffer, EventListener> {
269        let buffer = self.allocator.try_allocate_buffer(size)?;
270        let range = buffer.range();
271        self.pin_range(&range);
272        let slice = unsafe { self.allocator.buffer_source().subslice_ptr_unbounded(&range) };
273        // Forget the inner buffer so its Drop doesn't free the allocation prematurely;
274        // ownership is transferred to the returned PinnedBuffer.
275        std::mem::forget(buffer);
276        Ok(BufferImpl::new(slice, range, self.clone()))
277    }
278
279    pub fn allocate_buffer_sync_owned(self: &Arc<Self>, size: usize) -> OwnedBuffer {
280        loop {
281            match self.try_allocate_buffer_owned(size) {
282                Ok(buffer) => return buffer,
283                Err(listener) => listener.wait(),
284            }
285        }
286    }
287}
288
289impl Drop for PinnedBufferAllocator {
290    fn drop(&mut self) {
291        let mut inner = self.inner.lock();
292        for chunk in &mut inner.chunks {
293            if let Some(pmt) = chunk.pmt.take() {
294                // SAFETY: Allocator is being dropped, no DMA in flight.
295                let _ = unsafe { pmt.unpin() };
296            }
297        }
298    }
299}
300
301impl BufferAllocatorTrait for PinnedBufferAllocator {
302    fn free_buffer(&self, range: Range<usize>) {
303        self.free_buffer(range);
304    }
305
306    fn identifier(&self) -> usize {
307        std::ptr::from_ref(self).addr()
308    }
309
310    fn is_trusted(&self) -> bool {
311        false
312    }
313
314    fn vmo(&self) -> Option<Arc<zx::Vmo>> {
315        self.vmo()
316    }
317
318    fn paddrs(&self, range: &Range<usize>) -> Option<(&[zx_paddr_t], u64)> {
319        self.paddrs(range)
320    }
321}
322
323#[cfg(test)]
324mod tests {
325    use super::*;
326    use fake_bti::FakeBti;
327    use zx::Rights;
328
329    #[fuchsia::test]
330    async fn test_pinned_buffer() {
331        let fake_bti = FakeBti::create().expect("failed to create fake BTI");
332        fake_bti.set_paddrs(&[4096, 8192]);
333        let source = BufferSource::new(8192);
334        // Chunk size of 4096 so 8192 bytes = 2 chunks. Always keep chunk 0 pinned.
335        let allocator = Arc::new(PinnedBufferAllocator::with_chunk_size(
336            512,
337            source,
338            fake_bti.duplicate_handle(Rights::SAME_RIGHTS).unwrap(),
339            4096,
340            4096,
341        ));
342
343        assert!(!allocator.is_trusted());
344        assert!(allocator.vmo().is_some());
345        assert!(BufferAllocatorTrait::vmo(allocator.as_ref()).is_some());
346
347        // First allocation in chunk 0 (which is pre-pinned at initialization).
348        let buf = allocator.allocate_buffer(512).await;
349        assert_eq!(buf.range(), 0..512);
350        assert_eq!(buf.contiguity(), Some(4096));
351        assert_eq!(buf.paddrs(), Some(&[4096][..]));
352        assert!(buf.try_as_slice().is_none());
353        assert!(buf.vmo().is_some());
354
355        // Second allocation in the same chunk shares the existing pin.
356        let buf2 = allocator.allocate_buffer_sync(512);
357        assert_eq!(buf2.range(), 512..1024);
358        assert_eq!(buf2.contiguity(), Some(4096));
359        assert_eq!(buf2.paddrs(), Some(&[4096][..]));
360
361        // Allocation in chunk 1 pins chunk 1 dynamically.
362        let buf_chunk1 = allocator.allocate_buffer_sync_owned(4096);
363        assert_eq!(buf_chunk1.range(), 4096..8192);
364        assert_eq!(buf_chunk1.contiguity(), Some(4096));
365        assert_eq!(buf_chunk1.paddrs(), Some(&[8192][..]));
366
367        // Dropping buf_chunk1 unpins chunk 1 because chunk 1 > 0.
368        std::mem::drop(buf_chunk1);
369
370        // Dropping chunk 0 buffers leaves chunk 0 pinned because chunk 0 is always pinned.
371        std::mem::drop(buf);
372        std::mem::drop(buf2);
373
374        // Next allocation reuses chunk 0 and is immediately pinned without touching chunk 1.
375        let buf3 = allocator.allocate_buffer(512).await;
376        assert_eq!(buf3.range(), 0..512);
377        assert_eq!(buf3.contiguity(), Some(4096));
378        assert_eq!(buf3.paddrs(), Some(&[4096][..]));
379    }
380
381    #[fuchsia::test]
382    async fn test_spanning_chunks() {
383        let fake_bti = FakeBti::create().expect("failed to create fake BTI");
384        fake_bti.set_paddrs(&[4096, 8192]);
385        let source = BufferSource::new(8192);
386        // Chunk size 4096, contiguity 4096 -> 2 chunks.
387        let allocator = PinnedBufferAllocator::with_chunk_size(
388            512,
389            source,
390            fake_bti.duplicate_handle(Rights::SAME_RIGHTS).unwrap(),
391            4096,
392            4096,
393        );
394
395        // An 8192-byte allocation spans across chunk 0 and chunk 1.
396        let mut buf = allocator.allocate_buffer(8192).await;
397        assert_eq!(buf.range(), 0..8192);
398        assert_eq!(buf.contiguity(), Some(4096));
399        assert_eq!(buf.paddrs(), Some(&[4096, 8192][..]));
400
401        // Write and read data across both chunks.
402        buf.as_mut_ptr_slice().fill(0xab);
403        assert_eq!(buf.as_ptr_slice().to_vec(), vec![0xab; 8192]);
404
405        // Dropping the spanning buffer unpins chunk 1 while chunk 0 remains pinned.
406        std::mem::drop(buf);
407
408        // Next allocation in chunk 0 is immediately available with chunk 0's paddr.
409        let buf_c0 = allocator.allocate_buffer_sync(512);
410        assert_eq!(buf_c0.range(), 0..512);
411        assert_eq!(buf_c0.paddrs(), Some(&[4096][..]));
412    }
413
414    #[fuchsia::test]
415    async fn test_multiple_chunks_independent_lifecycle() {
416        let fake_bti = FakeBti::create().expect("failed to create fake BTI");
417        // Chunk 0 gets 0x1000 at init.
418        // Chunk 1 gets 0x2000 on first pin.
419        // Chunk 2 gets 0x3000 on pin.
420        // Chunk 1 gets 0x4000 on repin.
421        fake_bti.set_paddrs(&[0x1000, 0x2000, 0x3000, 0x4000]);
422        let source = BufferSource::new(16384);
423        // 4 chunks of 4096 bytes.
424        let allocator = PinnedBufferAllocator::with_chunk_size(
425            512,
426            source,
427            fake_bti.duplicate_handle(Rights::SAME_RIGHTS).unwrap(),
428            4096,
429            4096,
430        );
431
432        // Fill chunk 0.
433        let buf0 = allocator.allocate_buffer_sync(4096);
434        assert_eq!(buf0.paddrs(), Some(&[0x1000][..]));
435
436        // Allocate in chunk 1 and chunk 2.
437        let buf1 = allocator.allocate_buffer_sync(4096);
438        assert_eq!(buf1.paddrs(), Some(&[0x2000][..]));
439        let buf2 = allocator.allocate_buffer_sync(4096);
440        assert_eq!(buf2.paddrs(), Some(&[0x3000][..]));
441
442        // Drop buf1: chunk 1 is unpinned, but chunk 2 remains pinned.
443        std::mem::drop(buf1);
444        assert_eq!(buf2.paddrs(), Some(&[0x3000][..]));
445
446        // Allocate again: lowest-offset-first reuses chunk 1, dynamically re-pinning it.
447        let buf1_again = allocator.allocate_buffer_sync(4096);
448        assert_eq!(buf1_again.range(), 4096..8192);
449        assert_eq!(buf1_again.paddrs(), Some(&[0x4000][..]));
450
451        // Dropping buf2 unpins chunk 2.
452        std::mem::drop(buf2);
453
454        // Dropping buf1_again unpins chunk 1.
455        std::mem::drop(buf1_again);
456
457        // Dropping buf0 leaves chunk 0 pinned.
458        std::mem::drop(buf0);
459
460        // Chunk 0 is still pinned and retains 0x1000.
461        let buf0_again = allocator.allocate_buffer_sync(512);
462        assert_eq!(buf0_again.paddrs(), Some(&[0x1000][..]));
463    }
464
465    #[fuchsia::test]
466    async fn test_clean_transfer_buffer() {
467        let fake_bti = FakeBti::create().expect("failed to create fake BTI");
468        fake_bti.set_paddrs(&[4096, 8192]);
469        let source = BufferSource::new(8192);
470        let allocator = PinnedBufferAllocator::with_chunk_size(
471            512,
472            source,
473            fake_bti.duplicate_handle(Rights::SAME_RIGHTS).unwrap(),
474            4096,
475            4096,
476        );
477
478        let buf = allocator.allocate_buffer(4096).await;
479        // Clean unallocated memory while a buffer is active.
480        allocator.clean_transfer_buffer();
481        // Buffer is still valid and usable.
482        assert_eq!(buf.paddrs(), Some(&[4096][..]));
483    }
484
485    #[fuchsia::test]
486    async fn test_concurrent_pinned_allocations() {
487        use fuchsia_async as fasync;
488        let fake_bti = FakeBti::create().expect("failed to create fake BTI");
489        let source = BufferSource::new(16384);
490        let allocator = Arc::new(PinnedBufferAllocator::with_chunk_size(
491            512,
492            source,
493            fake_bti.duplicate_handle(Rights::SAME_RIGHTS).unwrap(),
494            4096,
495            4096,
496        ));
497
498        let mut tasks = Vec::new();
499        for i in 0..8 {
500            let alloc = allocator.clone();
501            tasks.push(async move {
502                let mut buf = alloc.allocate_buffer(512).await;
503                assert!(buf.paddrs().is_some());
504                buf.as_mut_ptr_slice().fill(i as u8);
505                fasync::Timer::new(std::time::Duration::from_millis(5)).await;
506                assert_eq!(buf.as_ptr_slice().to_vec(), vec![i as u8; 512]);
507            });
508        }
509        futures::future::join_all(tasks).await;
510    }
511
512    #[fuchsia::test]
513    async fn test_buffer_subslicing() {
514        let fake_bti = FakeBti::create().expect("failed to create fake BTI");
515        fake_bti.set_paddrs(&[4096]);
516        let source = BufferSource::new(4096);
517        let allocator = PinnedBufferAllocator::new(
518            512,
519            source,
520            fake_bti.duplicate_handle(Rights::SAME_RIGHTS).unwrap(),
521            4096,
522        );
523
524        let mut buf = allocator.allocate_buffer(1024).await;
525        buf.as_mut_ptr_slice().fill(0x77);
526
527        let sub_ref = buf.subslice(100..200);
528        assert_eq!(sub_ref.len(), 100);
529        assert_eq!(sub_ref.to_vec(), vec![0x77; 100]);
530
531        let buf_ref = buf.as_ref();
532        let (left, right) = buf_ref.split_at(512);
533        assert_eq!(left.len(), 512);
534        assert_eq!(right.len(), 512);
535    }
536}