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