Skip to main content

fuchsia_component_escrow/
lib.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
5// This library is only usable at API versions from which escrow is supported.
6
7#[cfg(fuchsia_api_level_at_least = "HEAD")]
8pub use everything::*;
9
10#[cfg(fuchsia_api_level_less_than = "HEAD")]
11mod everything {
12    use anyhow as _;
13    use fidl as _;
14    use fidl_fuchsia_component_sandbox as _;
15    use fidl_fuchsia_io as _;
16    use fidl_fuchsia_process_lifecycle as _;
17    use fuchsia_async as _;
18    use fuchsia_component_runtime as _;
19    use fuchsia_runtime as _;
20    use fuchsia_sync as _;
21    use futures as _;
22}
23
24#[cfg(fuchsia_api_level_at_least = "HEAD")]
25mod everything {
26    use anyhow::{Error, format_err};
27    use fidl::endpoints::ServerEnd;
28    use fidl_fuchsia_component_sandbox as fsandbox;
29    use fidl_fuchsia_io as fio;
30    use fidl_fuchsia_process_lifecycle as flifecycle;
31    use fuchsia_async::Task;
32    use fuchsia_component_runtime::Dictionary;
33    use fuchsia_runtime::{self as fruntime, HandleInfo, HandleType};
34    use fuchsia_sync::Mutex;
35    use futures::StreamExt;
36    use std::collections::HashSet;
37    use std::process;
38    use std::sync::Arc;
39
40    /// Holds the process lifecycle channel (or optionally just the control side of it), and uses
41    /// it to send escrow data and handles to component manager. This is a required step for a
42    /// component to gracefully shut itself down when it would otherwise sit idle, in order to save
43    /// system resources.
44    #[derive(Default, Clone)]
45    pub struct EscrowOperation {
46        inner: Arc<Mutex<Inner>>,
47    }
48
49    #[derive(Default)]
50    struct Inner {
51        fsandbox_dictionary: Option<fsandbox::DictionaryRef>,
52        lifecycle_control_handle: Option<flifecycle::LifecycleControlHandle>,
53        lifecycle_handle: Option<ServerEnd<flifecycle::LifecycleMarker>>,
54        watch_for_stop_task: Option<Task<()>>,
55        dictionary: Option<Dictionary>,
56    }
57
58    impl EscrowOperation {
59        /// Creates a new EscrowOperation by taking the process lifecycle handle. Does not actually
60        /// perform the escrow operation, see `Self::run`.
61        pub fn new() -> Self {
62            let lifecycle_handle =
63                fruntime::take_startup_handle(HandleInfo::new(HandleType::Lifecycle, 0))
64                    .expect("No lifecycle channel received, unable to escrow handles");
65            let channel: fidl::Channel = lifecycle_handle.into();
66            let lifecycle_handle: ServerEnd<flifecycle::LifecycleMarker> = channel.into();
67            Self::new_with_lifecycle_handle(lifecycle_handle)
68        }
69
70        /// Creates a new EscrowOperation with the provided process lifecycle handle. Does not
71        /// actually perform the escrow operation, see `Self::run`.
72        pub fn new_with_lifecycle_handle(
73            lifecycle_handle: ServerEnd<flifecycle::LifecycleMarker>,
74        ) -> Self {
75            Self {
76                inner: Arc::new(Mutex::new(Inner {
77                    lifecycle_handle: Some(lifecycle_handle),
78                    ..Default::default()
79                })),
80            }
81        }
82
83        /// Creates a new EscrowOperation with the provided process lifecycle control handle. Does
84        /// not actually perform the escrow operation, see `Self::run`.
85        pub fn new_with_control_handle(control_handle: flifecycle::LifecycleControlHandle) -> Self {
86            Self {
87                inner: Arc::new(Mutex::new(Inner {
88                    lifecycle_control_handle: Some(control_handle),
89                    ..Default::default()
90                })),
91            }
92        }
93
94        /// Adds a dictionary handle to the escrow operation, which will be returned to us when
95        /// if/when we are restarted.
96        ///
97        /// Deprecated, prefer `with_dictionary`.
98        pub fn with_fsandbox_dictionary(&self, fsandbox_dictionary: fsandbox::DictionaryRef) {
99            self.inner.lock().fsandbox_dictionary = Some(fsandbox_dictionary);
100        }
101
102        /// Adds a dictionary handle to the escrow operation, which will be returned to us when
103        /// if/when we are restarted.
104        pub fn with_dictionary(&self, dictionary: Dictionary) {
105            self.inner.lock().dictionary = Some(dictionary);
106        }
107
108        /// Starts a new async task that watches the process lifecycle handle for a stop
109        /// instruction, and exits this process if/when such a signal is received.
110        pub fn watch_for_stop(&self) -> Result<(), Error> {
111            let mut inner_guard = self.inner.lock();
112            let lifecycle_handle = inner_guard.lifecycle_handle.take().ok_or_else(|| {
113                format_err!(
114                    "EscrowOperation::wait_for_stop called without setting lifecycle handle"
115                )
116            })?;
117            let (mut stream, control_handle) = lifecycle_handle.into_stream_and_control_handle();
118            inner_guard.lifecycle_control_handle = Some(control_handle);
119            inner_guard.watch_for_stop_task = Some(Task::spawn(async move {
120                let Some(Ok(request)) = stream.next().await else {
121                    return;
122                };
123                match request {
124                    flifecycle::LifecycleRequest::Stop { .. } => {
125                        // Component manager will never initiate escrow operations. If we're being
126                        // asked to stop, then it's not expected of us to escrow our handles. Had
127                        // we not asked for our process lifecycle handle then at this point we'd
128                        // have our process unceremoniously stopped, so it's fine to do the same
129                        // thing manually.
130                        //
131                        // We use exit code 1 because this function is only used by components that
132                        // would not otherwise have a process lifecycle handle were it not a
133                        // requirement for escrowing their state, and if they didn't have the
134                        // handle they'd be stopped by the ELF runner terminating their process,
135                        // which results in a non-zero exit code.
136                        process::exit(1);
137                    }
138                }
139            }));
140            Ok(())
141        }
142
143        /// Runs this escrow operation, sending the outgoing directory and potentially a dictionary
144        /// handle to component manager. The component should exit immediately after calling this
145        /// function.
146        pub fn run(
147            &self,
148            outgoing_directory: ServerEnd<fio::DirectoryMarker>,
149        ) -> Result<(), Error> {
150            let mut inner_guard = self.inner.lock();
151            let lifecycle_control_handle = match inner_guard.lifecycle_control_handle.take() {
152                Some(lifecycle_control_handle) => lifecycle_control_handle,
153                None => {
154                    let lifecycle_handle = inner_guard
155                        .lifecycle_handle
156                        .take()
157                        .ok_or_else(|| format_err!("EscrowOperation::run called without setting lifecycle handle or control handle"))?;
158                    let (_stream, control) = lifecycle_handle.into_stream_and_control_handle();
159                    control
160                }
161            };
162
163            lifecycle_control_handle
164                .send_on_escrow(flifecycle::LifecycleOnEscrowRequest {
165                    outgoing_dir: Some(outgoing_directory),
166                    escrowed_dictionary: inner_guard.fsandbox_dictionary.take(),
167                    escrowed_dictionary_handle: inner_guard.dictionary.take().map(|d| d.handle),
168                    recoverable_bytes: Some(calculate_recoverable_memory()?),
169                    ..Default::default()
170                })
171                .map_err(|e| format_err!("Failed to escrow handles: {e:?}"))
172        }
173    }
174
175    /// Returns the number of bytes of memory that we are sure will be reclaimed when this process
176    /// exits.
177    fn calculate_recoverable_memory() -> Result<u64, Error> {
178        // To calculate how much memory will be saved when our component is escrowed, we look
179        // at the VMOs that we have handles to. For every process there are some VMOs that are
180        // both unable to be paged out, and not shared with any other processes, and will thus
181        // be closed when we exit. For example, any scudo and relro VMOs. It's possible that we
182        // have handles to other VMOs that are also unpageable and that aren't shared with
183        // other components, but it's harder to make generalized assumptions about such VMOs,
184        // so we only count VMOs we recognize here to get a confident floor on savings. This
185        // calculation also ignores potential savings from kernel page compression.
186        let process = fruntime::process_self();
187        let vmo_infos = process
188            .info_vmos_vec()
189            .map_err(|e| format_err!("Failed to inspect own VMOs: {e:?}"))?;
190        let vmos_infos_to_include = vmo_infos.into_iter().filter(|vmo_info| {
191            vmo_info.name.as_bstr().starts_with(b"scudo")
192                || vmo_info.name.as_bstr().starts_with(b"relro")
193                || vmo_info.name.as_bstr().starts_with(b"pthread_t")
194                || vmo_info.name.as_bstr().starts_with(b"pthread_create")
195                || vmo_info.name.as_bstr().starts_with(b"thrd_t:0x")
196                || vmo_info.name.as_bstr() == "initial-thread"
197        });
198        // We collect savings by koid because it's possible that the vmo_infos vector could
199        // have duplicates.
200        let savings_by_koid = vmos_infos_to_include
201            .map(|vmo_info| (vmo_info.koid, vmo_info.committed_private_bytes))
202            .collect::<HashSet<_>>();
203        let total_recoverable_bytes = savings_by_koid.into_iter().map(|(_, num)| num).sum();
204        Ok(total_recoverable_bytes)
205    }
206}