openhcl_dma_manager/
lib.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! This module provides a global DMA manager and client implementation for
5//! OpenHCL. The global manager owns the regions used to allocate DMA buffers
6//! and provides clients with access to these buffers.
7
8#![cfg(target_os = "linux")]
9#![forbid(unsafe_code)]
10
11use anyhow::Context;
12use hcl_mapper::HclMapper;
13use inspect::Inspect;
14use lower_vtl_permissions_guard::LowerVtlMemorySpawner;
15use memory_range::MemoryRange;
16use page_pool_alloc::PagePool;
17use page_pool_alloc::PagePoolAllocator;
18use page_pool_alloc::PagePoolAllocatorSpawner;
19use std::sync::Arc;
20use user_driver::DmaClient;
21use user_driver::lockmem::LockedMemorySpawner;
22
23/// Save restore support for [`OpenhclDmaManager`].
24pub mod save_restore {
25    use super::OpenhclDmaManager;
26    use mesh::payload::Protobuf;
27    use page_pool_alloc::save_restore::PagePoolState;
28    use vmcore::save_restore::RestoreError;
29    use vmcore::save_restore::SaveError;
30    use vmcore::save_restore::SaveRestore;
31
32    /// The saved state for [`OpenhclDmaManager`].
33    #[derive(Protobuf)]
34    #[mesh(package = "openhcl.openhcldmamanager")]
35    pub struct OpenhclDmaManagerState {
36        #[mesh(1)]
37        shared_pool: Option<PagePoolState>,
38        #[mesh(2)]
39        private_pool: Option<PagePoolState>,
40    }
41
42    impl SaveRestore for OpenhclDmaManager {
43        type SavedState = OpenhclDmaManagerState;
44
45        fn save(&mut self) -> Result<Self::SavedState, SaveError> {
46            let shared_pool = self
47                .shared_pool
48                .as_mut()
49                .map(SaveRestore::save)
50                .transpose()
51                .map_err(|e| {
52                    SaveError::ChildError("shared pool save failed".into(), Box::new(e))
53                })?;
54
55            let private_pool = self
56                .private_pool
57                .as_mut()
58                .map(SaveRestore::save)
59                .transpose()
60                .map_err(|e| {
61                    SaveError::ChildError("private pool save failed".into(), Box::new(e))
62                })?;
63
64            Ok(OpenhclDmaManagerState {
65                shared_pool,
66                private_pool,
67            })
68        }
69
70        fn restore(&mut self, state: Self::SavedState) -> Result<(), RestoreError> {
71            match (state.shared_pool, self.shared_pool.as_mut()) {
72                (None, None) => {}
73                (Some(_), None) => {
74                    return Err(RestoreError::InvalidSavedState(anyhow::anyhow!(
75                        "saved state for shared pool but no shared pool"
76                    )));
77                }
78                (None, Some(_)) => {
79                    // It's possible that previously we did not have a shared
80                    // pool, so there may not be any state to restore.
81                }
82                (Some(state), Some(pool)) => {
83                    pool.restore(state).map_err(|e| {
84                        RestoreError::ChildError("shared pool restore failed".into(), Box::new(e))
85                    })?;
86                }
87            }
88
89            match (state.private_pool, self.private_pool.as_mut()) {
90                (None, None) => {}
91                (Some(_), None) => {
92                    return Err(RestoreError::InvalidSavedState(anyhow::anyhow!(
93                        "saved state for private pool but no private pool"
94                    )));
95                }
96                (None, Some(_)) => {
97                    // It's possible that previously we did not have a private
98                    // pool, so there may not be any state to restore.
99                }
100                (Some(state), Some(pool)) => {
101                    pool.restore(state).map_err(|e| {
102                        RestoreError::ChildError("private pool restore failed".into(), Box::new(e))
103                    })?;
104                }
105            }
106
107            Ok(())
108        }
109    }
110}
111
112/// A global DMA manager that owns various pools of memory for managing
113/// buffers and clients using DMA.
114#[derive(Inspect)]
115pub struct OpenhclDmaManager {
116    /// Page pool with pages that are mapped with shared visibility on CVMs.
117    shared_pool: Option<PagePool>,
118    /// Page pool with pages that are mapped with private visibility on CVMs.
119    private_pool: Option<PagePool>,
120    #[inspect(skip)]
121    inner: Arc<DmaManagerInner>,
122}
123
124/// The required VTL permissions on DMA allocations.
125#[derive(Inspect)]
126pub enum LowerVtlPermissionPolicy {
127    /// No specific permission constraints are required.
128    Any,
129    /// All allocations must be accessible to VTL0.
130    Vtl0,
131}
132
133/// The CVM page visibility required for DMA allocations.
134#[derive(Copy, Clone, Inspect)]
135pub enum AllocationVisibility {
136    /// Allocations must be shared with the host (aka host visible).
137    Shared,
138    /// Allocations must be private to the guest (but is allowed to be visible to the VTL0 guest).
139    Private,
140}
141
142/// Client parameters for a new [`OpenhclDmaClient`].
143#[derive(Inspect)]
144pub struct DmaClientParameters {
145    /// The name for this client.
146    pub device_name: String,
147    /// The required VTL permissions on allocations.
148    pub lower_vtl_policy: LowerVtlPermissionPolicy,
149    /// The required CVM page visibility for allocations.
150    pub allocation_visibility: AllocationVisibility,
151    /// Whether allocations should be persistent. Persistent allocations can
152    /// survive save/restore.
153    pub persistent_allocations: bool,
154}
155
156struct DmaManagerInner {
157    shared_spawner: Option<PagePoolAllocatorSpawner>,
158    private_spawner: Option<PagePoolAllocatorSpawner>,
159    lower_vtl: Option<Arc<DmaManagerLowerVtl>>,
160}
161
162/// Used by [`OpenhclDmaManager`] to modify VTL permissions via
163/// [`LowerVtlMemorySpawner`].
164///
165/// This is required due to some users (like the GET or partition struct itself)
166/// that are constructed before the partition struct which normally implements
167/// this trait.
168///
169/// This type should never be created on a hardware isolated VM, as the
170/// hypervisor is untrusted.
171struct DmaManagerLowerVtl {
172    mshv_hvcall: hcl::ioctl::MshvHvcall,
173}
174
175impl DmaManagerLowerVtl {
176    pub fn new() -> anyhow::Result<Arc<Self>> {
177        let mshv_hvcall = hcl::ioctl::MshvHvcall::new().context("failed to open mshv_hvcall")?;
178        mshv_hvcall.set_allowed_hypercalls(&[hvdef::HypercallCode::HvCallModifyVtlProtectionMask]);
179        Ok(Arc::new(Self { mshv_hvcall }))
180    }
181}
182
183impl virt::VtlMemoryProtection for DmaManagerLowerVtl {
184    fn modify_vtl_page_setting(&self, pfn: u64, flags: hvdef::HvMapGpaFlags) -> anyhow::Result<()> {
185        self.mshv_hvcall
186            .modify_vtl_protection_mask(
187                MemoryRange::from_4k_gpn_range(pfn..pfn + 1),
188                flags,
189                hvdef::hypercall::HvInputVtl::CURRENT_VTL,
190            )
191            .context("failed to modify VTL page permissions")
192    }
193}
194
195impl DmaManagerInner {
196    fn new_dma_client(&self, params: DmaClientParameters) -> anyhow::Result<Arc<OpenhclDmaClient>> {
197        // Allocate the inner client that actually performs the allocations.
198        let backing = {
199            let DmaClientParameters {
200                device_name,
201                lower_vtl_policy,
202                allocation_visibility,
203                persistent_allocations,
204            } = &params;
205
206            struct ClientCreation<'a> {
207                allocation_visibility: AllocationVisibility,
208                persistent_allocations: bool,
209                shared_spawner: Option<&'a PagePoolAllocatorSpawner>,
210                private_spawner: Option<&'a PagePoolAllocatorSpawner>,
211            }
212
213            let creation = ClientCreation {
214                allocation_visibility: *allocation_visibility,
215                persistent_allocations: *persistent_allocations,
216                shared_spawner: self.shared_spawner.as_ref(),
217                private_spawner: self.private_spawner.as_ref(),
218            };
219
220            match creation {
221                ClientCreation {
222                    allocation_visibility: AllocationVisibility::Shared,
223                    persistent_allocations: _,
224                    shared_spawner: Some(shared),
225                    private_spawner: _,
226                } => {
227                    // The shared pool is used by default if available, or if
228                    // explicitly requested. All pages are accessible by all
229                    // VTLs, so no modification of VTL permissions are required
230                    // regardless of what the caller has asked for.
231                    DmaClientBacking::SharedPool(
232                        shared
233                            .allocator(device_name.into())
234                            .context("failed to create shared allocator")?,
235                    )
236                }
237                ClientCreation {
238                    allocation_visibility: AllocationVisibility::Shared,
239                    persistent_allocations: _,
240                    shared_spawner: None,
241                    private_spawner: _,
242                } => {
243                    // No sources available that support shared visibility.
244                    anyhow::bail!("no sources available for shared visibility")
245                }
246                ClientCreation {
247                    allocation_visibility: AllocationVisibility::Private,
248                    persistent_allocations: true,
249                    shared_spawner: _,
250                    private_spawner: Some(private),
251                } => match lower_vtl_policy {
252                    LowerVtlPermissionPolicy::Any => {
253                        // Only the private pool supports persistent
254                        // allocations.
255                        DmaClientBacking::PrivatePool(
256                            private
257                                .allocator(device_name.into())
258                                .context("failed to create private allocator")?,
259                        )
260                    }
261                    LowerVtlPermissionPolicy::Vtl0 => {
262                        // Private memory must be wrapped in a lower VTL memory
263                        // spawner, as otherwise it is accessible to VTL2 only.
264                        DmaClientBacking::PrivatePoolLowerVtl(LowerVtlMemorySpawner::new(
265                            private
266                                .allocator(device_name.into())
267                                .context("failed to create private allocator")?,
268                            self.lower_vtl
269                                .as_ref()
270                                .ok_or(anyhow::anyhow!(
271                                    "lower vtl not available on hardware isolated platforms"
272                                ))?
273                                .clone(),
274                        ))
275                    }
276                },
277                ClientCreation {
278                    allocation_visibility: AllocationVisibility::Private,
279                    persistent_allocations: true,
280                    shared_spawner: _,
281                    private_spawner: None,
282                } => {
283                    // No sources available that support private persistence.
284                    anyhow::bail!("no sources available for private persistent allocations")
285                }
286                ClientCreation {
287                    allocation_visibility: AllocationVisibility::Private,
288                    persistent_allocations: false,
289                    shared_spawner: _,
290                    private_spawner: _,
291                } => match lower_vtl_policy {
292                    LowerVtlPermissionPolicy::Any => {
293                        // No persistence needed means the `LockedMemorySpawner`
294                        // using normal VTL2 ram is fine.
295                        DmaClientBacking::LockedMemory(LockedMemorySpawner)
296                    }
297                    LowerVtlPermissionPolicy::Vtl0 => {
298                        // `LockedMemorySpawner` uses private VTL2 ram, so
299                        // lowering VTL permissions is required.
300                        DmaClientBacking::LockedMemoryLowerVtl(LowerVtlMemorySpawner::new(
301                            LockedMemorySpawner,
302                            self.lower_vtl
303                                .as_ref()
304                                .ok_or(anyhow::anyhow!(
305                                    "lower vtl not available on hardware isolated platforms"
306                                ))?
307                                .clone(),
308                        ))
309                    }
310                },
311            }
312        };
313
314        Ok(Arc::new(OpenhclDmaClient { backing, params }))
315    }
316}
317
318impl OpenhclDmaManager {
319    /// Creates a new [`OpenhclDmaManager`] with the given ranges to use for the
320    /// shared and private gpa pools.
321    pub fn new(
322        shared_ranges: &[MemoryRange],
323        private_ranges: &[MemoryRange],
324        vtom: u64,
325        isolation_type: virt::IsolationType,
326    ) -> anyhow::Result<Self> {
327        tracing::info!(
328            ?shared_ranges,
329            ?private_ranges,
330            vtom,
331            ?isolation_type,
332            "create dma manager"
333        );
334
335        let shared_pool = if shared_ranges.is_empty() {
336            None
337        } else {
338            Some(
339                PagePool::new(
340                    shared_ranges,
341                    HclMapper::new_shared(vtom).context("failed to create hcl mapper")?,
342                )
343                .context("failed to create shared page pool")?,
344            )
345        };
346
347        let private_pool = if private_ranges.is_empty() {
348            None
349        } else {
350            Some(
351                PagePool::new(
352                    private_ranges,
353                    HclMapper::new_private().context("failed to create hcl mapper")?,
354                )
355                .context("failed to create private page pool")?,
356            )
357        };
358
359        Ok(OpenhclDmaManager {
360            inner: Arc::new(DmaManagerInner {
361                shared_spawner: shared_pool.as_ref().map(|pool| pool.allocator_spawner()),
362                private_spawner: private_pool.as_ref().map(|pool| pool.allocator_spawner()),
363                lower_vtl: if isolation_type.is_hardware_isolated() {
364                    None
365                } else {
366                    Some(DmaManagerLowerVtl::new().context("failed to create lower vtl")?)
367                },
368            }),
369            shared_pool,
370            private_pool,
371        })
372    }
373
374    /// Creates a new DMA client with the given device name and lower VTL
375    /// policy.
376    pub fn new_client(&self, params: DmaClientParameters) -> anyhow::Result<Arc<OpenhclDmaClient>> {
377        self.inner.new_dma_client(params)
378    }
379
380    /// Returns a [`DmaClientSpawner`] for creating DMA clients.
381    pub fn client_spawner(&self) -> DmaClientSpawner {
382        DmaClientSpawner {
383            inner: self.inner.clone(),
384        }
385    }
386
387    /// Validate restore for the global DMA manager.
388    pub fn validate_restore(&self) -> anyhow::Result<()> {
389        // Finalize restore for any available pools. Do not allow leaking any
390        // allocations.
391        if let Some(shared_pool) = &self.shared_pool {
392            shared_pool
393                .validate_restore(false)
394                .context("failed to validate restore for shared pool")?
395        }
396
397        if let Some(private_pool) = &self.private_pool {
398            private_pool
399                .validate_restore(false)
400                .context("failed to validate restore for private pool")?
401        }
402
403        Ok(())
404    }
405}
406
407/// A spawner for creating DMA clients.
408#[derive(Clone)]
409pub struct DmaClientSpawner {
410    inner: Arc<DmaManagerInner>,
411}
412
413impl DmaClientSpawner {
414    /// Creates a new DMA client with the given parameters.
415    pub fn new_client(&self, params: DmaClientParameters) -> anyhow::Result<Arc<OpenhclDmaClient>> {
416        self.inner.new_dma_client(params)
417    }
418}
419
420/// The backing for allocations for an individual dma client. This is used so
421/// clients can be inspected to see what actually is backing their allocations.
422#[derive(Inspect)]
423#[inspect(tag = "type")]
424enum DmaClientBacking {
425    SharedPool(#[inspect(skip)] PagePoolAllocator),
426    PrivatePool(#[inspect(skip)] PagePoolAllocator),
427    LockedMemory(#[inspect(skip)] LockedMemorySpawner),
428    PrivatePoolLowerVtl(#[inspect(skip)] LowerVtlMemorySpawner<PagePoolAllocator>),
429    LockedMemoryLowerVtl(#[inspect(skip)] LowerVtlMemorySpawner<LockedMemorySpawner>),
430}
431
432impl DmaClientBacking {
433    fn allocate_dma_buffer(
434        &self,
435        total_size: usize,
436    ) -> anyhow::Result<user_driver::memory::MemoryBlock> {
437        match self {
438            DmaClientBacking::SharedPool(allocator) => allocator.allocate_dma_buffer(total_size),
439            DmaClientBacking::PrivatePool(allocator) => allocator.allocate_dma_buffer(total_size),
440            DmaClientBacking::LockedMemory(spawner) => spawner.allocate_dma_buffer(total_size),
441            DmaClientBacking::PrivatePoolLowerVtl(spawner) => {
442                spawner.allocate_dma_buffer(total_size)
443            }
444            DmaClientBacking::LockedMemoryLowerVtl(spawner) => {
445                spawner.allocate_dma_buffer(total_size)
446            }
447        }
448    }
449
450    fn attach_pending_buffers(&self) -> anyhow::Result<Vec<user_driver::memory::MemoryBlock>> {
451        match self {
452            DmaClientBacking::SharedPool(allocator) => allocator.attach_pending_buffers(),
453            DmaClientBacking::PrivatePool(allocator) => allocator.attach_pending_buffers(),
454            DmaClientBacking::LockedMemory(_) => {
455                anyhow::bail!(
456                    "attaching pending buffers is not supported with locked memory; \
457                    this client type does not maintain a pool of pending allocations. \
458                    To use attach_pending_buffers, create a client backed by a shared or private pool."
459                )
460            }
461            DmaClientBacking::PrivatePoolLowerVtl(spawner) => spawner.attach_pending_buffers(),
462            DmaClientBacking::LockedMemoryLowerVtl(spawner) => spawner.attach_pending_buffers(),
463        }
464    }
465}
466
467/// An OpenHCL dma client. This client implements inspect to allow seeing what
468/// policy and backing is used for this client.
469#[derive(Inspect)]
470pub struct OpenhclDmaClient {
471    backing: DmaClientBacking,
472    params: DmaClientParameters,
473}
474
475impl DmaClient for OpenhclDmaClient {
476    fn allocate_dma_buffer(
477        &self,
478        total_size: usize,
479    ) -> anyhow::Result<user_driver::memory::MemoryBlock> {
480        self.backing.allocate_dma_buffer(total_size)
481    }
482
483    fn attach_pending_buffers(&self) -> anyhow::Result<Vec<user_driver::memory::MemoryBlock>> {
484        self.backing.attach_pending_buffers()
485    }
486}