fshost_test_fixture/
disk_builder.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 blob_writer::BlobWriter;
6use block_client::{BlockClient as _, RemoteBlockClient};
7use delivery_blob::{CompressionMode, Type1Blob};
8use fake_keymint::with_keymint_service;
9use fidl_fuchsia_fs_startup::{CreateOptions, MountOptions};
10use fidl_fuchsia_fxfs::{
11    BlobCreatorProxy, CryptManagementMarker, CryptManagementProxy, CryptMarker, KeyPurpose,
12};
13use fs_management::filesystem::{
14    BlockConnector, DirBasedBlockConnector, Filesystem, ServingMultiVolumeFilesystem,
15};
16use fs_management::format::constants::{F2FS_MAGIC, FXFS_MAGIC, MINFS_MAGIC};
17use fs_management::{BLOBFS_TYPE_GUID, DATA_TYPE_GUID, FVM_TYPE_GUID, Fvm, Fxfs};
18use fuchsia_component::client::connect_to_protocol_at_dir_svc;
19use fuchsia_component_test::{Capability, ChildOptions, RealmBuilder, RealmInstance, Ref, Route};
20use fuchsia_hash::Hash;
21use gpt_component::gpt::GptManager;
22use key_bag::Aes256Key;
23use serde_json::json;
24use std::collections::HashSet;
25use std::ops::Deref;
26use std::sync::Arc;
27use storage_isolated_driver_manager::fvm::format_for_fvm;
28use storage_isolated_driver_manager::{BlockDeviceMatcher, find_block_device};
29use uuid::Uuid;
30use vmo_backed_block_server::{VmoBackedServer, VmoBackedServerTestingExt as _};
31use zerocopy::{Immutable, IntoBytes};
32use zx::{self as zx, HandleBased};
33use {
34    fidl_fuchsia_io as fio, fidl_fuchsia_logger as flogger, fidl_fuchsia_storage_block as fblock,
35    fuchsia_async as fasync,
36};
37
38pub const TEST_DISK_BLOCK_SIZE: u32 = 512;
39pub const FVM_SLICE_SIZE: u64 = 32 * 1024;
40pub const FVM_F2FS_SLICE_SIZE: u64 = 2 * 1024 * 1024;
41
42// The default disk size is about 55MiB, with about 51MiB dedicated to the data volume. This size
43// is chosen because the data volume has to be big enough to support f2fs (>= DEFAULT_F2FS_MIN_BYTES
44// defined in //src/storage/fshost/device/constants.rs), which has a relatively large minimum size
45// requirement to be formatted.
46//
47// Only the data volume is actually created with a specific size, the other volumes aren't passed
48// any sizes. Blobfs can resize itself on the fvm, and the other two potential volumes are only
49// used in specific circumstances and are never formatted. The remaining volume size is just used
50// for calculation.
51pub const DEFAULT_F2FS_MIN_BYTES: u64 = 50 * 1024 * 1024;
52pub const DEFAULT_DATA_VOLUME_SIZE: u64 = DEFAULT_F2FS_MIN_BYTES;
53pub const BLOBFS_MAX_BYTES: u64 = 8765432;
54// For migration tests, we make sure that the default disk size is twice the data volume size to
55// allow a second full data partition.
56pub const DEFAULT_DISK_SIZE: u64 = DEFAULT_DATA_VOLUME_SIZE * 2 + BLOBFS_MAX_BYTES;
57
58// We use a static key-bag so that the crypt instance can be shared across test executions safely.
59// These keys match the DATA_KEY and METADATA_KEY respectively, when wrapped with the "zxcrypt"
60// static key used by fshost.
61// Note this isn't used in the legacy crypto format.
62const KEY_BAG_CONTENTS: &'static str = r#"
63{
64    "version":1,
65    "keys": {
66        "0":{
67            "Aes128GcmSivWrapped": [
68                "7a7c6a718cfde7078f6edec5",
69                "7cc31b765c74db3191e269d2666267022639e758fe3370e8f36c166d888586454fd4de8aeb47aadd81c531b0a0a66f27"
70            ]
71        },
72        "1":{
73            "Aes128GcmSivWrapped": [
74                "b7d7f459cbee4cc536cc4324",
75                "9f6a5d894f526b61c5c091e5e02a7ff94d18e6ad36a0aa439c86081b726eca79e6b60bd86ee5d86a20b3df98f5265a99"
76            ]
77        }
78    }
79}"#;
80
81async fn generate_keymint_file_contents() -> Vec<u8> {
82    with_keymint_service(|keymint, _| async move {
83        let keymint = keymint.into_proxy();
84        let key_info = b"fuchsia";
85        let key_blob = keymint.create_sealing_key(&key_info[..]).await.unwrap().unwrap();
86
87        let data_key_sealed =
88            keymint.seal(&key_info[..], &key_blob, DATA_KEY.deref()).await.unwrap().unwrap();
89        let metadata_key_sealed =
90            keymint.seal(&key_info[..], &key_blob, METADATA_KEY.deref()).await.unwrap().unwrap();
91
92        let json = json!({
93            "sealing_key_info": key_info,
94            "sealing_key_blob": key_blob,
95            "sealed_keys": {
96                "data.data": data_key_sealed,
97                "data.metadata": metadata_key_sealed,
98            }
99        });
100
101        Ok(serde_json::to_vec_pretty(&json).unwrap())
102    })
103    .await
104    .unwrap()
105}
106
107pub const TEST_BLOB_CONTENTS: [u8; 1000] = [1; 1000];
108
109pub fn test_blob_hash() -> fuchsia_merkle::Hash {
110    fuchsia_merkle::root_from_slice(&TEST_BLOB_CONTENTS)
111}
112
113const DATA_KEY: Aes256Key = Aes256Key::create([
114    0xcf, 0x9e, 0x45, 0x2a, 0x22, 0xa5, 0x70, 0x31, 0x33, 0x3b, 0x4d, 0x6b, 0x6f, 0x78, 0x58, 0x29,
115    0x04, 0x79, 0xc7, 0xd6, 0xa9, 0x4b, 0xce, 0x82, 0x04, 0x56, 0x5e, 0x82, 0xfc, 0xe7, 0x37, 0xa8,
116]);
117
118const METADATA_KEY: Aes256Key = Aes256Key::create([
119    0x0f, 0x4d, 0xca, 0x6b, 0x35, 0x0e, 0x85, 0x6a, 0xb3, 0x8c, 0xdd, 0xe9, 0xda, 0x0e, 0xc8, 0x22,
120    0x8e, 0xea, 0xd8, 0x05, 0xc4, 0xc9, 0x0b, 0xa8, 0xd8, 0x85, 0x87, 0x50, 0x75, 0x40, 0x1c, 0x4c,
121]);
122
123pub const FVM_PART_INSTANCE_GUID: [u8; 16] = [3u8; 16];
124pub const DEFAULT_TEST_TYPE_GUID: [u8; 16] = [
125    0x66, 0x73, 0x68, 0x6F, 0x73, 0x74, 0x20, 0x69, 0x6E, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x73,
126];
127
128async fn create_hermetic_crypt_service(
129    data_key: Aes256Key,
130    metadata_key: Aes256Key,
131) -> RealmInstance {
132    let builder = RealmBuilder::new().await.unwrap();
133    let url = "#meta/fxfs-crypt.cm";
134    let crypt = builder.add_child("fxfs-crypt", url, ChildOptions::new().eager()).await.unwrap();
135    builder
136        .add_route(
137            Route::new()
138                .capability(Capability::protocol::<CryptMarker>())
139                .capability(Capability::protocol::<CryptManagementMarker>())
140                .from(&crypt)
141                .to(Ref::parent()),
142        )
143        .await
144        .unwrap();
145    builder
146        .add_route(
147            Route::new()
148                .capability(Capability::protocol::<flogger::LogSinkMarker>())
149                .from(Ref::parent())
150                .to(&crypt),
151        )
152        .await
153        .unwrap();
154    let realm = builder.build().await.expect("realm build failed");
155    let crypt_management: CryptManagementProxy =
156        realm.root.connect_to_protocol_at_exposed_dir().unwrap();
157    let wrapping_key_id_0 = [0; 16];
158    let mut wrapping_key_id_1 = [0; 16];
159    wrapping_key_id_1[0] = 1;
160    crypt_management
161        .add_wrapping_key(&wrapping_key_id_0, data_key.deref())
162        .await
163        .unwrap()
164        .expect("add_wrapping_key failed");
165    crypt_management
166        .add_wrapping_key(&wrapping_key_id_1, metadata_key.deref())
167        .await
168        .unwrap()
169        .expect("add_wrapping_key failed");
170    crypt_management
171        .set_active_key(KeyPurpose::Data, &wrapping_key_id_0)
172        .await
173        .unwrap()
174        .expect("set_active_key failed");
175    crypt_management
176        .set_active_key(KeyPurpose::Metadata, &wrapping_key_id_1)
177        .await
178        .unwrap()
179        .expect("set_active_key failed");
180    realm
181}
182
183/// Write a blob to the blob volume to ensure that on format, the blob volume does not get wiped.
184pub async fn write_blob(blob_creator: BlobCreatorProxy, data: &[u8]) -> Hash {
185    let hash = fuchsia_merkle::root_from_slice(data);
186    let compressed_data = Type1Blob::generate(&data, CompressionMode::Always);
187
188    let blob_writer_client_end = blob_creator
189        .create(&hash.into(), false)
190        .await
191        .expect("transport error on create")
192        .expect("failed to create blob");
193
194    let writer = blob_writer_client_end.into_proxy();
195    let mut blob_writer = BlobWriter::create(writer, compressed_data.len() as u64)
196        .await
197        .expect("failed to create BlobWriter");
198    blob_writer.write(&compressed_data).await.unwrap();
199    hash
200}
201
202pub enum Disk {
203    Prebuilt(zx::Vmo, Option<[u8; 16]>),
204    Builder(DiskBuilder),
205}
206
207impl Disk {
208    pub async fn into_vmo_and_type_guid(self) -> (zx::Vmo, Option<[u8; 16]>) {
209        match self {
210            Disk::Prebuilt(vmo, guid) => (vmo, guid),
211            Disk::Builder(builder) => builder.build().await,
212        }
213    }
214
215    pub fn builder(&mut self) -> &mut DiskBuilder {
216        match self {
217            Disk::Prebuilt(..) => panic!("attempted to get builder for prebuilt disk"),
218            Disk::Builder(builder) => builder,
219        }
220    }
221}
222
223#[derive(Debug)]
224pub struct DataSpec {
225    pub format: Option<&'static str>,
226    pub zxcrypt: bool,
227    pub crypt_policy: crypt_policy::Policy,
228}
229
230impl Default for DataSpec {
231    fn default() -> Self {
232        Self {
233            format: Default::default(),
234            zxcrypt: Default::default(),
235            crypt_policy: crypt_policy::Policy::Null,
236        }
237    }
238}
239
240#[derive(Debug)]
241pub struct VolumesSpec {
242    pub fxfs_blob: bool,
243    pub create_data_partition: bool,
244}
245
246enum FxfsType {
247    Fxfs(Box<dyn BlockConnector>),
248    FxBlob(ServingMultiVolumeFilesystem, RealmInstance),
249}
250
251#[derive(Debug)]
252pub struct DiskBuilder {
253    size: u64,
254    // Overrides all other options.  The disk will be unformatted.
255    uninitialized: bool,
256    blob_hash: Option<Hash>,
257    data_volume_size: u64,
258    fvm_slice_size: u64,
259    data_spec: DataSpec,
260    volumes_spec: VolumesSpec,
261    // Only used if `format` is Some.
262    corrupt_data: bool,
263    gpt: bool,
264    extra_volumes: Vec<&'static str>,
265    extra_gpt_partitions: Vec<(&'static str, u64)>,
266    // Note: fvm also means fxfs acting as the volume manager when using fxblob.
267    format_volume_manager: bool,
268    legacy_data_label: bool,
269    // Only used if 'fs_switch' set.
270    fs_switch: Option<String>,
271    // The type guid of the ramdisk when it's created for the test fshost.
272    type_guid: Option<[u8; 16]>,
273    system_partition_label: &'static str,
274}
275
276impl DiskBuilder {
277    pub fn uninitialized() -> DiskBuilder {
278        Self { uninitialized: true, type_guid: None, ..Self::new() }
279    }
280
281    pub fn new() -> DiskBuilder {
282        DiskBuilder {
283            size: DEFAULT_DISK_SIZE,
284            uninitialized: false,
285            blob_hash: None,
286            data_volume_size: DEFAULT_DATA_VOLUME_SIZE,
287            fvm_slice_size: FVM_SLICE_SIZE,
288            data_spec: DataSpec::default(),
289            volumes_spec: VolumesSpec { fxfs_blob: false, create_data_partition: true },
290            corrupt_data: false,
291            gpt: false,
292            extra_volumes: Vec::new(),
293            extra_gpt_partitions: Vec::new(),
294            format_volume_manager: true,
295            legacy_data_label: false,
296            fs_switch: None,
297            type_guid: Some(DEFAULT_TEST_TYPE_GUID),
298            system_partition_label: "fvm",
299        }
300    }
301
302    pub fn set_uninitialized(&mut self) -> &mut Self {
303        self.uninitialized = true;
304        self
305    }
306
307    pub fn size(&mut self, size: u64) -> &mut Self {
308        self.size = size;
309        self
310    }
311
312    pub fn data_volume_size(&mut self, data_volume_size: u64) -> &mut Self {
313        self.data_volume_size = data_volume_size;
314        // Increase the size of the disk if required. NB: We don't decrease the size of the disk
315        // because some tests set a lower initial size and expect to be able to resize to a larger
316        // one.
317        self.size = self.size.max(self.data_volume_size + BLOBFS_MAX_BYTES);
318        self
319    }
320
321    pub fn format_volumes(&mut self, volumes_spec: VolumesSpec) -> &mut Self {
322        self.volumes_spec = volumes_spec;
323        self
324    }
325
326    pub fn format_data(&mut self, data_spec: DataSpec) -> &mut Self {
327        log::info!(data_spec:?; "formatting data volume");
328        if !self.volumes_spec.fxfs_blob {
329            assert!(self.format_volume_manager);
330        } else {
331            if let Some(format) = data_spec.format {
332                assert_eq!(format, "fxfs");
333            }
334        }
335        if data_spec.format == Some("f2fs") {
336            self.fvm_slice_size = FVM_F2FS_SLICE_SIZE;
337        }
338        self.data_spec = data_spec;
339        self
340    }
341
342    pub fn set_fs_switch(&mut self, content: &str) -> &mut Self {
343        self.fs_switch = Some(content.to_string());
344        self
345    }
346
347    pub fn corrupt_data(&mut self) -> &mut Self {
348        self.corrupt_data = true;
349        self
350    }
351
352    pub fn with_gpt(&mut self) -> &mut Self {
353        self.gpt = true;
354        // The system partition matcher expects the type guid to be either None or completely zero,
355        // so if we are formatting with gpt we clear any type guid.
356        self.type_guid = None;
357        self
358    }
359
360    pub fn with_system_partition_label(&mut self, label: &'static str) -> &mut Self {
361        self.system_partition_label = label;
362        self
363    }
364
365    /// Appends an additional GPT partition.  The partitions are contiguous in the inserted order,
366    /// and the first one is immediately after the system partition.
367    pub fn with_extra_gpt_partition(
368        &mut self,
369        volume_name: &'static str,
370        num_blocks: u64,
371    ) -> &mut Self {
372        self.extra_gpt_partitions.push((volume_name, num_blocks));
373        self
374    }
375
376    pub fn with_extra_volume(&mut self, volume_name: &'static str) -> &mut Self {
377        self.extra_volumes.push(volume_name);
378        self
379    }
380
381    pub fn with_unformatted_volume_manager(&mut self) -> &mut Self {
382        assert!(self.data_spec.format.is_none());
383        self.format_volume_manager = false;
384        self
385    }
386
387    pub fn with_legacy_data_label(&mut self) -> &mut Self {
388        self.legacy_data_label = true;
389        self
390    }
391
392    pub async fn build(mut self) -> (zx::Vmo, Option<[u8; 16]>) {
393        log::info!("building disk: {:?}", self);
394        assert_eq!(
395            self.data_volume_size % self.fvm_slice_size,
396            0,
397            "data_volume_size {} needs to be a multiple of fvm slice size {}",
398            self.data_volume_size,
399            self.fvm_slice_size
400        );
401        if self.data_spec.format == Some("f2fs") {
402            assert!(self.data_volume_size >= DEFAULT_F2FS_MIN_BYTES);
403        }
404
405        let vmo = zx::Vmo::create(self.size).unwrap();
406
407        if self.uninitialized {
408            return (vmo, self.type_guid);
409        }
410
411        let server = Arc::new(VmoBackedServer::from_vmo(
412            TEST_DISK_BLOCK_SIZE,
413            vmo.duplicate_handle(zx::Rights::SAME_RIGHTS).unwrap(),
414        ));
415
416        if self.gpt {
417            // Format the disk with gpt, with a single empty partition named "fvm".
418            let client = Arc::new(
419                RemoteBlockClient::new(server.connect::<fblock::BlockProxy>()).await.unwrap(),
420            );
421            assert!(self.extra_gpt_partitions.len() < 10);
422            let fvm_num_blocks = self.size / TEST_DISK_BLOCK_SIZE as u64 - 138;
423            let mut start_block = 64;
424            let mut partitions = vec![gpt::PartitionInfo {
425                label: self.system_partition_label.to_string(),
426                type_guid: gpt::Guid::from_bytes(FVM_TYPE_GUID),
427                instance_guid: gpt::Guid::from_bytes(FVM_PART_INSTANCE_GUID),
428                start_block,
429                num_blocks: fvm_num_blocks,
430                flags: 0,
431            }];
432            start_block = start_block + fvm_num_blocks;
433            for (extra_partition, num_blocks) in &self.extra_gpt_partitions {
434                partitions.push(gpt::PartitionInfo {
435                    label: extra_partition.to_string(),
436                    type_guid: gpt::Guid::from_bytes(DEFAULT_TEST_TYPE_GUID),
437                    instance_guid: gpt::Guid::from_bytes(FVM_PART_INSTANCE_GUID),
438                    start_block,
439                    num_blocks: *num_blocks,
440                    flags: 0,
441                });
442                start_block += num_blocks;
443            }
444            let _ = gpt::Gpt::format(client, partitions).await.expect("gpt format failed");
445        }
446
447        if !self.format_volume_manager {
448            return (vmo, self.type_guid);
449        }
450
451        let mut gpt = None;
452        let connector: Box<dyn BlockConnector> = if self.gpt {
453            // Format the volume manager in the gpt partition named "fvm".
454            let partitions_dir = vfs::directory::immutable::simple();
455            let manager = GptManager::new(server.connect(), partitions_dir.clone()).await.unwrap();
456            let dir =
457                vfs::directory::serve(partitions_dir, fio::PERM_READABLE | fio::PERM_WRITABLE);
458            gpt = Some((manager, fuchsia_fs::directory::clone(&dir).unwrap()));
459            Box::new(DirBasedBlockConnector::new(dir, "part-000/volume".to_string()))
460        } else {
461            // Format the volume manager onto the disk directly.
462            Box::new(move |server_end| Ok(server.connect_server(server_end)))
463        };
464
465        if self.volumes_spec.fxfs_blob {
466            self.build_fxfs_as_volume_manager(connector).await;
467        } else {
468            self.build_fvm_as_volume_manager(connector).await;
469        }
470        if let Some((gpt, partitions_dir)) = gpt {
471            partitions_dir.close().await.unwrap().unwrap();
472            gpt.shutdown().await;
473        }
474        (vmo, self.type_guid)
475    }
476
477    pub(crate) async fn build_fxfs_as_volume_manager(
478        &mut self,
479        connector: Box<dyn BlockConnector>,
480    ) {
481        let crypt_realm = create_hermetic_crypt_service(DATA_KEY, METADATA_KEY).await;
482        let mut fxfs = Filesystem::from_boxed_config(connector, Box::new(Fxfs::default()));
483        // Wipes the device
484        fxfs.format().await.expect("format failed");
485        let fs = fxfs.serve_multi_volume().await.expect("serve_multi_volume failed");
486        let blob_volume = fs
487            .create_volume(
488                "blob",
489                CreateOptions::default(),
490                MountOptions { as_blob: Some(true), ..MountOptions::default() },
491            )
492            .await
493            .expect("failed to create blob volume");
494        let blob_creator = connect_to_protocol_at_dir_svc::<fidl_fuchsia_fxfs::BlobCreatorMarker>(
495            blob_volume.exposed_dir(),
496        )
497        .expect("failed to connect to the Blob service");
498        self.blob_hash = Some(write_blob(blob_creator, &TEST_BLOB_CONTENTS).await);
499
500        for volume in &self.extra_volumes {
501            fs.create_volume(volume, CreateOptions::default(), MountOptions::default())
502                .await
503                .expect("failed to make extra fxfs volume");
504        }
505
506        if self.data_spec.format.is_some() {
507            self.init_data_fxfs(FxfsType::FxBlob(fs, crypt_realm), self.data_spec.crypt_policy)
508                .await;
509        } else {
510            fs.shutdown().await.expect("shutdown failed");
511        }
512    }
513
514    async fn build_fvm_as_volume_manager(&mut self, connector: Box<dyn BlockConnector>) {
515        let block_device = connector.connect_block().unwrap().into_proxy();
516        let fvm_slice_size = self.fvm_slice_size;
517        fasync::unblock(move || format_for_fvm(&block_device, fvm_slice_size as usize))
518            .await
519            .unwrap();
520        let fvm_fs = Filesystem::from_boxed_config(connector, Box::new(Fvm::dynamic_child()));
521        let fvm = fvm_fs.serve_multi_volume().await.unwrap();
522
523        {
524            let blob_volume = fvm
525                .create_volume(
526                    "blobfs",
527                    CreateOptions {
528                        type_guid: Some(BLOBFS_TYPE_GUID),
529                        guid: Some(Uuid::new_v4().into_bytes()),
530                        ..Default::default()
531                    },
532                    MountOptions {
533                        uri: Some(String::from("#meta/blobfs.cm")),
534                        ..Default::default()
535                    },
536                )
537                .await
538                .expect("failed to make fvm blobfs volume");
539            let blob_creator =
540                connect_to_protocol_at_dir_svc::<fidl_fuchsia_fxfs::BlobCreatorMarker>(
541                    blob_volume.exposed_dir(),
542                )
543                .expect("failed to connect to the Blob service");
544            self.blob_hash = Some(write_blob(blob_creator, &TEST_BLOB_CONTENTS).await);
545
546            blob_volume.shutdown().await.unwrap();
547        }
548
549        if self.volumes_spec.create_data_partition {
550            let data_label = if self.legacy_data_label { "minfs" } else { "data" };
551
552            let _crypt_service;
553            let crypt = if self.data_spec.format != Some("fxfs") && self.data_spec.zxcrypt {
554                let (crypt, stream) = fidl::endpoints::create_request_stream();
555                _crypt_service = fasync::Task::spawn(zxcrypt_crypt::run_crypt_service(
556                    crypt_policy::Policy::Null,
557                    stream,
558                ));
559                Some(crypt)
560            } else {
561                None
562            };
563            let uri = match (&self.data_spec.format, self.corrupt_data) {
564                (None, _) => None,
565                (_, true) => None,
566                (Some("fxfs"), false) => None,
567                (Some("minfs"), false) => Some(String::from("#meta/minfs.cm")),
568                (Some("f2fs"), false) => Some(String::from("#meta/f2fs.cm")),
569                (Some(format), _) => panic!("unsupported data volume format '{}'", format),
570            };
571
572            let data_volume = fvm
573                .create_volume(
574                    data_label,
575                    CreateOptions {
576                        initial_size: Some(self.data_volume_size),
577                        type_guid: Some(DATA_TYPE_GUID),
578                        guid: Some(Uuid::new_v4().into_bytes()),
579                        ..Default::default()
580                    },
581                    MountOptions { crypt, uri, ..Default::default() },
582                )
583                .await
584                .unwrap();
585
586            if self.corrupt_data {
587                let volume_proxy = connect_to_protocol_at_dir_svc::<
588                    fidl_fuchsia_storage_block::BlockMarker,
589                >(data_volume.exposed_dir())
590                .unwrap();
591                match self.data_spec.format {
592                    Some("fxfs") => self.write_magic(volume_proxy, FXFS_MAGIC, 0).await,
593                    Some("minfs") => self.write_magic(volume_proxy, MINFS_MAGIC, 0).await,
594                    Some("f2fs") => self.write_magic(volume_proxy, F2FS_MAGIC, 1024).await,
595                    _ => (),
596                }
597            } else if self.data_spec.format == Some("fxfs") {
598                let dir = fuchsia_fs::directory::clone(data_volume.exposed_dir()).unwrap();
599                self.init_data_fxfs(
600                    FxfsType::Fxfs(Box::new(DirBasedBlockConnector::new(
601                        dir,
602                        String::from("svc/fuchsia.storage.block.Block"),
603                    ))),
604                    self.data_spec.crypt_policy,
605                )
606                .await
607            } else if self.data_spec.format.is_some() {
608                self.write_test_data(data_volume.root()).await;
609                data_volume.shutdown().await.unwrap();
610            }
611        }
612
613        for volume in &self.extra_volumes {
614            fvm.create_volume(
615                volume,
616                CreateOptions {
617                    type_guid: Some(DATA_TYPE_GUID),
618                    guid: Some(Uuid::new_v4().into_bytes()),
619                    ..Default::default()
620                },
621                MountOptions::default(),
622            )
623            .await
624            .expect("failed to make extra fvm volume");
625        }
626
627        fvm.shutdown().await.expect("fvm shutdown failed");
628    }
629
630    async fn init_data_fxfs(&self, fxfs: FxfsType, crypt_policy: crypt_policy::Policy) {
631        let mut fxblob = false;
632        let (fs, crypt_realm) = match fxfs {
633            FxfsType::Fxfs(connector) => {
634                let crypt_realm = create_hermetic_crypt_service(DATA_KEY, METADATA_KEY).await;
635                let mut fxfs =
636                    Filesystem::from_boxed_config(connector, Box::new(Fxfs::dynamic_child()));
637                fxfs.format().await.expect("format failed");
638                (fxfs.serve_multi_volume().await.expect("serve_multi_volume failed"), crypt_realm)
639            }
640            FxfsType::FxBlob(fs, crypt_realm) => {
641                fxblob = true;
642                (fs, crypt_realm)
643            }
644        };
645
646        let vol = {
647            let vol = fs
648                .create_volume("unencrypted", CreateOptions::default(), MountOptions::default())
649                .await
650                .expect("create_volume failed");
651            let keys_dir = fuchsia_fs::directory::create_directory(
652                vol.root(),
653                "keys",
654                fio::PERM_READABLE | fio::PERM_WRITABLE,
655            )
656            .await
657            .unwrap();
658            if let crypt_policy::Policy::Keymint = crypt_policy {
659                let keymint_file = fuchsia_fs::directory::open_file(
660                    &keys_dir,
661                    "keymint.0",
662                    fio::Flags::FLAG_MAYBE_CREATE
663                        | fio::Flags::PROTOCOL_FILE
664                        | fio::PERM_READABLE
665                        | fio::PERM_WRITABLE,
666                )
667                .await
668                .unwrap();
669                let contents = generate_keymint_file_contents().await;
670                let mut contents_ref = contents.as_slice();
671                if self.corrupt_data && fxblob {
672                    contents_ref = &TEST_BLOB_CONTENTS;
673                }
674                fuchsia_fs::file::write(&keymint_file, contents_ref).await.unwrap();
675                fuchsia_fs::file::close(keymint_file).await.unwrap();
676            } else {
677                let keys_file = fuchsia_fs::directory::open_file(
678                    &keys_dir,
679                    "fxfs-data",
680                    fio::Flags::FLAG_MAYBE_CREATE
681                        | fio::Flags::PROTOCOL_FILE
682                        | fio::PERM_READABLE
683                        | fio::PERM_WRITABLE,
684                )
685                .await
686                .unwrap();
687                let mut key_bag = KEY_BAG_CONTENTS.as_bytes();
688                if self.corrupt_data && fxblob {
689                    key_bag = &TEST_BLOB_CONTENTS;
690                }
691                fuchsia_fs::file::write(&keys_file, key_bag).await.unwrap();
692                fuchsia_fs::file::close(keys_file).await.unwrap();
693            }
694            fuchsia_fs::directory::close(keys_dir).await.unwrap();
695
696            let crypt = Some(
697                crypt_realm
698                    .root
699                    .connect_to_protocol_at_exposed_dir()
700                    .expect("Unable to connect to Crypt service"),
701            );
702            fs.create_volume(
703                "data",
704                CreateOptions::default(),
705                MountOptions { crypt, ..MountOptions::default() },
706            )
707            .await
708            .expect("create_volume failed")
709        };
710        self.write_test_data(&vol.root()).await;
711        fs.shutdown().await.expect("shutdown failed");
712    }
713
714    /// Create a small set of known files to test for presence. The test tree is
715    ///  root
716    ///   |- .testdata (file, empty)
717    ///   |- ssh (directory, non-empty)
718    ///   |   |- authorized_keys (file, non-empty)
719    ///   |   |- config (directory, empty)
720    ///   |- problems (directory, empty (no problems))
721    async fn write_test_data(&self, root: &fio::DirectoryProxy) {
722        fuchsia_fs::directory::open_file(
723            root,
724            ".testdata",
725            fio::Flags::FLAG_MAYBE_CREATE | fio::PERM_READABLE,
726        )
727        .await
728        .unwrap();
729
730        let ssh_dir = fuchsia_fs::directory::create_directory(
731            root,
732            "ssh",
733            fio::PERM_READABLE | fio::PERM_WRITABLE,
734        )
735        .await
736        .unwrap();
737        let authorized_keys = fuchsia_fs::directory::open_file(
738            &ssh_dir,
739            "authorized_keys",
740            fio::Flags::FLAG_MAYBE_CREATE | fio::PERM_READABLE | fio::PERM_WRITABLE,
741        )
742        .await
743        .unwrap();
744        fuchsia_fs::file::write(&authorized_keys, "public key!").await.unwrap();
745        fuchsia_fs::directory::create_directory(&ssh_dir, "config", fio::PERM_READABLE)
746            .await
747            .unwrap();
748
749        fuchsia_fs::directory::create_directory(&root, "problems", fio::PERM_READABLE)
750            .await
751            .unwrap();
752
753        if let Some(content) = &self.fs_switch {
754            let fs_switch = fuchsia_fs::directory::open_file(
755                &root,
756                "fs_switch",
757                fio::Flags::FLAG_MAYBE_CREATE | fio::PERM_READABLE | fio::PERM_WRITABLE,
758            )
759            .await
760            .unwrap();
761            fuchsia_fs::file::write(&fs_switch, content).await.unwrap();
762        }
763    }
764
765    async fn write_magic<const N: usize>(
766        &self,
767        volume_proxy: fblock::BlockProxy,
768        value: [u8; N],
769        offset: u64,
770    ) {
771        let client = block_client::RemoteBlockClient::new(volume_proxy)
772            .await
773            .expect("Failed to create client");
774        let block_size = client.block_size() as usize;
775        assert!(value.len() <= block_size);
776        let mut data = vec![0xffu8; block_size];
777        data[..value.len()].copy_from_slice(&value);
778        let buffer = block_client::BufferSlice::Memory(&data[..]);
779        client.write_at(buffer, offset).await.expect("write failed");
780    }
781
782    /// Create a vmo artifact with the format of a compressed zbi boot item containing this
783    /// filesystem.
784    pub(crate) async fn build_as_zbi_ramdisk(self) -> zx::Vmo {
785        /// The following types and constants are defined in
786        /// sdk/lib/zbi-format/include/lib/zbi-format/zbi.h.
787        const ZBI_TYPE_STORAGE_RAMDISK: u32 = 0x4b534452;
788        const ZBI_FLAGS_VERSION: u32 = 0x00010000;
789        const ZBI_ITEM_MAGIC: u32 = 0xb5781729;
790        const ZBI_FLAGS_STORAGE_COMPRESSED: u32 = 0x00000001;
791
792        #[repr(C)]
793        #[derive(IntoBytes, Immutable)]
794        struct ZbiHeader {
795            type_: u32,
796            length: u32,
797            extra: u32,
798            flags: u32,
799            _reserved0: u32,
800            _reserved1: u32,
801            magic: u32,
802            _crc32: u32,
803        }
804
805        let (ramdisk_vmo, _) = self.build().await;
806        let extra = ramdisk_vmo.get_size().unwrap() as u32;
807        let mut decompressed_buf = vec![0u8; extra as usize];
808        ramdisk_vmo.read(&mut decompressed_buf, 0).unwrap();
809        let compressed_buf = zstd::encode_all(decompressed_buf.as_slice(), 0).unwrap();
810        let length = compressed_buf.len() as u32;
811
812        let header = ZbiHeader {
813            type_: ZBI_TYPE_STORAGE_RAMDISK,
814            length,
815            extra,
816            flags: ZBI_FLAGS_VERSION | ZBI_FLAGS_STORAGE_COMPRESSED,
817            _reserved0: 0,
818            _reserved1: 0,
819            magic: ZBI_ITEM_MAGIC,
820            _crc32: 0,
821        };
822
823        let header_size = std::mem::size_of::<ZbiHeader>() as u64;
824        let zbi_vmo = zx::Vmo::create(header_size + length as u64).unwrap();
825        zbi_vmo.write(header.as_bytes(), 0).unwrap();
826        zbi_vmo.write(&compressed_buf, header_size).unwrap();
827
828        zbi_vmo
829    }
830}
831
832/// Helper function to return a set of the volumes found within a given `disk`, which must have a
833/// valid GPT containing an fxfs partition in the first entry.
834pub async fn list_all_fxfs_volumes(disk: &Disk) -> HashSet<String> {
835    let Disk::Prebuilt(vmo, _) = disk else {
836        panic!("list_all_fxfs_volumes only supports prebuilt disks");
837    };
838    let server = Arc::new(VmoBackedServer::from_vmo(
839        TEST_DISK_BLOCK_SIZE,
840        vmo.duplicate_handle(zx::Rights::SAME_RIGHTS).unwrap(),
841    ));
842
843    let partitions_dir = vfs::directory::immutable::simple();
844    let manager = GptManager::new(server.connect(), partitions_dir.clone()).await.unwrap();
845    let dir =
846        vfs::directory::serve(partitions_dir.clone(), fio::PERM_READABLE | fio::PERM_WRITABLE);
847
848    let partitions = fuchsia_fs::directory::readdir(&dir).await.unwrap().into_iter().map(|entry| {
849        let dir = fuchsia_fs::directory::clone(&dir).unwrap();
850        let name = entry.name;
851        DirBasedBlockConnector::new(dir, format!("{name}/volume"))
852    });
853    let connector = find_block_device(
854        &[
855            BlockDeviceMatcher::TypeGuid(&FVM_TYPE_GUID),
856            BlockDeviceMatcher::InstanceGuid(&FVM_PART_INSTANCE_GUID),
857        ],
858        partitions,
859    )
860    .await
861    .expect("failed to match partition")
862    .expect("did not match fxfs partition");
863
864    let fxfs = Filesystem::from_boxed_config(Box::new(connector), Box::new(Fxfs::default()));
865    let fs = fxfs.serve_multi_volume().await.expect("serve_multi_volume failed");
866    let volumes = fs.list_volumes().await.expect("list_volumes failed");
867    fs.shutdown().await.expect("shutdown failed");
868    manager.shutdown().await;
869    volumes.into_iter().collect()
870}
871
872/// Returns the set of volume names expected for an fxblob-based system.
873pub fn expected_fxblob_volumes() -> HashSet<String> {
874    ["blob", "data", "unencrypted"].into_iter().map(str::to_owned).collect()
875}