1use 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
43pub 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 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 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
296struct TornDown(bool);
299
300impl Drop for TornDown {
301 fn drop(&mut self) {
302 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 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 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 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 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 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}