Skip to main content

system_update_committer/
main.rs

1// Copyright 2020 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#![allow(clippy::let_unit_value)]
6
7use ::fidl::endpoints::RequestStream as _;
8use anyhow::{Context as _, Error, anyhow};
9use fidl_fuchsia_component_sandbox as fsandbox;
10use fidl_fuchsia_io as fio;
11use fidl_fuchsia_paver as fpaver;
12use fidl_fuchsia_process_lifecycle as flifecycle;
13use fidl_fuchsia_update as fupdate;
14use fidl_fuchsia_update_verify::HealthVerificationMarker;
15use fuchsia_async as fasync;
16use fuchsia_component::client::connect_to_protocol;
17use fuchsia_component::escrow::EscrowOperation;
18use fuchsia_component::server::ServiceFs;
19use fuchsia_inspect::health::Reporter as _;
20use fuchsia_inspect::{self as finspect};
21use futures::channel::oneshot;
22use futures::future::{FutureExt as _, TryFutureExt as _};
23use futures::stream::StreamExt as _;
24use log::{error, info, warn};
25use std::sync::Arc;
26
27mod fidl;
28mod metadata;
29mod reboot;
30
31// The feedback component persists the reboot reason. It obtains the reboot reason by registering a
32// watcher with the power manager, so we delay reboot slightly to give the feedback component a
33// chance to obtain and persist the reboot reason.
34const MINIMUM_REBOOT_WAIT: std::time::Duration = std::time::Duration::from_secs(5);
35
36#[fuchsia::main(logging_tags = ["system-update-committer"])]
37pub fn main() -> Result<(), Error> {
38    info!("starting system-update-committer");
39
40    let mut executor = fasync::LocalExecutorBuilder::new().build();
41    let () = executor.run_singlethreaded(main_async()).map_err(|err| {
42        // Use anyhow to print the error chain.
43        let err = anyhow!(err);
44        error!("error running system-update-committer: {:#}", err);
45        err
46    })?;
47
48    info!("stopping system-update-committer");
49    Ok(())
50}
51
52async fn main_async() -> Result<(), Error> {
53    match fuchsia_runtime::take_startup_handle(fuchsia_runtime::HandleInfo::new(
54        fuchsia_runtime::HandleType::EscrowedDictionary,
55        0,
56    )) {
57        Some(dictionary) => {
58            resume_from_escrow(fsandbox::DictionaryRef { token: dictionary.into() })
59                .await
60                .context("resume_from_idle_stop")
61        }
62        None => fresh_run().await.context("first_run"),
63    }
64}
65
66struct EscrowState {
67    // Keep the internal pair alive so clients can't observe EVENTPAIR_PEER_CLOSED.
68    p_internal: ::fidl::NullableHandle,
69    p_external: ::fidl::NullableHandle,
70    // Inspect fails open so in rare cases it may not be available.
71    frozen_inspect: Option<::fidl::NullableHandle>,
72}
73
74// It should not be possible for this function to be called more than once per boot in prod because:
75//   1. it is only called when the component is not supplied with escrowed state
76//   2. CM guarantees that escrowed state will always be supplied when restarting a component
77//   3. the component always* escrows state when exiting cleanly
78//   4. the component is `on_terminate: "reboot"` and so unclean exits should trigger reboot
79//
80//   * If the component receives a fuchsia.process.lifecycle/LifeCycle.Stop message it will exit
81//     cleanly without escrowing state (see https://fxbug.dev/332341289), but because the component
82//     is not a dynamic component this should only be possible by manually destroying it (e.g. via
83//     `ffx component stop ...`), which never occurs in prod [0].
84//
85// Regardless, it should be safe to call this function multiple times per boot because if the
86// component has exited cleanly then commit must not be necessary (either it was already unnecessary
87// when it was called the first time or the first call successfully committed) and calling this
88// function when commit is not necessary is safe.
89//
90// Returns success if the component is idle or asked to stop.
91//
92// [0] https://fuchsia.dev/fuchsia-src/contribute/governance/rfcs/0110_reboot_for_critical_components?hl=en#detecting_termination_of_reboot-on-terminate_components
93async fn fresh_run() -> Result<(), Error> {
94    let reboot_deadline = std::time::Instant::now() + MINIMUM_REBOOT_WAIT;
95
96    let inspector = finspect::Inspector::default();
97    let inspect_controller =
98        inspect_runtime::publish(&inspector, inspect_runtime::PublishOptions::default());
99
100    let config = system_update_committer_config::Config::take_from_startup_handle();
101    let idle_timeout = if config.stop_on_idle_timeout_millis >= 0 {
102        zx::MonotonicDuration::from_millis(config.stop_on_idle_timeout_millis)
103    } else {
104        zx::MonotonicDuration::INFINITE
105    };
106    let commit_timeout = if config.commit_timeout_seconds >= 0 {
107        zx::MonotonicDuration::from_seconds(config.commit_timeout_seconds)
108    } else {
109        zx::MonotonicDuration::INFINITE
110    };
111    inspector
112        .root()
113        .record_child("structured_config", |config_node| config.record_inspect(config_node));
114
115    let verification_node = inspector.root().create_child("verification");
116    let commit_node = metadata::CommitInspect::new(inspector.root().create_child("commit"));
117    let mut health_node = finspect::health::Node::new(inspector.root());
118    let verification_node_ref = &verification_node;
119    let commit_node_ref = &commit_node;
120    let health_node_ref = &mut health_node;
121
122    let paver =
123        connect_to_protocol::<fpaver::PaverMarker>().context("while connecting to paver")?;
124    let (boot_manager, boot_manager_server_end) = ::fidl::endpoints::create_proxy();
125    paver
126        .find_boot_manager(boot_manager_server_end)
127        .context("transport error while calling find_boot_manager()")?;
128    let reboot_proxy =
129        connect_to_protocol::<fidl_fuchsia_hardware_power_statecontrol::AdminMarker>()
130            .context("while connecting to power state control")?;
131
132    let health_verification = connect_to_protocol::<HealthVerificationMarker>()
133        .context("while connecting to health verification")?;
134
135    let (p_internal, p_external) = zx::EventPair::create();
136    let p_internal_clone =
137        p_internal.duplicate_handle(zx::Rights::BASIC).context("while duplicating p_internal")?;
138
139    let (unblocker, blocker) = oneshot::channel();
140
141    // Handle putting boot metadata in happy state, rebooting on failure (if necessary), and
142    // reporting health to the inspect health node.
143    let commit_fut = async move {
144        match metadata::put_metadata_in_happy_state(
145            &boot_manager,
146            &p_internal,
147            unblocker,
148            &health_verification,
149            commit_timeout,
150            verification_node_ref,
151            commit_node_ref,
152        )
153        .await
154        {
155            Err(e) => {
156                let msg = format!(
157                    "Failed to commit system. Rebooting at {:?} given error {:#} and {:?}",
158                    reboot_deadline,
159                    anyhow!(e),
160                    config
161                );
162                health_node_ref.set_unhealthy(&msg);
163                warn!("{msg}");
164                reboot::wait_and_reboot(fasync::Timer::new(reboot_deadline), &reboot_proxy).await;
165            }
166            Ok(commit_result) => {
167                info!("{}", commit_result.log_msg());
168                health_node_ref.set_ok();
169            }
170        }
171    }
172    .fuse();
173    let mut commit_fut = std::pin::pin!(commit_fut);
174
175    let p_external_clone =
176        p_external.duplicate_handle(zx::Rights::BASIC).context("while duplicating p_external")?;
177    let fidl_server = Arc::new(fidl::FuchsiaUpdateFidlServer::new(
178        p_external_clone,
179        blocker.map_err(|e| e.to_string()),
180        idle_timeout,
181    ));
182    let mut fs = ServiceFs::new_local();
183    fs.take_and_serve_directory_handle()
184        .context("while taking directory handle")?
185        .dir("svc")
186        .add_fidl_service(Services::CommitStatusProvider);
187    let fs = fs.until_stalled(idle_timeout);
188    let active_guard = fs.try_active_guard().unwrap();
189
190    let mut service_fut = async move {
191        let out_dir = fuchsia_sync::Mutex::new(None);
192        let () = fs
193            .for_each_concurrent(None, |item| async {
194                use fuchsia_component::server::Item;
195                match item {
196                    Item::Request(Services::CommitStatusProvider(stream), _active_guard) => {
197                        let () = fidl_server
198                            .clone()
199                            .handle_commit_status_provider_stream(stream)
200                            .await
201                            .unwrap_or_else(|e| {
202                                warn!("handling CommitStatusProviderStream {e:#}");
203                            });
204                    }
205                    Item::Stalled(outgoing_dir) => {
206                        *out_dir.lock() = Some(outgoing_dir);
207                    }
208                }
209            })
210            .await;
211        let out_dir = out_dir
212            .lock()
213            .take()
214            .expect("StallableServiceFs should return the out dir before ending");
215        Ok({
216            let frozen_inspect = if let Some(inspect_controller) = inspect_controller {
217                Some(
218                    inspect_controller
219                        .escrow_frozen(inspect_runtime::EscrowOptions::default())
220                        .await
221                        .context("freezing inspect")?
222                        .token
223                        .into_handle(),
224                )
225            } else {
226                None
227            };
228            (
229                EscrowState {
230                    p_internal: p_internal_clone.into(),
231                    p_external: p_external.into(),
232                    frozen_inspect,
233                },
234                out_dir.into(),
235            )
236        })
237    }
238    .boxed_local()
239    .fuse();
240    let service_fut = futures::select! {
241        () = commit_fut => {
242            // Keep serving the out dir until the device is committed to avoid the confusing
243            // situation in which the component is running but new clients are being ignored.
244            drop(active_guard);
245            service_fut
246        },
247        _ = service_fut => {
248            panic!("fidl service fut completed before commit fut. this should be impossible \
249            because of the active guard");
250        }
251    };
252
253    run_until_idle_or_component_stopped(service_fut).await
254}
255
256// Returns success if the component is idle or asked to stop.
257async fn resume_from_escrow(escrowed_state: fsandbox::DictionaryRef) -> Result<(), Error> {
258    let EscrowState { p_internal, p_external, frozen_inspect } =
259        EscrowState::load(escrowed_state).await.context("loading escrowed state")?;
260
261    let config = system_update_committer_config::Config::take_from_startup_handle();
262    let idle_timeout = if config.stop_on_idle_timeout_millis >= 0 {
263        zx::MonotonicDuration::from_millis(config.stop_on_idle_timeout_millis)
264    } else {
265        zx::MonotonicDuration::INFINITE
266    };
267
268    let p_external_clone =
269        p_external.duplicate_handle(zx::Rights::BASIC).context("while duplicating p_external")?;
270    let fidl_server = Arc::new(fidl::FuchsiaUpdateFidlServer::new(
271        p_external_clone.into(),
272        futures::future::ready(Ok(())),
273        idle_timeout,
274    ));
275    let mut fs = ServiceFs::new_local();
276    fs.take_and_serve_directory_handle()
277        .context("while taking directory handle")?
278        .dir("svc")
279        .add_fidl_service(Services::CommitStatusProvider);
280    let fs = fs.until_stalled(idle_timeout);
281
282    let service_fut = async move {
283        let out_dir = fuchsia_sync::Mutex::new(None);
284        let () = fs
285            .for_each_concurrent(None, |item| async {
286                use fuchsia_component::server::Item;
287                match item {
288                    Item::Request(Services::CommitStatusProvider(stream), _active_guard) => {
289                        let () = fidl_server
290                            .clone()
291                            .handle_commit_status_provider_stream(stream)
292                            .await
293                            .unwrap_or_else(|e| {
294                                warn!("handling CommitStatusProviderStream {e:#}");
295                            });
296                    }
297                    Item::Stalled(outgoing_dir) => {
298                        *out_dir.lock() = Some(outgoing_dir);
299                    }
300                }
301            })
302            .await;
303        let out_dir = out_dir
304            .lock()
305            .take()
306            .expect("StallableServiceFs should return the out dir before ending");
307        Ok((EscrowState { p_internal, p_external, frozen_inspect }, out_dir.into()))
308    }
309    .boxed_local()
310    .fuse();
311    run_until_idle_or_component_stopped(service_fut).await
312}
313
314// Runs service_fut until it completes or a Lifecycle.Stop message is received, then escrows state
315// if possible.
316#[allow(clippy::type_complexity)]
317async fn run_until_idle_or_component_stopped(
318    mut service_fut: futures::future::Fuse<
319        futures::future::LocalBoxFuture<
320            '_,
321            Result<(EscrowState, ::fidl::endpoints::ServerEnd<fio::DirectoryMarker>), Error>,
322        >,
323    >,
324) -> Result<(), Error> {
325    // We have ignored fuchsia.process/Lifecycle.Stop messages until now so that `fresh_run` can try
326    // to commit the system without interruption.
327    // Components have 5 seconds [0] to respond to Stop messages before being terminated by CM. We
328    // use the entire period to maximize the chance we commit the system if it is pending and passes
329    // the checks to avoid unnecessarily spending one of the seven boot attempts before rollback.
330    // [0] https://cs.opensource.google/fuchsia/fuchsia/+/main:src/sys/component_manager/src/model/environment.rs;l=31;drc=2f83da829133fd5432e7d9a3aeb4f46750f8572e
331    let lifecycle = fuchsia_runtime::take_startup_handle(fuchsia_runtime::HandleInfo::new(
332        fuchsia_runtime::HandleType::Lifecycle,
333        0,
334    ))
335    .context("taking lifecycle handle")?;
336    let lifecycle: ::fidl::endpoints::ServerEnd<flifecycle::LifecycleMarker> = lifecycle.into();
337    let (mut lifecycle_request_stream, lifecycle_controller) =
338        lifecycle.into_stream_and_control_handle();
339    let escrow_operation = EscrowOperation::new_with_control_handle(lifecycle_controller);
340
341    futures::select! {
342        res = service_fut => {
343            let (state, out_dir) = res?;
344            let escrowed_dictionary = state.store().await.context("escrowing state")?;
345            escrow_operation.with_fsandbox_dictionary(escrowed_dictionary);
346            escrow_operation.run(out_dir).context("failed to run escrow operation")?;
347            Ok(())
348        },
349        req = lifecycle_request_stream.next() => {
350            match req.ok_or_else(|| anyhow::anyhow!("LifecycleRequest stream closed unexpectedly"))?
351            .context("error reading from LifecycleRequest stream")?
352                {
353                flifecycle::LifecycleRequest::Stop{ control_handle} => {
354                    // TODO(https://fxbug.dev/332341289) Exit cleanly by escrowing state including
355                    // any client connections.
356                    info!(
357                        "received flifecycle::LifecycleRequest::Stop. Any client connections will \
358                         be closed. This should only happen during shutdown."
359                    );
360                    // The shutdown request is acknowledged by closing the lifecycle channel which
361                    // causes CM to kill the component.
362                    // Intentionally leak the channel (which is held by `escrow_operation`) so that
363                    // it will be closed when the OS cleans up the process, allowing the rest of
364                    // the component's own cleanup to occur.
365
366                    // Drop these so the Arc<Channel> in the request stream can be unwrapped.
367                    drop((control_handle, escrow_operation));
368                    // Leak the internal channel instead of the RequestStream because the Fuchsia
369                    // executor will panic if it is dropped while registered receivers are still
370                    // alive.
371                    let (inner, _terminated): (_, bool) = lifecycle_request_stream.into_inner();
372                    let inner = std::sync::Arc::try_unwrap(inner).map_err(
373                        |_: std::sync::Arc<_>| {
374                            anyhow::anyhow!("failed to extract lifecycle channel from Arc")
375                        },
376                    )?;
377                    let inner: zx::Channel = inner.into_channel().into_zx_channel();
378                    std::mem::forget(inner);
379                    Ok(())
380                }
381            }
382        }
383    }
384}
385
386impl EscrowState {
387    const INTERNAL_EVENTPAIR: &'static str = "p_internal";
388    const EXTERNAL_EVENTPAIR: &'static str = "p_external";
389    const INSPECT: &'static str = "frozen_inspect";
390
391    async fn load(dict: fsandbox::DictionaryRef) -> Result<Self, Error> {
392        let store =
393            fuchsia_component::client::connect_to_protocol::<fsandbox::CapabilityStoreMarker>()?;
394        let id_generator = sandbox::CapabilityIdGenerator::new();
395
396        let dict_id = id_generator.next();
397        let () = store
398            .import(dict_id, fsandbox::Capability::Dictionary(dict))
399            .await?
400            .map_err(|e| anyhow!("{e:?}"))?;
401
402        let remove_from_dict = |key: &'static str| async {
403            let id = id_generator.next();
404            match store
405                .dictionary_remove(dict_id, key, Some(&fsandbox::WrappedNewCapabilityId { id }))
406                .await?
407            {
408                Ok(()) => {
409                    let fsandbox::Capability::Handle(handle) =
410                        store.export(id).await?.map_err(|e| anyhow!("{e:?}"))?
411                    else {
412                        anyhow::bail!("Bad capability type from dictionary");
413                    };
414                    Ok(Some(handle))
415                }
416                Err(fsandbox::CapabilityStoreError::ItemNotFound) => Ok(None),
417                Err(e) => {
418                    anyhow::bail!("exporting frozen inspect {e:?}");
419                }
420            }
421        };
422
423        let p_internal = remove_from_dict(Self::INTERNAL_EVENTPAIR)
424            .await?
425            .ok_or_else(|| anyhow!("internal eventpair missing from escrow"))?;
426        let p_external = remove_from_dict(Self::EXTERNAL_EVENTPAIR)
427            .await?
428            .ok_or_else(|| anyhow!("external eventpair missing from escrow"))?;
429        let frozen_inspect = remove_from_dict(Self::INSPECT).await?;
430
431        // TODO(https://fxbug.dev/383161492) Using the unescrowed inspect token, unescrow and
432        // republish the inspect data itself (using fuchsia.inspect/InspectSink.FetchEscrow) so that
433        // the component appears to be running if an inspect snapshot is taken (i.e. so that
434        // escrowed=false) in the inspect snapshot).
435        Ok(Self { p_internal, p_external, frozen_inspect })
436    }
437
438    async fn store(self) -> Result<fsandbox::DictionaryRef, Error> {
439        let Self { p_internal, p_external, frozen_inspect } = self;
440        let store =
441            fuchsia_component::client::connect_to_protocol::<fsandbox::CapabilityStoreMarker>()?;
442        let id_generator = sandbox::CapabilityIdGenerator::new();
443        let dict_id = id_generator.next();
444        let () = store.dictionary_create(dict_id).await?.map_err(|e| anyhow!("{e:?}"))?;
445
446        let insert_in_dict = |handle, key| async {
447            let id = id_generator.next();
448            let () = store
449                .import(id, fsandbox::Capability::Handle(handle))
450                .await?
451                .map_err(|e| anyhow!("{e:?}"))?;
452            let () = store
453                .dictionary_insert(dict_id, &fsandbox::DictionaryItem { key, value: id })
454                .await?
455                .map_err(|e| anyhow!("{e:?}"))?;
456            Result::<_, anyhow::Error>::Ok(())
457        };
458
459        if let Some(frozen_inspect) = frozen_inspect {
460            let () = insert_in_dict(frozen_inspect, Self::INSPECT.into()).await?;
461        }
462        let () = insert_in_dict(p_internal, Self::INTERNAL_EVENTPAIR.into()).await?;
463        let () = insert_in_dict(p_external, Self::EXTERNAL_EVENTPAIR.into()).await?;
464
465        let fsandbox::Capability::Dictionary(dictionary_ref) =
466            store.export(dict_id).await?.map_err(|e| anyhow!("{e:?}"))?
467        else {
468            anyhow::bail!("Bad capability type from dictionary");
469        };
470        Ok(dictionary_ref)
471    }
472}
473
474enum Services {
475    CommitStatusProvider(fupdate::CommitStatusProviderRequestStream),
476}
477
478#[cfg(test)]
479mod tests {
480    use super::*;
481
482    #[fasync::run_singlethreaded(test)]
483    async fn escrow_state_round_trip() {
484        let (p_internal, p_external) = zx::EventPair::create();
485        let frozen_inspect = Some(zx::Event::create().into());
486
487        let state = EscrowState {
488            p_internal: p_internal.into(),
489            p_external: p_external.into(),
490            frozen_inspect,
491        };
492
493        let stored = state.store().await.unwrap();
494        let loaded = EscrowState::load(stored).await.unwrap();
495        assert!(loaded.frozen_inspect.is_some());
496    }
497
498    #[fasync::run_singlethreaded(test)]
499    async fn escrow_state_round_trip_missing_inspect() {
500        let (p_internal, p_external) = zx::EventPair::create();
501
502        let state = EscrowState {
503            p_internal: p_internal.into(),
504            p_external: p_external.into(),
505            frozen_inspect: None,
506        };
507
508        let stored = state.store().await.unwrap();
509        let loaded = EscrowState::load(stored).await.unwrap();
510        assert_eq!(loaded.frozen_inspect, None);
511    }
512}