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::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 24MiB, with 8MiB dedicated to the data volume. When formatting
41// f2fs, DiskBuilder::format_data increases the data volume size to DEFAULT_F2FS_MIN_BYTES (50MiB),
42// which has a relatively large minimum size requirement to be formatted.
43//
44// Only the data volume is actually created with a specific size, the other volumes aren't passed
45// any sizes. Blobfs can resize itself on the fvm, and the other two potential volumes are only
46// used in specific circumstances and are never formatted. The remaining volume size is just used
47// for calculation.
48pub const DEFAULT_F2FS_MIN_BYTES: u64 = 50 * 1024 * 1024;
49pub const DEFAULT_DATA_VOLUME_SIZE: u64 = 8 * 1024 * 1024;
50pub const BLOBFS_MAX_BYTES: u64 = 8765432;
51// For migration tests, we make sure that the default disk size is twice the data volume size to
52// allow a second full data partition.
53pub const DEFAULT_DISK_SIZE: u64 = DEFAULT_DATA_VOLUME_SIZE * 2 + BLOBFS_MAX_BYTES;
54
55// We use a static key-bag so that the crypt instance can be shared across test executions safely.
56// These keys match the DATA_KEY and METADATA_KEY respectively, when wrapped with the "zxcrypt"
57// static key used by fshost.
58// Note this isn't used in the legacy crypto format.
59const KEY_BAG_CONTENTS: &'static str = r#"
60{
61    "version":1,
62    "keys": {
63        "0":{
64            "Aes128GcmSivWrapped": [
65                "7a7c6a718cfde7078f6edec5",
66                "7cc31b765c74db3191e269d2666267022639e758fe3370e8f36c166d888586454fd4de8aeb47aadd81c531b0a0a66f27"
67            ]
68        },
69        "1":{
70            "Aes128GcmSivWrapped": [
71                "b7d7f459cbee4cc536cc4324",
72                "9f6a5d894f526b61c5c091e5e02a7ff94d18e6ad36a0aa439c86081b726eca79e6b60bd86ee5d86a20b3df98f5265a99"
73            ]
74        }
75    }
76}"#;
77
78async fn generate_keymint_file_contents(
79    old_blob: Option<&[u8]>,
80    custom_keymint: Option<Arc<FakeKeymint>>,
81) -> Vec<u8> {
82    let serve_keymint = |keymint_proxy: fidl_fuchsia_security_keymint::SealingKeysProxy| async move {
83        let key_info = b"fuchsia";
84        let key_blob = keymint_proxy.create_sealing_key(&key_info[..]).await.unwrap().unwrap();
85        assert!(!key_blob.is_empty());
86
87        let data_key_sealed =
88            keymint_proxy.seal(&key_info[..], &key_blob, DATA_KEY.deref()).await.unwrap().unwrap();
89        let metadata_key_sealed = keymint_proxy
90            .seal(&key_info[..], &key_blob, METADATA_KEY.deref())
91            .await
92            .unwrap()
93            .unwrap();
94
95        let mut json = json!({
96            "sealing_key_info": key_info,
97            "sealing_key_blob": key_blob,
98            "sealed_keys": {
99                "data.data": data_key_sealed,
100                "data.metadata": metadata_key_sealed,
101            }
102        });
103
104        if let Some(old) = old_blob {
105            json.as_object_mut().unwrap().insert(
106                "old_blob".to_string(),
107                serde_json::Value::Array(
108                    old.iter().map(|&b| serde_json::Value::Number(b.into())).collect(),
109                ),
110            );
111        }
112
113        serde_json::to_vec_pretty(&json).unwrap()
114    };
115
116    if let Some(fake_keymint) = custom_keymint {
117        // When we have a custom injected mock FakeKeymint, we don't need to spin up FIDL proxies
118        // and background tasks. The mock natively generates and registers its own bytes.
119        let key_info = b"fuchsia".to_vec();
120
121        // We simulate what the hardware would do during an initial "seal" of data and
122        // metadata keys.
123        let key_blob = fake_keymint.generate_static_sealing_key(&key_info);
124
125        let data_key_sealed =
126            fake_keymint.generate_static_sealed_data(&key_info, &key_blob, DATA_KEY.deref());
127        let metadata_key_sealed =
128            fake_keymint.generate_static_sealed_data(&key_info, &key_blob, METADATA_KEY.deref());
129
130        let mut json = json!({
131            "sealing_key_info": key_info,
132            "sealing_key_blob": key_blob,
133            "sealed_keys": {
134                "data.data": data_key_sealed,
135                "data.metadata": metadata_key_sealed,
136            }
137        });
138
139        if let Some(old) = old_blob {
140            json.as_object_mut().unwrap().insert(
141                "old_blob".to_string(),
142                serde_json::Value::Array(
143                    old.iter().map(|&b| serde_json::Value::Number(b.into())).collect(),
144                ),
145            );
146        }
147
148        serde_json::to_vec_pretty(&json).unwrap()
149    } else {
150        with_keymint_service(|proxy, _| async move { Ok(serve_keymint(proxy.into_proxy()).await) })
151            .await
152            .unwrap()
153    }
154}
155
156pub const TEST_BLOB_CONTENTS: [u8; 1000] = [1; 1000];
157
158pub fn test_blob_hash() -> fuchsia_merkle::Hash {
159    fuchsia_merkle::root_from_slice(&TEST_BLOB_CONTENTS)
160}
161
162const DATA_KEY: Aes256Key = Aes256Key::create([
163    0xcf, 0x9e, 0x45, 0x2a, 0x22, 0xa5, 0x70, 0x31, 0x33, 0x3b, 0x4d, 0x6b, 0x6f, 0x78, 0x58, 0x29,
164    0x04, 0x79, 0xc7, 0xd6, 0xa9, 0x4b, 0xce, 0x82, 0x04, 0x56, 0x5e, 0x82, 0xfc, 0xe7, 0x37, 0xa8,
165]);
166
167const METADATA_KEY: Aes256Key = Aes256Key::create([
168    0x0f, 0x4d, 0xca, 0x6b, 0x35, 0x0e, 0x85, 0x6a, 0xb3, 0x8c, 0xdd, 0xe9, 0xda, 0x0e, 0xc8, 0x22,
169    0x8e, 0xea, 0xd8, 0x05, 0xc4, 0xc9, 0x0b, 0xa8, 0xd8, 0x85, 0x87, 0x50, 0x75, 0x40, 0x1c, 0x4c,
170]);
171
172pub const FVM_PART_INSTANCE_GUID: [u8; 16] = [3u8; 16];
173pub const DEFAULT_TEST_TYPE_GUID: [u8; 16] = [
174    0x66, 0x73, 0x68, 0x6F, 0x73, 0x74, 0x20, 0x69, 0x6E, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x73,
175];
176
177async fn create_hermetic_crypt_service(
178    data_key: Aes256Key,
179    metadata_key: Aes256Key,
180) -> RealmInstance {
181    let builder = RealmBuilder::new().await.unwrap();
182    let url = "#meta/fxfs-crypt.cm";
183    let crypt = builder.add_child("fxfs-crypt", url, ChildOptions::new().eager()).await.unwrap();
184    builder
185        .add_route(
186            Route::new()
187                .capability(Capability::protocol::<CryptMarker>())
188                .capability(Capability::protocol::<CryptManagementMarker>())
189                .from(&crypt)
190                .to(Ref::parent()),
191        )
192        .await
193        .unwrap();
194    builder
195        .add_route(
196            Route::new()
197                .capability(Capability::protocol::<flogger::LogSinkMarker>())
198                .from(Ref::parent())
199                .to(&crypt),
200        )
201        .await
202        .unwrap();
203    let realm = builder.build().await.expect("realm build failed");
204    let crypt_management: CryptManagementProxy =
205        realm.root.connect_to_protocol_at_exposed_dir().unwrap();
206    let wrapping_key_id_0 = [0; 16];
207    let mut wrapping_key_id_1 = [0; 16];
208    wrapping_key_id_1[0] = 1;
209    crypt_management
210        .add_wrapping_key(&wrapping_key_id_0, data_key.deref())
211        .await
212        .unwrap()
213        .expect("add_wrapping_key failed");
214    crypt_management
215        .add_wrapping_key(&wrapping_key_id_1, metadata_key.deref())
216        .await
217        .unwrap()
218        .expect("add_wrapping_key failed");
219    crypt_management
220        .set_active_key(KeyPurpose::Data, &wrapping_key_id_0)
221        .await
222        .unwrap()
223        .expect("set_active_key failed");
224    crypt_management
225        .set_active_key(KeyPurpose::Metadata, &wrapping_key_id_1)
226        .await
227        .unwrap()
228        .expect("set_active_key failed");
229    realm
230}
231
232/// Write a blob to the blob volume to ensure that on format, the blob volume does not get wiped.
233pub async fn write_blob(blob_creator: BlobCreatorProxy, data: &[u8]) -> Hash {
234    let hash = fuchsia_merkle::root_from_slice(data);
235    let compressed_data = Type1Blob::generate(&data, CompressionMode::Always);
236
237    let blob_writer_client_end = blob_creator
238        .create(&hash.into(), false)
239        .await
240        .expect("transport error on create")
241        .expect("failed to create blob");
242
243    let writer = blob_writer_client_end.into_proxy();
244    let mut blob_writer = BlobWriter::create(writer, compressed_data.len() as u64)
245        .await
246        .expect("failed to create BlobWriter");
247    blob_writer.write(&compressed_data).await.unwrap();
248    hash
249}
250
251#[allow(clippy::large_enum_variant)]
252pub enum Disk {
253    Prebuilt(zx::Vmo, Option<[u8; 16]>),
254    Builder(DiskBuilder),
255}
256
257impl Disk {
258    pub async fn into_vmo_and_type_guid(self) -> (zx::Vmo, Option<[u8; 16]>) {
259        match self {
260            Disk::Prebuilt(vmo, guid) => (vmo, guid),
261            Disk::Builder(builder) => builder.build().await,
262        }
263    }
264
265    pub fn builder(&mut self) -> &mut DiskBuilder {
266        match self {
267            Disk::Prebuilt(..) => panic!("attempted to get builder for prebuilt disk"),
268            Disk::Builder(builder) => builder,
269        }
270    }
271}
272
273#[derive(Debug)]
274pub struct DataSpec {
275    pub format: Option<&'static str>,
276    pub zxcrypt: bool,
277    pub crypt_policy: crypt_policy::Policy,
278}
279
280impl Default for DataSpec {
281    fn default() -> Self {
282        Self {
283            format: Default::default(),
284            zxcrypt: Default::default(),
285            crypt_policy: crypt_policy::Policy::Null,
286        }
287    }
288}
289
290#[derive(Debug)]
291pub struct VolumesSpec {
292    pub fxfs_blob: bool,
293    pub create_data_partition: bool,
294}
295
296enum FxfsType {
297    Fxfs(Box<dyn BlockConnector>),
298    FxBlob(ServingMultiVolumeFilesystem, RealmInstance),
299}
300
301pub struct DiskBuilder {
302    size: u64,
303    // Overrides all other options.  The disk will be unformatted.
304    uninitialized: bool,
305    blob_hash: Option<Hash>,
306    data_volume_size: u64,
307    fvm_slice_size: u64,
308    data_spec: DataSpec,
309    volumes_spec: VolumesSpec,
310    // Only used if `format` is Some.
311    corrupt_data: bool,
312    gpt: bool,
313    extra_volumes: Vec<&'static str>,
314    extra_gpt_partitions: Vec<(&'static str, u64)>,
315    // Note: fvm also means fxfs acting as the volume manager when using fxblob.
316    format_volume_manager: bool,
317    legacy_data_label: bool,
318    // Only used if 'fs_switch' set.
319    fs_switch: Option<String>,
320    // The type guid of the ramdisk when it's created for the test fshost.
321    type_guid: Option<[u8; 16]>,
322    system_partition_label: &'static str,
323    /// If set, pre-populates the keymint sealing key metadata on disk with
324    /// this content as the previous sealing key. Our crypt policy
325    /// code should attempt to delete this key when unseal is called.
326    keymint_old_blob: Option<Vec<u8>>,
327    /// Allows a keymint instance to be provided instead of the default one.
328    keymint: Option<std::sync::Arc<FakeKeymint>>,
329}
330
331impl std::fmt::Debug for DiskBuilder {
332    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
333        f.debug_struct("DiskBuilder")
334            .field("size", &self.size)
335            .field("uninitialized", &self.uninitialized)
336            .field("blob_hash", &self.blob_hash)
337            .field("data_volume_size", &self.data_volume_size)
338            .field("fvm_slice_size", &self.fvm_slice_size)
339            .field("data_spec", &self.data_spec)
340            .field("volumes_spec", &self.volumes_spec)
341            .field("corrupt_data", &self.corrupt_data)
342            .field("gpt", &self.gpt)
343            .field("format_volume_manager", &self.format_volume_manager)
344            .field("legacy_data_label", &self.legacy_data_label)
345            .field("fs_switch", &self.fs_switch)
346            .field("type_guid", &self.type_guid)
347            .field("system_partition_label", &self.system_partition_label)
348            .field("keymint_old_blob", &self.keymint_old_blob)
349            .finish_non_exhaustive()
350    }
351}
352
353impl DiskBuilder {
354    pub fn uninitialized() -> DiskBuilder {
355        Self { uninitialized: true, type_guid: None, ..Self::new() }
356    }
357
358    pub fn new() -> DiskBuilder {
359        DiskBuilder {
360            size: DEFAULT_DISK_SIZE,
361            uninitialized: false,
362            blob_hash: None,
363            data_volume_size: DEFAULT_DATA_VOLUME_SIZE,
364            fvm_slice_size: FVM_SLICE_SIZE,
365            data_spec: DataSpec::default(),
366            volumes_spec: VolumesSpec { fxfs_blob: false, create_data_partition: true },
367            corrupt_data: false,
368            gpt: false,
369            extra_volumes: Vec::new(),
370            extra_gpt_partitions: Vec::new(),
371            format_volume_manager: true,
372            legacy_data_label: false,
373            fs_switch: None,
374            type_guid: Some(DEFAULT_TEST_TYPE_GUID),
375            system_partition_label: "fvm",
376            keymint_old_blob: None,
377            keymint: None,
378        }
379    }
380
381    pub fn with_crypt_policy(&mut self, policy: crypt_policy::Policy) -> &mut Self {
382        self.data_spec.crypt_policy = policy;
383        self
384    }
385
386    pub fn with_keymint_instance(&mut self, keymint: std::sync::Arc<FakeKeymint>) -> &mut Self {
387        self.keymint = Some(keymint);
388        self
389    }
390
391    pub fn set_uninitialized(&mut self) -> &mut Self {
392        self.uninitialized = true;
393        self
394    }
395
396    pub fn with_keymint_old_blob(&mut self, blob: Vec<u8>) -> &mut Self {
397        self.keymint_old_blob = Some(blob);
398        self
399    }
400
401    pub fn size(&mut self, size: u64) -> &mut Self {
402        self.size = size;
403        self
404    }
405
406    pub fn data_volume_size(&mut self, data_volume_size: u64) -> &mut Self {
407        self.data_volume_size = data_volume_size;
408        // Increase the size of the disk if required. NB: We don't decrease the size of the disk
409        // because some tests set a lower initial size and expect to be able to resize to a larger
410        // one.
411        self.size = self.size.max(self.data_volume_size + BLOBFS_MAX_BYTES);
412        self
413    }
414
415    pub fn format_volumes(&mut self, volumes_spec: VolumesSpec) -> &mut Self {
416        self.volumes_spec = volumes_spec;
417        self
418    }
419
420    pub fn format_data(&mut self, data_spec: DataSpec) -> &mut Self {
421        log::info!(data_spec:?; "formatting data volume");
422        if !self.volumes_spec.fxfs_blob {
423            assert!(self.format_volume_manager);
424        } else {
425            if let Some(format) = data_spec.format {
426                assert_eq!(format, "fxfs");
427            }
428        }
429        if data_spec.format == Some("f2fs") {
430            self.fvm_slice_size = FVM_F2FS_SLICE_SIZE;
431            self.data_volume_size(self.data_volume_size.max(DEFAULT_F2FS_MIN_BYTES));
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        // Defined in //sdk/lib/zbi-format/include/lib/zbi-format/internal/storage.h.
891        const ZBI_FLAGS_STORAGE_COMPRESSED: zbi::Flags = zbi::Flags::from_bits_retain(1);
892
893        let (ramdisk_vmo, _) = self.build().await;
894        let extra = ramdisk_vmo.get_size().unwrap() as u32;
895        let mut decompressed_buf = vec![0u8; extra as usize];
896        ramdisk_vmo.read(&mut decompressed_buf, 0).unwrap();
897        let compressed_buf = zstd::encode_all(decompressed_buf.as_slice(), 0).unwrap();
898        let length = compressed_buf.len() as u32;
899
900        let header = zbi::Header {
901            r#type: zbi::Type::StorageRamdisk,
902            length,
903            extra,
904            flags: zbi::Flags::VERSION | ZBI_FLAGS_STORAGE_COMPRESSED,
905            reserved0: 0,
906            reserved1: 0,
907            magic: zbi::ITEM_MAGIC,
908            crc32: 0,
909        };
910
911        let header_size = std::mem::size_of::<zbi::Header>() as u64;
912        let zbi_vmo = zx::Vmo::create(header_size + length as u64).unwrap();
913        zbi_vmo.write(header.as_bytes(), 0).unwrap();
914        zbi_vmo.write(&compressed_buf, header_size).unwrap();
915
916        zbi_vmo
917    }
918}
919
920/// Helper function to return a set of the volumes found within a given `disk`, which must have a
921/// valid GPT containing an fxfs partition in the first entry.
922pub async fn list_all_fxfs_volumes(disk: &Disk) -> HashSet<String> {
923    let Disk::Prebuilt(vmo, _) = disk else {
924        panic!("list_all_fxfs_volumes only supports prebuilt disks");
925    };
926    let server = Arc::new(
927        VmoBackedServer::from_vmo(
928            TEST_DISK_BLOCK_SIZE,
929            vmo.duplicate_handle(zx::Rights::SAME_RIGHTS).unwrap(),
930        )
931        .unwrap(),
932    );
933
934    let partitions_dir = vfs::directory::immutable::simple();
935    let manager = GptManager::new(server.connect(), partitions_dir.clone()).await.unwrap();
936    let dir = vfs::directory::serve(
937        partitions_dir.clone(),
938        vfs::execution_scope::ExecutionScope::new(),
939        fio::PERM_READABLE | fio::PERM_WRITABLE,
940    );
941
942    let partitions = fuchsia_fs::directory::readdir(&dir).await.unwrap().into_iter().map(|entry| {
943        let dir = fuchsia_fs::directory::clone(&dir).unwrap();
944        let name = entry.name;
945        DirBasedBlockConnector::new(dir, format!("{name}/volume"))
946    });
947    let connector = find_block_device(
948        &[
949            BlockDeviceMatcher::TypeGuid(&FVM_TYPE_GUID),
950            BlockDeviceMatcher::InstanceGuid(&FVM_PART_INSTANCE_GUID),
951        ],
952        partitions,
953    )
954    .await
955    .expect("failed to match partition")
956    .expect("did not match fxfs partition");
957
958    let fxfs = Filesystem::from_boxed_config(Box::new(connector), Box::new(Fxfs::default()));
959    let fs = fxfs.serve_multi_volume().await.expect("serve_multi_volume failed");
960    let volumes = fs.list_volumes().await.expect("list_volumes failed");
961    fs.shutdown().await.expect("shutdown failed");
962    manager.shutdown().await;
963    volumes.into_iter().collect()
964}
965
966/// Returns the set of volume names expected for an fxblob-based system.
967pub fn expected_fxblob_volumes() -> HashSet<String> {
968    ["blob", "data", "unencrypted"].into_iter().map(str::to_owned).collect()
969}