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