Skip to main content

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