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