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