isolated_swd/
updater.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.
4use anyhow::{Context, Error, anyhow};
5use fidl_fuchsia_paver::{BootManagerMarker, Configuration, PaverMarker, PaverProxy};
6use fidl_fuchsia_update_installer::{InstallerMarker, InstallerProxy, RebootControllerMarker};
7use fidl_fuchsia_update_installer_ext::options::{Initiator, Options};
8use fidl_fuchsia_update_installer_ext::{UpdateAttempt, start_update};
9
10use futures::prelude::*;
11
12pub const DEFAULT_UPDATE_PACKAGE_URL: &str = "fuchsia-pkg://fuchsia.com/update";
13
14pub struct Updater {
15    proxy: InstallerProxy,
16    paver_proxy: PaverProxy,
17}
18
19impl Updater {
20    pub fn new_with_proxies(proxy: InstallerProxy, paver_proxy: PaverProxy) -> Self {
21        Self { proxy, paver_proxy }
22    }
23
24    pub fn new() -> Result<Self, Error> {
25        Ok(Self::new_with_proxies(
26            fuchsia_component::client::connect_to_protocol::<InstallerMarker>()?,
27            fuchsia_component::client::connect_to_protocol::<PaverMarker>()?,
28        ))
29    }
30
31    pub async fn start_update(
32        &mut self,
33        update_package: Option<&url::Url>,
34        signature: Option<&[u8]>,
35    ) -> Result<UpdateAttempt, Error> {
36        let update_package = match update_package {
37            Some(url) => url.to_owned(),
38            None => DEFAULT_UPDATE_PACKAGE_URL.parse().unwrap(),
39        };
40
41        let (reboot_controller, reboot_controller_server_end) =
42            fidl::endpoints::create_proxy::<RebootControllerMarker>();
43        let () = reboot_controller.detach().context("disabling automatic reboot")?;
44
45        start_update(
46            &update_package,
47            Options {
48                initiator: Initiator::User,
49                allow_attach_to_existing_attempt: false,
50                should_write_recovery: false,
51            },
52            &self.proxy,
53            Some(reboot_controller_server_end),
54            signature,
55        )
56        .await
57        .context("starting system update")
58    }
59
60    /// Perform an update, skipping the final reboot.
61    /// If `update_package` is Some, use the given package URL as the URL for the update package.
62    /// Otherwise, `system-updater` uses the default URL.
63    /// This will not install any images to the recovery partitions.
64    pub async fn install_update(
65        &mut self,
66        update_package: Option<&url::Url>,
67        signature: Option<&[u8]>,
68    ) -> Result<(), Error> {
69        let attempt = self.start_update(update_package, signature).await?;
70
71        let () = Self::monitor_update_attempt(attempt).await.context("monitoring installation")?;
72
73        let () = Self::activate_installed_slot(&self.paver_proxy)
74            .await
75            .context("activating installed slot")?;
76
77        Ok(())
78    }
79
80    async fn monitor_update_attempt(mut attempt: UpdateAttempt) -> Result<(), Error> {
81        while let Some(state) = attempt.try_next().await.context("fetching next update state")? {
82            log::info!("Install: {:?}", state);
83            if state.is_success() {
84                return Ok(());
85            } else if state.is_failure() {
86                return Err(anyhow!("update attempt failed in state {:?}", state));
87            }
88        }
89
90        Err(anyhow!("unexpected end of update attempt"))
91    }
92
93    async fn activate_installed_slot(paver: &PaverProxy) -> Result<(), Error> {
94        let (boot_manager, remote) = fidl::endpoints::create_proxy::<BootManagerMarker>();
95
96        paver.find_boot_manager(remote).context("finding boot manager")?;
97
98        let result = boot_manager.query_active_configuration().await;
99        if let Err(fidl::Error::ClientChannelClosed { status: zx::Status::NOT_SUPPORTED, .. }) =
100            result
101        {
102            // board does not actually support ABR, so return.
103            log::info!("ABR not supported, not configuring slots.");
104            return Ok(());
105        }
106        let result = result?;
107        if result.is_ok() {
108            // active slot is valid - assume that system-updater handled this for us.
109            return Ok(());
110        }
111
112        // In recovery, the paver will return ZX_ERR_NOT_SUPPORTED to query_active_configuration(),
113        // even on devices which support ABR. Handle this manually in case it is actually
114        // supported.
115        zx::Status::ok(
116            boot_manager
117                .set_configuration_active(Configuration::A)
118                .await
119                .context("Sending set active configuration request")?,
120        )
121        .context("Setting A to active configuration")?;
122        Ok(())
123    }
124}
125
126#[cfg(test)]
127pub(crate) mod for_tests {
128    use super::*;
129    use crate::resolver::for_tests::{EMPTY_REPO_PATH, ResolverForTest};
130    use blobfs_ramdisk::BlobfsRamdisk;
131    use fidl_fuchsia_paver::PaverRequestStream;
132    use fuchsia_component_test::{
133        Capability, ChildOptions, DirectoryContents, RealmBuilder, RealmInstance, Ref, Route,
134    };
135    use fuchsia_merkle::Hash;
136    use fuchsia_pkg_testing::serve::ServedRepository;
137    use fuchsia_pkg_testing::{Package, RepositoryBuilder, SystemImageBuilder};
138    use mock_paver::{MockPaverService, MockPaverServiceBuilder, PaverEvent};
139    use std::collections::BTreeSet;
140    use std::sync::Arc;
141    use {fidl_fuchsia_metrics as fmetrics, fuchsia_async as fasync};
142
143    pub const TEST_REPO_URL: &str = "fuchsia-pkg://fuchsia.com";
144    pub struct UpdaterBuilder {
145        paver_builder: MockPaverServiceBuilder,
146        packages: Vec<Package>,
147        // The zbi and optional vbmeta contents.
148        fuchsia_image: Option<(Vec<u8>, Option<Vec<u8>>)>,
149        // The zbi and optional vbmeta contents of the recovery partition.
150        recovery_image: Option<(Vec<u8>, Option<Vec<u8>>)>,
151        repo_url: fuchsia_url::RepositoryUrl,
152    }
153
154    impl UpdaterBuilder {
155        /// Construct a new UpdateBuilder. Initially, this contains no images and an empty system
156        /// image package.
157        pub async fn new() -> UpdaterBuilder {
158            UpdaterBuilder {
159                paver_builder: MockPaverServiceBuilder::new(),
160                packages: vec![SystemImageBuilder::new().build().await],
161                fuchsia_image: None,
162                recovery_image: None,
163                repo_url: TEST_REPO_URL.parse().unwrap(),
164            }
165        }
166
167        /// Add a package to the update package this builder will generate.
168        pub fn add_package(mut self, package: Package) -> Self {
169            self.packages.push(package);
170            self
171        }
172
173        /// The zbi and optional vbmeta images to write.
174        pub fn fuchsia_image(mut self, zbi: Vec<u8>, vbmeta: Option<Vec<u8>>) -> Self {
175            assert_eq!(self.fuchsia_image, None);
176            self.fuchsia_image = Some((zbi, vbmeta));
177            self
178        }
179
180        /// The zbi and optional vbmeta images to write to the recovery partition.
181        pub fn recovery_image(mut self, zbi: Vec<u8>, vbmeta: Option<Vec<u8>>) -> Self {
182            assert_eq!(self.recovery_image, None);
183            self.recovery_image = Some((zbi, vbmeta));
184            self
185        }
186
187        /// Mutate the `MockPaverServiceBuilder` contained in this UpdaterBuilder.
188        pub fn paver<F>(mut self, f: F) -> Self
189        where
190            F: FnOnce(MockPaverServiceBuilder) -> MockPaverServiceBuilder,
191        {
192            self.paver_builder = f(self.paver_builder);
193            self
194        }
195
196        pub fn repo_url(mut self, url: &str) -> Self {
197            self.repo_url = url.parse().expect("Valid URL supplied to repo_url()");
198            self
199        }
200
201        fn serve_mock_paver(stream: PaverRequestStream, paver: Arc<MockPaverService>) {
202            let paver_clone = Arc::clone(&paver);
203            fasync::Task::spawn(
204                Arc::clone(&paver_clone)
205                    .run_paver_service(stream)
206                    .unwrap_or_else(|e| panic!("Failed to run mock paver: {e:?}")),
207            )
208            .detach();
209        }
210
211        async fn run_mock_paver(
212            handles: fuchsia_component_test::LocalComponentHandles,
213            paver: Arc<MockPaverService>,
214        ) -> Result<(), Error> {
215            let mut fs = fuchsia_component::server::ServiceFs::new();
216            fs.dir("svc")
217                .add_fidl_service(move |stream| Self::serve_mock_paver(stream, Arc::clone(&paver)));
218            fs.serve_connection(handles.outgoing_dir)?;
219            let () = fs.for_each_concurrent(None, |req| async move { req }).await;
220            Ok(())
221        }
222
223        /// Create an UpdateForTest from this UpdaterBuilder.
224        /// This will construct an update package containing all packages and images added to the
225        /// builder, create a repository containing the packages, and create a MockPaver.
226        pub async fn build(self) -> UpdaterForTest {
227            let mut update = fuchsia_pkg_testing::UpdatePackageBuilder::new(self.repo_url.clone())
228                .packages(
229                    self.packages
230                        .iter()
231                        .map(|p| {
232                            fuchsia_url::PinnedAbsolutePackageUrl::new(
233                                self.repo_url.clone(),
234                                p.name().clone(),
235                                None,
236                                *p.hash(),
237                            )
238                        })
239                        .collect::<Vec<_>>(),
240                );
241            if let Some((zbi, vbmeta)) = self.fuchsia_image {
242                update = update.fuchsia_image(zbi, vbmeta);
243            }
244            if let Some((zbi, vbmeta)) = self.recovery_image {
245                update = update.recovery_image(zbi, vbmeta);
246            }
247            let (update, images) = update.build().await;
248
249            // Do not include the images package, system-updater triggers GC after resolving it.
250            let expected_blobfs_contents = self
251                .packages
252                .iter()
253                .chain([update.as_package()])
254                .flat_map(|p| p.list_blobs())
255                .collect();
256
257            let repo = Arc::new(
258                self.packages
259                    .iter()
260                    .chain([update.as_package(), &images])
261                    .fold(
262                        RepositoryBuilder::from_template_dir(EMPTY_REPO_PATH)
263                            .add_package(update.as_package()),
264                        |repo, package| repo.add_package(package),
265                    )
266                    .build()
267                    .await
268                    .expect("Building repo"),
269            );
270
271            let realm_builder = RealmBuilder::new().await.unwrap();
272            let blobfs = BlobfsRamdisk::start().await.context("starting blobfs").unwrap();
273
274            let served_repo = Arc::new(Arc::clone(&repo).server().start().unwrap());
275
276            let resolver_realm = ResolverForTest::realm_setup(
277                &realm_builder,
278                Arc::clone(&served_repo),
279                self.repo_url.clone(),
280                &blobfs,
281            )
282            .await
283            .unwrap();
284
285            let system_updater = realm_builder
286                .add_child("system-updater", "#meta/system-updater.cm", ChildOptions::new())
287                .await
288                .unwrap();
289
290            let service_reflector = realm_builder
291                .add_local_child(
292                    "system_updater_service_reflector",
293                    move |handles| {
294                        let mut fs = fuchsia_component::server::ServiceFs::new();
295                        // Not necessary for updates, but without this system-updater will wait 30
296                        // seconds trying to flush cobalt logs before logging an attempt error,
297                        // and the test is torn down before then, so the error is lost. Also
298                        // prevents spam of irrelevant error logs.
299                        fs.dir("svc").add_fidl_service(move |stream| {
300                            fasync::Task::spawn(
301                                Arc::new(mock_metrics::MockMetricEventLoggerFactory::new())
302                                    .run_logger_factory(stream),
303                            )
304                            .detach()
305                        });
306                        async move {
307                            fs.serve_connection(handles.outgoing_dir).unwrap();
308                            let () = fs.collect().await;
309                            Ok(())
310                        }
311                        .boxed()
312                    },
313                    ChildOptions::new(),
314                )
315                .await
316                .unwrap();
317
318            realm_builder
319                .add_route(
320                    Route::new()
321                        .capability(
322                            Capability::protocol::<fmetrics::MetricEventLoggerFactoryMarker>(),
323                        )
324                        .from(&service_reflector)
325                        .to(&system_updater),
326                )
327                .await
328                .unwrap();
329
330            // Set up paver and routes
331            let paver = Arc::new(self.paver_builder.build());
332            let paver_clone = Arc::clone(&paver);
333            let mock_paver = realm_builder
334                .add_local_child(
335                    "paver",
336                    move |handles| Box::pin(Self::run_mock_paver(handles, Arc::clone(&paver))),
337                    ChildOptions::new(),
338                )
339                .await
340                .unwrap();
341
342            realm_builder
343                .add_route(
344                    Route::new()
345                        .capability(Capability::protocol_by_name("fuchsia.paver.Paver"))
346                        .from(&mock_paver)
347                        .to(&system_updater),
348                )
349                .await
350                .unwrap();
351            realm_builder
352                .add_route(
353                    Route::new()
354                        .capability(Capability::protocol_by_name("fuchsia.paver.Paver"))
355                        .from(&mock_paver)
356                        .to(Ref::parent()),
357                )
358                .await
359                .unwrap();
360
361            // Set up build-info and routes
362            realm_builder
363                .read_only_directory(
364                    "build-info",
365                    vec![&system_updater],
366                    DirectoryContents::new().add_file("board", "test".as_bytes()),
367                )
368                .await
369                .unwrap();
370
371            // Set up pkg-resolver and pkg-cache routes
372            realm_builder
373                .add_route(
374                    Route::new()
375                        .capability(
376                            Capability::protocol_by_name("fuchsia.pkg.PackageResolver-ota")
377                                .as_("fuchsia.pkg.PackageResolver"),
378                        )
379                        .from(&resolver_realm.resolver)
380                        .to(&system_updater),
381                )
382                .await
383                .unwrap();
384
385            realm_builder
386                .add_route(
387                    Route::new()
388                        .capability(Capability::protocol_by_name("fuchsia.pkg.PackageCache"))
389                        .capability(Capability::protocol_by_name("fuchsia.pkg.RetainedPackages"))
390                        .capability(Capability::protocol_by_name(
391                            "fuchsia.pkg.garbagecollector.Manager",
392                        ))
393                        .from(&resolver_realm.cache)
394                        .to(&system_updater),
395                )
396                .await
397                .unwrap();
398            realm_builder
399                .add_capability(cm_rust::CapabilityDecl::Config(cm_rust::ConfigurationDecl {
400                    name: "fuchsia.system-updater.ManifestPublicKeys".parse().unwrap(),
401                    value: Vec::<String>::new().into(),
402                }))
403                .await
404                .unwrap();
405            realm_builder
406                .add_route(
407                    Route::new()
408                        .capability(Capability::configuration(
409                            "fuchsia.system-updater.ManifestPublicKeys",
410                        ))
411                        .from(Ref::self_())
412                        .to(&system_updater),
413                )
414                .await
415                .unwrap();
416            realm_builder
417                .add_route(
418                    Route::new()
419                        .capability(Capability::configuration(
420                            "fuchsia.system-updater.VerifyExistingBlobs",
421                        ))
422                        .from(Ref::void())
423                        .to(&system_updater),
424                )
425                .await
426                .unwrap();
427
428            // Make sure the component under test can log.
429            realm_builder
430                .add_route(
431                    Route::new()
432                        .capability(Capability::protocol_by_name("fuchsia.logger.LogSink"))
433                        .from(Ref::parent())
434                        .to(&system_updater),
435                )
436                .await
437                .unwrap();
438
439            // Expose system_updater to the parent
440            realm_builder
441                .add_route(
442                    Route::new()
443                        .capability(Capability::protocol_by_name(
444                            "fuchsia.update.installer.Installer",
445                        ))
446                        .from(&system_updater)
447                        .to(Ref::parent()),
448                )
449                .await
450                .unwrap();
451
452            // Expose pkg-cache to these tests, for use by verify_packages
453            realm_builder
454                .add_route(
455                    Route::new()
456                        .capability(Capability::protocol_by_name("fuchsia.pkg.PackageCache"))
457                        .from(&resolver_realm.cache)
458                        .to(Ref::parent()),
459                )
460                .await
461                .unwrap();
462
463            let realm_instance = realm_builder.build().await.unwrap();
464
465            let installer_proxy = realm_instance.root.connect_to_protocol_at_exposed_dir().unwrap();
466            let paver_proxy = realm_instance.root.connect_to_protocol_at_exposed_dir().unwrap();
467
468            let updater = Updater::new_with_proxies(installer_proxy, paver_proxy);
469
470            let resolver = ResolverForTest::new(&realm_instance, blobfs, Arc::clone(&served_repo))
471                .await
472                .unwrap();
473
474            UpdaterForTest {
475                served_repo,
476                paver: paver_clone,
477                expected_blobfs_contents,
478                update_merkle_root: *update.as_package().hash(),
479                repo_url: self.repo_url,
480                updater,
481                resolver,
482                realm_instance,
483            }
484        }
485
486        #[cfg(test)]
487        pub async fn build_and_run(self) -> UpdaterResult {
488            self.build().await.run().await
489        }
490    }
491
492    /// This wraps the `Updater` in order to reduce test boilerplate.
493    /// Should be constructed using `UpdaterBuilder`.
494    pub struct UpdaterForTest {
495        #[expect(dead_code)]
496        pub served_repo: Arc<ServedRepository>,
497        pub paver: Arc<MockPaverService>,
498        pub expected_blobfs_contents: BTreeSet<Hash>,
499        pub update_merkle_root: Hash,
500        #[expect(dead_code)]
501        pub repo_url: fuchsia_url::RepositoryUrl,
502        pub resolver: ResolverForTest,
503        pub updater: Updater,
504        pub realm_instance: RealmInstance,
505    }
506
507    impl UpdaterForTest {
508        /// Run the system update, returning an `UpdaterResult` containing information about the
509        /// result of the update.
510        pub async fn run(mut self) -> UpdaterResult {
511            let () = self.updater.install_update(None, None).await.expect("installing update");
512
513            UpdaterResult {
514                paver_events: self.paver.take_events(),
515                resolver: self.resolver,
516                expected_blobfs_contents: self.expected_blobfs_contents,
517                realm_instance: self.realm_instance,
518            }
519        }
520    }
521
522    /// Contains information about the state of the system after the updater was run.
523    pub struct UpdaterResult {
524        /// All paver events received by the MockPaver during the update.
525        pub paver_events: Vec<PaverEvent>,
526        /// The resolver used by the updater.
527        pub resolver: ResolverForTest,
528        /// All the blobs that should be in blobfs after the update.
529        pub expected_blobfs_contents: BTreeSet<Hash>,
530        // The RealmInstance used to run this update, for introspection into component states.
531        #[expect(dead_code)]
532        pub realm_instance: RealmInstance,
533    }
534
535    impl UpdaterResult {
536        /// Verify that all packages that should have been resolved by the update
537        /// were resolved.
538        pub async fn verify_packages(&self) {
539            // Verify directly against blobfs to avoid any trickery pkg-resolver or pkg-cache may
540            // engage in.
541            let actual_contents =
542                self.resolver.cache.blobfs.list_blobs().expect("Listing blobfs blobs");
543            assert_eq!(actual_contents, self.expected_blobfs_contents);
544        }
545    }
546}
547
548#[cfg(test)]
549pub mod tests {
550    use super::for_tests::UpdaterBuilder;
551    use super::*;
552    use fidl_fuchsia_paver::Asset;
553    use fuchsia_async as fasync;
554    use fuchsia_pkg_testing::PackageBuilder;
555    use mock_paver::PaverEvent;
556
557    #[fasync::run_singlethreaded(test)]
558    pub async fn test_updater() {
559        let data = "hello world!".as_bytes();
560        let test_package = PackageBuilder::new("test_package")
561            .add_resource_at("bin/hello", "this is a test".as_bytes())
562            .add_resource_at("data/file", "this is a file".as_bytes())
563            .add_resource_at("meta/test_package.cm", "{}".as_bytes())
564            .build()
565            .await
566            .expect("Building test_package");
567        let updater = UpdaterBuilder::new()
568            .await
569            .paver(|p| {
570                // Emulate ABR not being supported
571                p.boot_manager_close_with_epitaph(zx::Status::NOT_SUPPORTED)
572            })
573            .add_package(test_package)
574            .fuchsia_image(data.to_vec(), Some(data.to_vec()))
575            .recovery_image(data.to_vec(), Some(data.to_vec()));
576        let result = updater.build_and_run().await;
577
578        assert_eq!(
579            result.paver_events,
580            vec![
581                PaverEvent::WriteAsset {
582                    configuration: Configuration::A,
583                    asset: Asset::Kernel,
584                    payload: data.to_vec()
585                },
586                PaverEvent::WriteAsset {
587                    configuration: Configuration::B,
588                    asset: Asset::Kernel,
589                    payload: data.to_vec()
590                },
591                PaverEvent::WriteAsset {
592                    configuration: Configuration::A,
593                    asset: Asset::VerifiedBootMetadata,
594                    payload: data.to_vec()
595                },
596                PaverEvent::WriteAsset {
597                    configuration: Configuration::B,
598                    asset: Asset::VerifiedBootMetadata,
599                    payload: data.to_vec()
600                },
601                // isolated-swd does not write recovery even if an image is provided.
602                PaverEvent::DataSinkFlush,
603            ]
604        );
605
606        let () = result.verify_packages().await;
607    }
608}