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