Skip to main content

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