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_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
47pub 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 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 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
347struct TornDown(bool);
350
351impl Drop for TornDown {
352 fn drop(&mut self) {
353 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 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 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 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 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 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}