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