Skip to main content

memory_pinning/
shadow_process.rs

1// Copyright 2025 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 fidl::HandleBased;
6use fidl_fuchsia_scheduler::{
7    RoleManagerMarker, RoleManagerSetRoleRequest, RoleManagerSynchronousProxy, RoleName, RoleTarget,
8};
9use starnix_logging::log_warn;
10use starnix_uapi::errors::Errno;
11use starnix_uapi::from_status_like_fdio;
12use std::sync::{Arc, Weak};
13
14/// A high-priority memory profile that (as of writing) disables reclamation for any mappings it
15/// contains.
16const MEMORY_ROLE: &str = "fuchsia.starnix.pinned_memory";
17
18/// Provides a hack for truly pinning memory in the absence of partial VMAR profiles or an
19/// analogous feature (https://fxbug.dev/446265172). The pins produced by this type will stay
20/// pinned even under critical memory pressure levels and should be used with extreme care.
21///
22/// Requires access to the `fuchsia.scheduler.RoleManager` protocol capability to actually pin
23/// memory.
24#[derive(Debug)]
25pub struct ShadowProcess {
26    // Keep the process alive but we're never going to start it.
27    process: zx::Process,
28    vmar: Arc<zx::Vmar>,
29}
30
31impl ShadowProcess {
32    /// Create a new shadow process for pinning memory. Connects to `fuchsia.scheduler.RoleManager`
33    /// in the process' namespace.
34    pub fn new(name: zx::Name) -> Result<Self, zx::Status> {
35        let role_manager =
36            fuchsia_component::client::connect_to_protocol_sync::<RoleManagerMarker>()
37                .expect("this can only fail if a process' namespace is broken");
38        Self::from_role_manager(name, role_manager)
39    }
40
41    fn from_role_manager(
42        name: zx::Name,
43        role_manager: RoleManagerSynchronousProxy,
44    ) -> Result<Self, zx::Status> {
45        let (process, vmar) =
46            zx::Process::create(&fuchsia_runtime::job_default(), name, Default::default())?;
47        let vmar_dupe = vmar.duplicate_handle(zx::Rights::SAME_RIGHTS)?;
48        if let Err(e) = role_manager.set_role(
49            RoleManagerSetRoleRequest {
50                target: Some(RoleTarget::Vmar(vmar_dupe)),
51                role: Some(RoleName { role: MEMORY_ROLE.to_string() }),
52                ..Default::default()
53            },
54            zx::MonotonicInstant::INFINITE,
55        ) {
56            log_warn!(e:%, name:%; "Unable to set role for memory pin shadow process' vmar.");
57        }
58
59        Ok(Self { process, vmar: Arc::new(vmar) })
60    }
61
62    /// Pin the provided range of the provided VMO to ensure those pages stay resident under
63    /// memory pressure.
64    pub fn pin_pages(
65        &self,
66        vmo: &zx::Vmo,
67        offset: u64,
68        length: usize,
69    ) -> Result<Arc<PinnedMapping>, Errno> {
70        let base = self
71            .vmar
72            .map(0, vmo, offset, length, zx::VmarFlags::PERM_READ)
73            .map_err(|e| from_status_like_fdio!(e))?;
74        Ok(Arc::new(PinnedMapping { vmar: Arc::downgrade(&self.vmar), base, length }))
75    }
76
77    /// Return a handle to the VMAR where all mappings are pinned.
78    pub fn vmar(&self) -> Arc<zx::Vmar> {
79        self.vmar.clone()
80    }
81}
82
83impl Drop for ShadowProcess {
84    fn drop(&mut self) {
85        use zx::Task;
86
87        // Ensure the process exits so that it doesn't confuse the kernel's shutdown logic.
88        self.process.kill().expect("must be able to kill process we created");
89    }
90}
91
92/// A token for a region of pinned memory. Will unpin the memory when dropped.
93#[derive(Clone, Debug)]
94pub struct PinnedMapping {
95    vmar: Weak<zx::Vmar>,
96    base: usize,
97    length: usize,
98}
99
100impl Drop for PinnedMapping {
101    fn drop(&mut self) {
102        if let Some(vmar) = self.vmar.upgrade() {
103            // SAFETY: this address is not observable outside this module and it is just a key into
104            // the high priority VMAR for this module's purposes. No pointers or references have
105            // been created pointing into this mapping which makes it sound to unmap.
106            if let Err(e) = unsafe { vmar.unmap(self.base, self.length) } {
107                log_warn!(e:%; "Failed to unmap mlock() pin mapping.");
108            }
109        }
110    }
111}
112
113impl std::cmp::PartialEq for PinnedMapping {
114    fn eq(&self, rhs: &Self) -> bool {
115        Weak::ptr_eq(&self.vmar, &rhs.vmar) && self.base == rhs.base && self.length == rhs.length
116    }
117}
118impl std::cmp::Eq for PinnedMapping {}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use fidl_fuchsia_scheduler::{RoleManagerRequest, RoleManagerSetRoleResponse};
124    use futures::StreamExt;
125
126    #[fuchsia::test]
127    fn create_without_role_manager_succeeds() {
128        // There's no RoleManager available in the unit test environment.
129        let _shadow_process = ShadowProcess::new(zx::Name::new_lossy("noop")).unwrap();
130    }
131
132    #[fuchsia::test]
133    async fn creation_sets_role() {
134        let (role_manager_client, mut role_manager_server) =
135            fidl::endpoints::create_sync_proxy_and_stream::<RoleManagerMarker>();
136
137        // Creating a ShadowProcess blocks the calling thread until the role manager replies, spawn
138        // a separate thread.
139        let (send_vmar_koid, recv_vmar_koid) = futures::channel::oneshot::channel();
140        std::thread::spawn(move || {
141            let shadow_process = ShadowProcess::from_role_manager(
142                zx::Name::new_lossy("role_manager_test"),
143                role_manager_client,
144            )
145            .unwrap();
146            send_vmar_koid.send(shadow_process.vmar.koid().unwrap()).unwrap();
147        });
148
149        match role_manager_server.next().await.unwrap().unwrap() {
150            RoleManagerRequest::SetRole { payload, responder } => {
151                responder.send(Ok(RoleManagerSetRoleResponse::default())).unwrap();
152                let shadow_vmar_koid: zx::Koid = recv_vmar_koid.await.unwrap();
153
154                let received_vmar_koid = match &payload.target {
155                    Some(RoleTarget::Vmar(vmar)) => vmar.koid().unwrap(),
156                    other => panic!("unexpected SetRole target {other:#?}"),
157                };
158                assert_eq!(shadow_vmar_koid, received_vmar_koid);
159                assert_eq!(payload.role, Some(RoleName { role: MEMORY_ROLE.to_string() }),);
160            }
161            other => panic!("unexpected SetRole request {other:?}"),
162        }
163    }
164
165    #[fuchsia::test]
166    fn vmo_is_mapped_in_shadow_vmar() {
167        let shadow_process = ShadowProcess::new(zx::Name::new_lossy("vmo_mapping_test")).unwrap();
168
169        let get_shadow_mappings = || {
170            shadow_process
171                .vmar
172                .maps_vec()
173                .unwrap()
174                .into_iter()
175                .filter_map(|info| info.details().as_mapping().map(ToOwned::to_owned))
176                .collect::<Vec<_>>()
177        };
178        assert_eq!(get_shadow_mappings(), &[], "VMAR should be empty before any pinning");
179
180        // Initialize a VMO and populate its pages
181        let to_map = zx::Vmo::create(8192).unwrap();
182        to_map.write(&[1u8; 8192][..], 0).unwrap();
183
184        // Pin the VMO pages
185        let pinned_mapping = shadow_process.pin_pages(&to_map, 0, 8192).unwrap();
186        let mappings_after_pinning = get_shadow_mappings();
187        assert_eq!(mappings_after_pinning.len(), 1, "there should only be one mapping in VMAR");
188
189        // Check the mapping in the shadow process' VMAR. It should be a read-only mapping and
190        // be fully committed & populated.
191        let pinned_mapping_info = &mappings_after_pinning[0];
192        assert_eq!(pinned_mapping_info.mmu_flags, zx::VmarFlagsExtended::PERM_READ);
193        assert_eq!(pinned_mapping_info.vmo_koid, to_map.koid().unwrap());
194        assert_eq!(pinned_mapping_info.vmo_offset, 0);
195        assert_eq!(pinned_mapping_info.committed_bytes, 8192);
196        assert_eq!(pinned_mapping_info.populated_bytes, 8192);
197
198        drop(pinned_mapping);
199        assert_eq!(get_shadow_mappings(), &[], "dropping PinnedMap must clean up");
200    }
201}