fshost_test_fixture/
lib.rs

1// Copyright 2022 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 assert_matches::assert_matches;
6use diagnostics_assertions::assert_data_tree;
7use diagnostics_reader::ArchiveReader;
8use disk_builder::Disk;
9use fidl::endpoints::{ServiceMarker as _, create_proxy};
10use fidl_fuchsia_fxfs::{BlobReaderMarker, CryptManagementProxy, CryptProxy, KeyPurpose};
11use fuchsia_component::client::{
12    connect_to_named_protocol_at_dir_root, connect_to_protocol_at_dir_root,
13};
14use fuchsia_component_test::{Capability, ChildOptions, RealmBuilder, RealmInstance, Ref, Route};
15use fuchsia_driver_test::{DriverTestRealmBuilder, DriverTestRealmInstance};
16use futures::channel::mpsc;
17use futures::{FutureExt as _, StreamExt as _};
18use ramdevice_client::{RamdiskClient, RamdiskClientBuilder};
19use std::pin::pin;
20use std::time::Duration;
21use {
22    fidl_fuchsia_boot as fboot, fidl_fuchsia_driver_test as fdt,
23    fidl_fuchsia_feedback as ffeedback, fidl_fuchsia_fshost_fxfsprovisioner as ffxfsprovisioner,
24    fidl_fuchsia_hardware_block_volume as fvolume, fidl_fuchsia_hardware_ramdisk as framdisk,
25    fidl_fuchsia_io as fio, fidl_fuchsia_security_keymint as fkeymint,
26    fidl_fuchsia_storage_block as fblock, fidl_fuchsia_storage_partitions as fpartitions,
27    fuchsia_async as fasync,
28};
29
30pub mod disk_builder;
31mod mocks;
32
33pub use disk_builder::write_blob;
34pub use fshost_assembly_config::{BlockDeviceConfig, BlockDeviceIdentifiers, BlockDeviceParent};
35
36pub const VFS_TYPE_BLOBFS: u32 = 0x9e694d21;
37pub const VFS_TYPE_MINFS: u32 = 0x6e694d21;
38pub const VFS_TYPE_MEMFS: u32 = 0x3e694d21;
39pub const VFS_TYPE_FXFS: u32 = 0x73667866;
40pub const VFS_TYPE_F2FS: u32 = 0xfe694d21;
41pub const STARNIX_VOLUME_NAME: &str = "starnix_volume";
42
43/// fshost will expose an alias of its fuchsia.hardware.block.volume.Service directory at this path.
44/// This allows tests to disambiguate service instances from the driver test realm, which are
45/// automatically aggregated.
46pub const FSHOST_VOLUME_SERVICE_DIR_NAME: &str = "VolumeService";
47
48pub fn round_down<
49    T: Into<U>,
50    U: Copy + std::ops::Rem<U, Output = U> + std::ops::Sub<U, Output = U>,
51>(
52    offset: U,
53    block_size: T,
54) -> U {
55    let block_size = block_size.into();
56    offset - offset % block_size
57}
58
59pub struct TestFixtureBuilder {
60    no_fuchsia_boot: bool,
61    disk: Option<Disk>,
62    extra_disks: Vec<Disk>,
63    fshost: fshost_testing::FshostBuilder,
64    zbi_ramdisk: Option<disk_builder::DiskBuilder>,
65    storage_host: bool,
66    force_fxfs_provisioner_failure: bool,
67}
68
69impl TestFixtureBuilder {
70    pub fn new(fshost_component_name: &'static str, storage_host: bool) -> Self {
71        Self {
72            no_fuchsia_boot: false,
73            disk: None,
74            extra_disks: Vec::new(),
75            fshost: fshost_testing::FshostBuilder::new(fshost_component_name),
76            zbi_ramdisk: None,
77            storage_host,
78            force_fxfs_provisioner_failure: false,
79        }
80    }
81
82    pub fn fshost(&mut self) -> &mut fshost_testing::FshostBuilder {
83        &mut self.fshost
84    }
85
86    pub fn with_disk(&mut self) -> &mut disk_builder::DiskBuilder {
87        self.disk = Some(Disk::Builder(disk_builder::DiskBuilder::new()));
88        self.disk.as_mut().unwrap().builder()
89    }
90
91    pub fn with_extra_disk(&mut self) -> &mut disk_builder::DiskBuilder {
92        self.extra_disks.push(Disk::Builder(disk_builder::DiskBuilder::new()));
93        self.extra_disks.last_mut().unwrap().builder()
94    }
95
96    pub fn with_uninitialized_disk(mut self) -> Self {
97        self.disk = Some(Disk::Builder(disk_builder::DiskBuilder::uninitialized()));
98        self
99    }
100
101    pub fn with_disk_from(mut self, disk: Disk) -> Self {
102        self.disk = Some(disk);
103        self
104    }
105
106    pub fn with_zbi_ramdisk(&mut self) -> &mut disk_builder::DiskBuilder {
107        self.zbi_ramdisk = Some(disk_builder::DiskBuilder::new());
108        self.zbi_ramdisk.as_mut().unwrap()
109    }
110
111    pub fn no_fuchsia_boot(mut self) -> Self {
112        self.no_fuchsia_boot = true;
113        self
114    }
115
116    pub fn with_device_config(mut self, device_config: Vec<BlockDeviceConfig>) -> Self {
117        self.fshost.set_device_config(device_config);
118        self
119    }
120
121    pub fn with_crypt_policy(mut self, policy: crypt_policy::Policy) -> Self {
122        self.fshost.set_crypt_policy(policy);
123        self
124    }
125
126    pub fn force_fxfs_provisioner_failure(mut self) -> Self {
127        self.force_fxfs_provisioner_failure = true;
128        self
129    }
130
131    pub async fn build(self) -> TestFixture {
132        let builder = RealmBuilder::new().await.unwrap();
133        let fshost = self.fshost.build(&builder).await;
134        // Create a second alias which routes fshost's volume Service capability to the parent.
135        builder
136            .add_route(
137                Route::new()
138                    .capability(
139                        Capability::service::<fvolume::ServiceMarker>()
140                            .as_(FSHOST_VOLUME_SERVICE_DIR_NAME),
141                    )
142                    .from(&fshost)
143                    .to(Ref::parent()),
144            )
145            .await
146            .unwrap();
147
148        let maybe_zbi_vmo = match self.zbi_ramdisk {
149            Some(disk_builder) => Some(disk_builder.build_as_zbi_ramdisk().await),
150            None => None,
151        };
152        let (tx, crash_reports) = mpsc::channel(32);
153        let mocks = mocks::new_mocks(maybe_zbi_vmo, tx, self.force_fxfs_provisioner_failure);
154
155        let mocks = builder
156            .add_local_child("mocks", move |h| mocks(h).boxed(), ChildOptions::new())
157            .await
158            .unwrap();
159        builder
160            .add_route(
161                Route::new()
162                    .capability(Capability::protocol::<fkeymint::SealingKeysMarker>())
163                    .capability(Capability::protocol::<fkeymint::AdminMarker>())
164                    .from(&mocks)
165                    .to(Ref::parent()),
166            )
167            .await
168            .unwrap();
169        builder
170            .add_route(
171                Route::new()
172                    .capability(Capability::protocol::<ffeedback::CrashReporterMarker>())
173                    .capability(Capability::protocol::<ffxfsprovisioner::FxfsProvisionerMarker>())
174                    .capability(Capability::protocol::<fkeymint::SealingKeysMarker>())
175                    .capability(Capability::protocol::<fkeymint::AdminMarker>())
176                    .from(&mocks)
177                    .to(&fshost),
178            )
179            .await
180            .unwrap();
181        if !self.no_fuchsia_boot {
182            builder
183                .add_route(
184                    Route::new()
185                        .capability(Capability::protocol::<fboot::ArgumentsMarker>())
186                        .capability(Capability::protocol::<fboot::ItemsMarker>())
187                        .from(&mocks)
188                        .to(&fshost),
189                )
190                .await
191                .unwrap();
192        }
193
194        builder
195            .add_route(
196                Route::new()
197                    .capability(Capability::dictionary("diagnostics"))
198                    .from(Ref::parent())
199                    .to(&fshost),
200            )
201            .await
202            .unwrap();
203
204        let dtr_exposes = vec![
205            fidl_fuchsia_component_test::Capability::Service(
206                fidl_fuchsia_component_test::Service {
207                    name: Some("fuchsia.hardware.ramdisk.Service".to_owned()),
208                    ..Default::default()
209                },
210            ),
211            fidl_fuchsia_component_test::Capability::Service(
212                fidl_fuchsia_component_test::Service {
213                    name: Some("fuchsia.hardware.block.volume.Service".to_owned()),
214                    ..Default::default()
215                },
216            ),
217        ];
218        builder.driver_test_realm_setup().await.unwrap();
219        builder.driver_test_realm_add_dtr_exposes(&dtr_exposes).await.unwrap();
220        builder
221            .add_route(
222                Route::new()
223                    .capability(Capability::directory("dev-topological").rights(fio::R_STAR_DIR))
224                    .capability(Capability::service::<fvolume::ServiceMarker>())
225                    .from(Ref::child(fuchsia_driver_test::COMPONENT_NAME))
226                    .to(&fshost),
227            )
228            .await
229            .unwrap();
230        builder
231            .add_route(
232                Route::new()
233                    .capability(
234                        Capability::directory("dev-class")
235                            .rights(fio::R_STAR_DIR)
236                            .subdir("block")
237                            .as_("dev-class-block"),
238                    )
239                    .from(Ref::child(fuchsia_driver_test::COMPONENT_NAME))
240                    .to(Ref::parent()),
241            )
242            .await
243            .unwrap();
244
245        let mut fixture = TestFixture {
246            realm: builder.build().await.unwrap(),
247            ramdisks: Vec::new(),
248            main_disk: None,
249            crash_reports,
250            torn_down: TornDown(false),
251            storage_host: self.storage_host,
252        };
253
254        log::info!(
255            realm_name:? = fixture.realm.root.child_name();
256            "built new test realm",
257        );
258
259        fixture
260            .realm
261            .driver_test_realm_start(fdt::RealmArgs {
262                root_driver: Some("fuchsia-boot:///platform-bus#meta/platform-bus.cm".to_owned()),
263                dtr_exposes: Some(dtr_exposes),
264                software_devices: Some(vec![
265                    fdt::SoftwareDevice {
266                        device_name: "ram-disk".to_string(),
267                        device_id: bind_fuchsia_platform::BIND_PLATFORM_DEV_DID_RAM_DISK,
268                    },
269                    fdt::SoftwareDevice {
270                        device_name: "ram-nand".to_string(),
271                        device_id: bind_fuchsia_platform::BIND_PLATFORM_DEV_DID_RAM_NAND,
272                    },
273                ]),
274                ..Default::default()
275            })
276            .await
277            .unwrap();
278
279        // The order of adding disks matters here, unfortunately. fshost should not change behavior
280        // based on the order disks appear, but because we take the first available that matches
281        // whatever relevant criteria, it's useful to test that matchers don't get clogged up by
282        // previous disks.
283        // TODO(https://fxbug.dev/380353856): This type of testing should be irrelevant once the
284        // block devices are determined by configuration options instead of heuristically.
285        for disk in self.extra_disks.into_iter() {
286            fixture.add_disk(disk).await;
287        }
288        if let Some(disk) = self.disk {
289            fixture.add_main_disk(disk).await;
290        }
291
292        fixture
293    }
294}
295
296/// Create a separate struct that does the drop-assert because fixture.tear_down can't call
297/// realm.destroy if it has the drop impl itself.
298struct TornDown(bool);
299
300impl Drop for TornDown {
301    fn drop(&mut self) {
302        // Because tear_down is async, it needs to be called by the test in an async context. It
303        // checks some properties so for correctness it must be called.
304        assert!(self.0, "fixture.tear_down() must be called");
305    }
306}
307
308pub struct TestFixture {
309    pub realm: RealmInstance,
310    pub ramdisks: Vec<RamdiskClient>,
311    pub main_disk: Option<Disk>,
312    pub crash_reports: mpsc::Receiver<ffeedback::CrashReport>,
313    torn_down: TornDown,
314    storage_host: bool,
315}
316
317impl TestFixture {
318    pub async fn tear_down(mut self) -> Option<Disk> {
319        log::info!(realm_name:? = self.realm.root.child_name(); "tearing down");
320        let disk = self.main_disk.take();
321        // Check the crash reports before destroying the realm because tearing down the realm can
322        // cause mounting errors that trigger a crash report.
323        assert_matches!(self.crash_reports.try_next(), Ok(None) | Err(_));
324        self.realm.destroy().await.unwrap();
325        self.torn_down.0 = true;
326        disk
327    }
328
329    pub fn exposed_dir(&self) -> &fio::DirectoryProxy {
330        self.realm.root.get_exposed_dir()
331    }
332
333    pub fn dir(&self, dir: &str, flags: fio::Flags) -> fio::DirectoryProxy {
334        let (dev, server) = create_proxy::<fio::DirectoryMarker>();
335        let flags = flags | fio::Flags::PROTOCOL_DIRECTORY;
336        self.realm
337            .root
338            .get_exposed_dir()
339            .open(dir, flags, &fio::Options::default(), server.into_channel())
340            .expect("open failed");
341        dev
342    }
343
344    pub async fn check_fs_type(&self, dir: &str, fs_type: u32) {
345        let (status, info) =
346            self.dir(dir, fio::Flags::empty()).query_filesystem().await.expect("query failed");
347        assert_eq!(zx::Status::from_raw(status), zx::Status::OK);
348        assert!(info.is_some());
349        let info_type = info.unwrap().fs_type;
350        assert_eq!(info_type, fs_type, "{:#08x} != {:#08x}", info_type, fs_type);
351    }
352
353    pub async fn check_test_blob(&self) {
354        let expected_blob_hash = disk_builder::test_blob_hash();
355        let reader =
356            connect_to_protocol_at_dir_root::<BlobReaderMarker>(self.realm.root.get_exposed_dir())
357                .expect("failed to connect to the BlobReader");
358        let _vmo = reader
359            .get_vmo(&expected_blob_hash.into())
360            .await
361            .expect("blob get_vmo fidl error")
362            .unwrap_or_else(|e| match zx::Status::from_raw(e) {
363                zx::Status::NOT_FOUND => panic!("Test blob not found - blobfs lost data!"),
364                s => panic!("Error while opening test blob vmo: {s}"),
365            });
366    }
367
368    /// Check for the existence of a well-known set of test files in the data volume. These files
369    /// are placed by the disk builder if it formats the filesystem beforehand.
370    pub async fn check_test_data_file(&self) {
371        let (file, server) = create_proxy::<fio::NodeMarker>();
372        self.dir("data", fio::PERM_READABLE)
373            .open(".testdata", fio::PERM_READABLE, &fio::Options::default(), server.into_channel())
374            .expect("open failed");
375        file.get_attributes(fio::NodeAttributesQuery::empty())
376            .await
377            .expect("Fidl transport error on get_attributes()")
378            .expect("get_attr failed - data was probably deleted!");
379
380        let data = self.dir("data", fio::PERM_READABLE);
381        fuchsia_fs::directory::open_file(&data, ".testdata", fio::PERM_READABLE).await.unwrap();
382
383        fuchsia_fs::directory::open_directory(&data, "ssh", fio::PERM_READABLE).await.unwrap();
384        fuchsia_fs::directory::open_directory(&data, "ssh/config", fio::PERM_READABLE)
385            .await
386            .unwrap();
387        fuchsia_fs::directory::open_directory(&data, "problems", fio::PERM_READABLE).await.unwrap();
388
389        let authorized_keys =
390            fuchsia_fs::directory::open_file(&data, "ssh/authorized_keys", fio::PERM_READABLE)
391                .await
392                .unwrap();
393        assert_eq!(
394            &fuchsia_fs::file::read_to_string(&authorized_keys).await.unwrap(),
395            "public key!"
396        );
397    }
398
399    /// Checks for the absence of the .testdata marker file, indicating the data filesystem was
400    /// reformatted.
401    pub async fn check_test_data_file_absent(&self) {
402        let err = fuchsia_fs::directory::open_file(
403            &self.dir("data", fio::PERM_READABLE),
404            ".testdata",
405            fio::PERM_READABLE,
406        )
407        .await
408        .expect_err("open_file failed");
409        assert!(err.is_not_found_error());
410    }
411
412    pub async fn add_main_disk(&mut self, disk: Disk) {
413        assert!(self.main_disk.is_none());
414        let (vmo, type_guid) = disk.into_vmo_and_type_guid().await;
415        let vmo_clone =
416            vmo.create_child(zx::VmoChildOptions::SLICE, 0, vmo.get_size().unwrap()).unwrap();
417
418        self.add_ramdisk(vmo, type_guid).await;
419        self.main_disk = Some(Disk::Prebuilt(vmo_clone, type_guid));
420    }
421
422    pub async fn add_disk(&mut self, disk: Disk) {
423        let (vmo, type_guid) = disk.into_vmo_and_type_guid().await;
424        self.add_ramdisk(vmo, type_guid).await;
425    }
426
427    async fn add_ramdisk(&mut self, vmo: zx::Vmo, type_guid: Option<[u8; 16]>) {
428        let mut ramdisk_builder = if self.storage_host {
429            RamdiskClientBuilder::new_with_vmo(vmo, Some(512)).use_v2().publish().ramdisk_service(
430                self.dir(framdisk::ServiceMarker::SERVICE_NAME, fio::Flags::empty()),
431            )
432        } else {
433            RamdiskClientBuilder::new_with_vmo(vmo, Some(512))
434                .dev_root(self.dir("dev-topological", fio::Flags::empty()))
435        };
436        if let Some(guid) = type_guid {
437            ramdisk_builder = ramdisk_builder.guid(guid);
438        }
439        let mut ramdisk = pin!(ramdisk_builder.build().fuse());
440
441        let ramdisk = futures::select_biased!(
442            res = ramdisk => res,
443            _ = fasync::Timer::new(Duration::from_secs(120))
444                .fuse() => panic!("Timed out waiting for RamdiskClient"),
445        )
446        .unwrap();
447        self.ramdisks.push(ramdisk);
448    }
449
450    pub fn connect_to_crypt(&self) -> CryptProxy {
451        self.realm
452            .root
453            .connect_to_protocol_at_exposed_dir()
454            .expect("connect_to_protocol_at_exposed_dir failed for the Crypt protocol")
455    }
456
457    pub async fn setup_starnix_crypt(&self) -> (CryptProxy, CryptManagementProxy) {
458        let crypt_management: CryptManagementProxy =
459            self.realm.root.connect_to_protocol_at_exposed_dir().expect(
460                "connect_to_protocol_at_exposed_dir failed for the CryptManagement protocol",
461            );
462        let crypt = self
463            .realm
464            .root
465            .connect_to_protocol_at_exposed_dir()
466            .expect("connect_to_protocol_at_exposed_dir failed for the Crypt protocol");
467        let key = vec![0xABu8; 32];
468        crypt_management
469            .add_wrapping_key(&u128::to_le_bytes(0), key.as_slice())
470            .await
471            .expect("fidl transport error")
472            .expect("add wrapping key failed");
473        crypt_management
474            .add_wrapping_key(&u128::to_le_bytes(1), key.as_slice())
475            .await
476            .expect("fidl transport error")
477            .expect("add wrapping key failed");
478        crypt_management
479            .set_active_key(KeyPurpose::Data, &u128::to_le_bytes(0))
480            .await
481            .expect("fidl transport error")
482            .expect("set metadata key failed");
483        crypt_management
484            .set_active_key(KeyPurpose::Metadata, &u128::to_le_bytes(1))
485            .await
486            .expect("fidl transport error")
487            .expect("set metadata key failed");
488        (crypt, crypt_management)
489    }
490
491    /// This must be called if any crash reports are expected, since spurious reports will cause a
492    /// failure in TestFixture::tear_down.
493    pub async fn wait_for_crash_reports(
494        &mut self,
495        count: usize,
496        expected_program: &'_ str,
497        expected_signature: &'_ str,
498    ) {
499        log::info!("Waiting for {count} crash reports");
500        for _ in 0..count {
501            let report = self.crash_reports.next().await.expect("Sender closed");
502            assert_eq!(report.program_name.as_deref(), Some(expected_program));
503            assert_eq!(report.crash_signature.as_deref(), Some(expected_signature));
504        }
505        if count > 0 {
506            let selector =
507                format!("realm_builder\\:{}/test-fshost:root", self.realm.root.child_name());
508            log::info!("Checking inspect for corruption event, selector={selector}");
509            let tree = ArchiveReader::inspect()
510                .add_selector(selector)
511                .snapshot()
512                .await
513                .unwrap()
514                .into_iter()
515                .next()
516                .and_then(|result| result.payload)
517                .expect("expected one inspect hierarchy");
518
519            let format = || expected_program.to_string();
520
521            assert_data_tree!(tree, root: contains {
522                corruption_events: contains {
523                    format() => 1u64,
524                }
525            });
526        }
527    }
528
529    // Check that the system partition table contains partitions with labels found in `expected`.
530    pub async fn check_system_partitions(&self, mut expected: Vec<&str>) {
531        let partitions =
532            self.dir(fpartitions::PartitionServiceMarker::SERVICE_NAME, fio::PERM_READABLE);
533        let entries =
534            fuchsia_fs::directory::readdir(&partitions).await.expect("Failed to read partitions");
535
536        assert_eq!(entries.len(), expected.len());
537
538        let mut found_partition_labels = Vec::new();
539        for entry in entries {
540            let endpoint_name = format!("{}/volume", entry.name);
541            let volume = connect_to_named_protocol_at_dir_root::<fblock::BlockMarker>(
542                &partitions,
543                &endpoint_name,
544            )
545            .expect("failed to connect to named protocol at dir root");
546            let (raw_status, label) = volume.get_name().await.expect("failed to call get_name");
547            zx::Status::ok(raw_status).expect("get_name status failed");
548            found_partition_labels.push(label.expect("partition label expected to be some value"));
549        }
550        found_partition_labels.sort();
551        expected.sort();
552        assert_eq!(found_partition_labels, expected);
553    }
554}