Skip to main content

fxfs/object_store/journal/
super_block.rs

1// Copyright 2021 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
5//! We currently store two of these super-blocks (A/B) starting at offset 0 and 512kB.
6//!
7//! Immediately following the serialized `SuperBlockHeader` structure below is a stream of
8//! serialized operations that are replayed into the root parent `ObjectStore`. Note that the root
9//! parent object store exists entirely in RAM until serialized back into the super-block.
10//!
11//! Super-blocks are updated alternately with a monotonically increasing generation number.
12//! At mount time, the super-block used is the valid `SuperBlock` with the highest generation
13//! number.
14//!
15//! Note the asymmetry here regarding load/save:
16//!   * We load a superblock from a Device/SuperBlockInstance and return a
17//!     (SuperBlockHeader, ObjectStore) pair. The ObjectStore is populated directly from device.
18//!   * We save a superblock from a (SuperBlockHeader, Vec<ObjectItem>) pair to a WriteObjectHandle.
19//!
20//! This asymmetry is required for consistency.
21//! The Vec<ObjectItem> is produced by scanning the root_parent_store. This is the responsibility
22//! of the journal code, which must hold a lock to avoid concurrent updates. However, this lock
23//! must NOT be held when saving the superblock as additional extents may need to be allocated as
24//! part of the save process.
25use crate::errors::FxfsError;
26use crate::filesystem::{ApplyContext, ApplyMode, FxFilesystem, JournalingObject};
27use crate::log::*;
28use crate::lsm_tree::types::LayerIterator;
29use crate::lsm_tree::{LSMTree, LayerSet, Query};
30use crate::metrics;
31use crate::object_handle::ObjectHandle as _;
32use crate::object_store::allocator::Reservation;
33use crate::object_store::data_object_handle::{FileExtent, OverwriteOptions};
34use crate::object_store::journal::bootstrap_handle::BootstrapObjectHandle;
35use crate::object_store::journal::reader::{JournalReader, ReadResult};
36use crate::object_store::journal::writer::JournalWriter;
37use crate::object_store::journal::{BLOCK_SIZE, JournalCheckpoint, JournalCheckpointV32};
38use crate::object_store::object_record::{ObjectItem, ObjectItemV56};
39use crate::object_store::transaction::{AssocObj, Options};
40use crate::object_store::tree::MajorCompactable;
41use crate::object_store::{
42    DataObjectHandle, HandleOptions, HandleOwner, Mutation, ObjectKey, ObjectStore, ObjectValue,
43};
44use crate::range::RangeExt;
45use crate::serialized_types::{EARLIEST_SUPPORTED_VERSION, Version, Versioned, VersionedLatest};
46use anyhow::{Context, Error, bail, ensure};
47use fprint::TypeFingerprint;
48use fuchsia_inspect::{Property as _, UintProperty};
49use fuchsia_sync::Mutex;
50use futures::FutureExt;
51use rustc_hash::FxHashMap as HashMap;
52use serde::{Deserialize, Serialize};
53use std::collections::{HashSet, VecDeque};
54use std::fmt;
55use std::io::{Read, Write};
56use std::ops::Range;
57use std::sync::Arc;
58use std::time::SystemTime;
59use storage_device::Device;
60use storage_units::BlockSize;
61use uuid::Uuid;
62
63// These only exist in the root store.
64const SUPER_BLOCK_A_OBJECT_ID: u64 = 1;
65const SUPER_BLOCK_B_OBJECT_ID: u64 = 2;
66
67/// The superblock is extended in units of `SUPER_BLOCK_CHUNK_SIZE` as required.
68pub const SUPER_BLOCK_CHUNK_SIZE: u64 = 65536;
69
70/// Each superblock is one block but may contain records that extend its own length.
71pub(crate) const MIN_SUPER_BLOCK_SIZE: u64 = 4096;
72
73/// All superblocks start with the magic bytes "FxfsSupr".
74const SUPER_BLOCK_MAGIC: &[u8; 8] = b"FxfsSupr";
75
76/// An enum representing one of our super-block instances.
77///
78/// This provides hard-coded constants related to the location and properties of the super-blocks
79/// that are required to bootstrap the filesystem.
80#[derive(Copy, Clone, Debug, Eq, PartialEq)]
81pub enum SuperBlockInstance {
82    A,
83    B,
84}
85
86impl SuperBlockInstance {
87    /// Returns the next [SuperBlockInstance] for use in round-robining writes across super-blocks.
88    pub fn next(&self) -> SuperBlockInstance {
89        match self {
90            SuperBlockInstance::A => SuperBlockInstance::B,
91            SuperBlockInstance::B => SuperBlockInstance::A,
92        }
93    }
94
95    pub fn object_id(&self) -> u64 {
96        match self {
97            SuperBlockInstance::A => SUPER_BLOCK_A_OBJECT_ID,
98            SuperBlockInstance::B => SUPER_BLOCK_B_OBJECT_ID,
99        }
100    }
101
102    /// Returns the byte range where the first extent of the [SuperBlockInstance] is stored.
103    /// (Note that a [SuperBlockInstance] may still have multiple extents.)
104    pub fn first_extent(&self) -> Range<u64> {
105        match self {
106            SuperBlockInstance::A => 0..MIN_SUPER_BLOCK_SIZE,
107            SuperBlockInstance::B => 524288..524288 + MIN_SUPER_BLOCK_SIZE,
108        }
109    }
110}
111
112pub type SuperBlockHeader = SuperBlockHeaderV32;
113
114#[derive(
115    Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, TypeFingerprint, Versioned,
116)]
117pub struct SuperBlockHeaderV32 {
118    /// The globally unique identifier for the filesystem.
119    pub guid: UuidWrapperV32,
120
121    /// There are two super-blocks which are used in an A/B configuration. The super-block with the
122    /// greatest generation number is what is used when mounting an Fxfs image; the other is
123    /// discarded.
124    pub generation: u64,
125
126    /// The root parent store is an in-memory only store and serves as the backing store for the
127    /// root store and the journal.  The records for this store are serialized into the super-block
128    /// and mutations are also recorded in the journal.
129    pub root_parent_store_object_id: u64,
130
131    /// The root parent needs a graveyard and there's nowhere else to store it other than in the
132    /// super-block.
133    pub root_parent_graveyard_directory_object_id: u64,
134
135    /// The root object store contains all other metadata objects (including the allocator, the
136    /// journal and the super-blocks) and is the parent for all other object stores.
137    pub root_store_object_id: u64,
138
139    /// This is in the root object store.
140    pub allocator_object_id: u64,
141
142    /// This is in the root parent object store.
143    pub journal_object_id: u64,
144
145    /// Start checkpoint for the journal file.
146    pub journal_checkpoint: JournalCheckpointV32,
147
148    /// Offset of the journal file when the super-block was written.  If no entry is present in
149    /// journal_file_offsets for a particular object, then an object might have dependencies on the
150    /// journal from super_block_journal_file_offset onwards, but not earlier.
151    pub super_block_journal_file_offset: u64,
152
153    /// object id -> journal file offset. Indicates where each object has been flushed to.
154    pub journal_file_offsets: HashMap<u64, u64>,
155
156    /// Records the amount of borrowed metadata space as applicable at
157    /// `super_block_journal_file_offset`.
158    pub borrowed_metadata_space: u64,
159
160    /// The earliest version of Fxfs used to create any still-existing struct in the filesystem.
161    ///
162    /// Note: structs in the filesystem may had been made with various different versions of Fxfs.
163    pub earliest_version: Version,
164}
165
166type UuidWrapper = UuidWrapperV32;
167#[derive(Clone, Default, Eq, PartialEq)]
168pub struct UuidWrapperV32(pub Uuid);
169
170impl UuidWrapper {
171    fn new() -> Self {
172        Self(Uuid::new_v4())
173    }
174    #[cfg(test)]
175    fn nil() -> Self {
176        Self(Uuid::nil())
177    }
178}
179
180impl fmt::Debug for UuidWrapper {
181    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182        // The UUID uniquely identifies the filesystem, so we should redact it so that we don't leak
183        // it in logs.
184        f.write_str("<redacted>")
185    }
186}
187
188impl TypeFingerprint for UuidWrapper {
189    fn fingerprint() -> String {
190        "<[u8;16]>".to_owned()
191    }
192}
193
194// Uuid serializes like a slice, but SuperBlockHeader used to contain [u8; 16] and we want to remain
195// compatible.
196impl Serialize for UuidWrapper {
197    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
198        self.0.as_bytes().serialize(serializer)
199    }
200}
201
202impl<'de> Deserialize<'de> for UuidWrapper {
203    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
204        <[u8; 16]>::deserialize(deserializer).map(|bytes| UuidWrapperV32(Uuid::from_bytes(bytes)))
205    }
206}
207
208pub type SuperBlockRecord = SuperBlockRecordV56;
209
210#[allow(clippy::large_enum_variant)]
211#[derive(Debug, Serialize, Deserialize, TypeFingerprint, Versioned)]
212pub enum SuperBlockRecordV56 {
213    // When reading the super-block we know the initial extent, but not subsequent extents, so these
214    // records need to exist to allow us to completely read the super-block.
215    Extent(Range<u64>),
216
217    // Following the super-block header are ObjectItem records that are to be replayed into the root
218    // parent object store.
219    ObjectItem(ObjectItemV56),
220
221    // Marks the end of the full super-block.
222    End,
223}
224
225struct SuperBlockMetrics {
226    /// Time we wrote the most recent superblock in milliseconds since [`std::time::UNIX_EPOCH`].
227    /// Uses [`std::time::SystemTime`] as the clock source.
228    last_super_block_update_time_ms: UintProperty,
229
230    /// Offset of the most recent superblock we wrote in the journal.
231    last_super_block_offset: UintProperty,
232}
233
234impl Default for SuperBlockMetrics {
235    fn default() -> Self {
236        SuperBlockMetrics {
237            last_super_block_update_time_ms: metrics::detail()
238                .create_uint("last_super_block_update_time_ms", 0),
239            last_super_block_offset: metrics::detail().create_uint("last_super_block_offset", 0),
240        }
241    }
242}
243
244/// Reads an individual (A/B) super-block instance and root_parent_store from device.
245/// Users should use SuperBlockManager::load() instead.
246async fn read(
247    device: Arc<dyn Device>,
248    block_size: BlockSize,
249    instance: SuperBlockInstance,
250) -> Result<(SuperBlockHeader, SuperBlockInstance, ObjectStore), Error> {
251    let (super_block_header, mut reader) = SuperBlockHeader::read_header(device.clone(), instance)
252        .await
253        .context("failed to read superblock")?;
254    let root_parent = ObjectStore::new_root_parent(
255        device,
256        block_size,
257        super_block_header.root_parent_store_object_id,
258    );
259    root_parent.set_graveyard_directory_object_id(
260        super_block_header.root_parent_graveyard_directory_object_id,
261    );
262
263    loop {
264        // TODO: Flatten a layer and move reader here?
265        let mutation = match reader.next_item().await? {
266            // RecordReader should filter out extent records.
267            SuperBlockRecord::Extent(_) => bail!("Unexpected extent record"),
268            SuperBlockRecord::ObjectItem(item) => Mutation::insert_object(item.key, item.value),
269            SuperBlockRecord::End => break,
270        };
271        root_parent.apply_mutation(
272            mutation,
273            &ApplyContext {
274                mode: ApplyMode::Replay,
275                // A file offset of 0 is safe here because this is only used for the root parent
276                // store, which is completely reconstructed from the superblock at each mount,
277                // making the checkpoint offset irrelevant.
278                checkpoint: JournalCheckpoint { file_offset: 0, ..Default::default() },
279            },
280            AssocObj::None,
281        )?;
282    }
283    Ok((super_block_header, instance, root_parent))
284}
285
286/// Write a super-block to the given file handle.
287/// Requires that the filesystem is fully loaded and writable as this may require allocation.
288async fn write<S: HandleOwner>(
289    super_block_header: &SuperBlockHeader,
290    items: LayerSet<ObjectKey, ObjectValue>,
291    handle: DataObjectHandle<S>,
292) -> Result<(), Error> {
293    let object_manager = handle.store().filesystem().object_manager().clone();
294    // TODO(https://fxbug.dev/42177407): Don't use the same code here for Journal and SuperBlock. They
295    // aren't the same things and it is already getting convoluted. e.g of diff stream content:
296    //   Superblock:  (Magic, Ver, Header(Ver), Extent(Ver)*, SuperBlockRecord(Ver)*, ...)
297    //   Journal:     (Ver, JournalRecord(Ver)*, RESET, Ver2, JournalRecord(Ver2)*, ...)
298    // We should abstract away the checksum code and implement these separately.
299
300    let mut writer =
301        SuperBlockWriter::new(handle, super_block_header, object_manager.metadata_reservation())
302            .await?;
303    let mut merger = items.merger();
304    let mut iter = LSMTree::major_iter(merger.query(Query::FullScan).await?).await?;
305    while let Some(item) = iter.get() {
306        writer.write_root_parent_item(item.cloned()).await?;
307        iter.advance().await?;
308    }
309    writer.finalize().await
310}
311
312// Compacts and returns the *old* snapshot of the root_parent store.
313// Must be performed whilst holding a writer lock.
314pub fn compact_root_parent(
315    root_parent_store: &ObjectStore,
316) -> Result<LayerSet<ObjectKey, ObjectValue>, Error> {
317    // The root parent always uses in-memory layers which shouldn't be async, so we can use
318    // `now_or_never`.
319    let tree = root_parent_store.tree();
320    let layer_set = tree.layer_set();
321    {
322        let mut merger = layer_set.merger();
323        let mut iter = LSMTree::major_iter(merger.query(Query::FullScan).now_or_never().unwrap()?)
324            .now_or_never()
325            .unwrap()?;
326        let new_layer = LSMTree::new_mutable_layer();
327        while let Some(item_ref) = iter.get() {
328            new_layer.insert(item_ref.cloned())?;
329            iter.advance().now_or_never().unwrap()?;
330        }
331        tree.set_mutable_layer(new_layer);
332    }
333    Ok(layer_set)
334}
335
336/// This encapsulates the A/B alternating super-block logic.
337/// All super-block load/save operations should be via the methods on this type.
338pub(super) struct SuperBlockManager {
339    pub next_instance: Mutex<SuperBlockInstance>,
340    metrics: SuperBlockMetrics,
341}
342
343impl SuperBlockManager {
344    pub fn new() -> Self {
345        Self { next_instance: Mutex::new(SuperBlockInstance::A), metrics: Default::default() }
346    }
347
348    /// Loads both A/B super-blocks and root_parent ObjectStores and and returns the newest valid
349    /// pair. Also ensures the next superblock updated via |save| will be the other instance.
350    pub async fn load(
351        &self,
352        device: Arc<dyn Device>,
353        block_size: BlockSize,
354    ) -> Result<(SuperBlockHeader, ObjectStore), Error> {
355        // Superblocks consume a minimum of one block. We currently hard code the length of
356        // this first extent. It should work with larger block sizes, but has not been tested.
357        // TODO(https://fxbug.dev/42063349): Consider relaxing this.
358        debug_assert!(MIN_SUPER_BLOCK_SIZE == block_size);
359
360        let (super_block, current_super_block, root_parent) = match futures::join!(
361            read(device.clone(), block_size, SuperBlockInstance::A),
362            read(device.clone(), block_size, SuperBlockInstance::B)
363        ) {
364            (Err(e1), Err(e2)) => {
365                bail!("Failed to load both superblocks due to {:?}\nand\n{:?}", e1, e2)
366            }
367            (Ok(result), Err(_)) => result,
368            (Err(_), Ok(result)) => result,
369            (Ok(result1), Ok(result2)) => {
370                // Break the tie by taking the super-block with the greatest generation.
371                if (result2.0.generation as i64).wrapping_sub(result1.0.generation as i64) > 0 {
372                    result2
373                } else {
374                    result1
375                }
376            }
377        };
378        info!(super_block:?, current_super_block:?; "loaded super-block");
379        *self.next_instance.lock() = current_super_block.next();
380        Ok((super_block, root_parent))
381    }
382
383    /// Writes the provided superblock and root_parent ObjectStore to the device.
384    /// Requires that the filesystem is fully loaded and writable as this may require allocation.
385    pub async fn save(
386        &self,
387        super_block_header: SuperBlockHeader,
388        filesystem: Arc<FxFilesystem>,
389        root_parent: LayerSet<ObjectKey, ObjectValue>,
390    ) -> Result<(), Error> {
391        let root_store = filesystem.root_store();
392        let instance = *self.next_instance.lock();
393        let handle = ObjectStore::open_object(
394            &root_store,
395            instance.object_id(),
396            HandleOptions { skip_journal_checks: true, ..Default::default() },
397            None,
398        )
399        .await
400        .context("Failed to open superblock object")?;
401        write(&super_block_header, root_parent, handle).await?;
402        *self.next_instance.lock() = instance.next();
403        self.metrics
404            .last_super_block_offset
405            .set(super_block_header.super_block_journal_file_offset);
406        self.metrics.last_super_block_update_time_ms.set(
407            SystemTime::now()
408                .duration_since(SystemTime::UNIX_EPOCH)
409                .unwrap()
410                .as_millis()
411                .try_into()
412                .unwrap_or(0u64),
413        );
414        Ok(())
415    }
416}
417
418impl SuperBlockHeader {
419    /// Creates a new instance with random GUID.
420    pub fn new(
421        generation: u64,
422        root_parent_store_object_id: u64,
423        root_parent_graveyard_directory_object_id: u64,
424        root_store_object_id: u64,
425        allocator_object_id: u64,
426        journal_object_id: u64,
427        journal_checkpoint: JournalCheckpoint,
428        earliest_version: Version,
429    ) -> Self {
430        SuperBlockHeader {
431            guid: UuidWrapper::new(),
432            generation,
433            root_parent_store_object_id,
434            root_parent_graveyard_directory_object_id,
435            root_store_object_id,
436            allocator_object_id,
437            journal_object_id,
438            journal_checkpoint,
439            earliest_version,
440            ..Default::default()
441        }
442    }
443
444    /// Read the super-block header, and return it and a reader that produces the records that are
445    /// to be replayed in to the root parent object store.
446    async fn read_header(
447        device: Arc<dyn Device>,
448        target_super_block: SuperBlockInstance,
449    ) -> Result<(SuperBlockHeader, RecordReader), Error> {
450        let handle = BootstrapObjectHandle::new(
451            target_super_block.object_id(),
452            device,
453            target_super_block.first_extent(),
454        );
455        let mut reader = JournalReader::new(handle, &JournalCheckpoint::default());
456        reader.set_eof_ok();
457
458        reader.fill_buf().await?;
459
460        let mut super_block_header;
461        let super_block_version;
462        reader.consume({
463            let mut cursor = std::io::Cursor::new(reader.buffer());
464            // Validate magic bytes.
465            let mut magic_bytes: [u8; 8] = [0; 8];
466            cursor.read_exact(&mut magic_bytes)?;
467            if magic_bytes.as_slice() != SUPER_BLOCK_MAGIC.as_slice() {
468                bail!("Invalid magic: {:?}", magic_bytes);
469            }
470            (super_block_header, super_block_version) =
471                SuperBlockHeader::deserialize_with_version(&mut cursor)?;
472
473            // Ensure all store IDs are distinct.
474            let mut stores = HashSet::new();
475            ensure!(
476                stores.insert(super_block_header.root_parent_store_object_id),
477                FxfsError::Inconsistent
478            );
479            ensure!(
480                stores.insert(super_block_header.root_store_object_id),
481                FxfsError::Inconsistent
482            );
483
484            // Ensure all objects in the root parent store are distinct.
485            let mut root_parent_objects = HashSet::new();
486            ensure!(
487                root_parent_objects
488                    .insert(super_block_header.root_parent_graveyard_directory_object_id),
489                FxfsError::Inconsistent
490            );
491            ensure!(
492                root_parent_objects.insert(super_block_header.root_store_object_id),
493                FxfsError::Inconsistent
494            );
495            ensure!(
496                root_parent_objects.insert(super_block_header.journal_object_id),
497                FxfsError::Inconsistent
498            );
499
500            // The allocator (in root_store) cannot match any store ID.
501            ensure!(
502                !stores.contains(&super_block_header.allocator_object_id),
503                FxfsError::Inconsistent
504            );
505
506            if super_block_version < EARLIEST_SUPPORTED_VERSION {
507                bail!("Unsupported SuperBlock version: {:?}", super_block_version);
508            }
509
510            // NOTE: It is possible that data was written to the journal with an old version
511            // but no compaction ever happened, so the journal version could potentially be older
512            // than the layer file versions.
513            if super_block_header.journal_checkpoint.version < EARLIEST_SUPPORTED_VERSION {
514                bail!(
515                    "Unsupported JournalCheckpoint version: {:?}",
516                    super_block_header.journal_checkpoint.version
517                );
518            }
519
520            if super_block_header.earliest_version < EARLIEST_SUPPORTED_VERSION {
521                bail!(
522                    "Filesystem contains struct with unsupported version: {:?}",
523                    super_block_header.earliest_version
524                );
525            }
526
527            cursor.position() as usize
528        });
529
530        // If guid is zeroed (e.g. in a newly imaged system), assign one randomly.
531        if super_block_header.guid.0.is_nil() {
532            super_block_header.guid = UuidWrapper::new();
533        }
534        reader.set_version(super_block_version);
535        Ok((super_block_header, RecordReader { reader }))
536    }
537}
538
539struct SuperBlockWriter<'a, S: HandleOwner> {
540    handle: DataObjectHandle<S>,
541    writer: JournalWriter,
542    existing_extents: VecDeque<FileExtent>,
543    size: u64,
544    reservation: &'a Reservation,
545}
546
547impl<'a, S: HandleOwner> SuperBlockWriter<'a, S> {
548    /// Create a new writer, outputs FXFS magic, version and SuperBlockHeader.
549    /// On success, the writer is ready to accept root parent store mutations.
550    pub async fn new(
551        handle: DataObjectHandle<S>,
552        super_block_header: &SuperBlockHeader,
553        reservation: &'a Reservation,
554    ) -> Result<Self, Error> {
555        let existing_extents = handle.device_extents().await?;
556        let mut this = Self {
557            handle,
558            writer: JournalWriter::new(BLOCK_SIZE, 0),
559            existing_extents: existing_extents.into_iter().collect(),
560            size: 0,
561            reservation,
562        };
563        this.writer.write_all(SUPER_BLOCK_MAGIC)?;
564        super_block_header.serialize_with_version(&mut this.writer)?;
565        Ok(this)
566    }
567
568    /// Internal helper function to pull ranges from a list of existing extents and tack
569    /// corresponding extent records onto the journal.
570    fn try_extend_existing(&mut self, target_size: u64) -> Result<(), Error> {
571        while self.size < target_size {
572            if let Some(extent) = self.existing_extents.pop_front() {
573                ensure!(
574                    extent.logical_range().start == self.size,
575                    "superblock file contains a hole."
576                );
577                self.size += extent.length();
578                SuperBlockRecord::Extent(extent.device_range().clone())
579                    .serialize_into(&mut self.writer)?;
580            } else {
581                break;
582            }
583        }
584        Ok(())
585    }
586
587    pub async fn write_root_parent_item(&mut self, record: ObjectItem) -> Result<(), Error> {
588        let min_len = self.writer.journal_file_checkpoint().file_offset + SUPER_BLOCK_CHUNK_SIZE;
589        self.try_extend_existing(min_len)?;
590        if min_len > self.size {
591            // Need to allocate some more space.
592            let mut transaction = self
593                .handle
594                .new_transaction_with_options(Options {
595                    skip_journal_checks: true,
596                    borrow_metadata_space: true,
597                    allocator_reservation: Some(self.reservation),
598                    ..Default::default()
599                })
600                .await?;
601            let mut file_range = self.size..self.size + SUPER_BLOCK_CHUNK_SIZE;
602            let allocated = self
603                .handle
604                .preallocate_range(&mut transaction, &mut file_range)
605                .await
606                .context("preallocate superblock")?;
607            if file_range.start < file_range.end {
608                bail!("preallocate_range returned too little space");
609            }
610            transaction.commit().await?;
611            for device_range in allocated {
612                self.size += device_range.end - device_range.start;
613                SuperBlockRecord::Extent(device_range).serialize_into(&mut self.writer)?;
614            }
615        }
616        SuperBlockRecord::ObjectItem(record).serialize_into(&mut self.writer)?;
617        Ok(())
618    }
619
620    pub async fn finalize(mut self) -> Result<(), Error> {
621        SuperBlockRecord::End.serialize_into(&mut self.writer)?;
622        self.writer.pad_to_block()?;
623        let mut buf = self.handle.allocate_buffer(self.writer.flushable_bytes()).await;
624        let offset = self.writer.take_flushable(buf.as_mut());
625        self.handle.overwrite(offset, buf.as_mut(), OverwriteOptions::default()).await?;
626        let len =
627            std::cmp::max(MIN_SUPER_BLOCK_SIZE, self.writer.journal_file_checkpoint().file_offset)
628                + SUPER_BLOCK_CHUNK_SIZE;
629        self.handle
630            .truncate_with_options(
631                Options {
632                    skip_journal_checks: true,
633                    borrow_metadata_space: true,
634                    ..Default::default()
635                },
636                len,
637            )
638            .await?;
639        Ok(())
640    }
641}
642
643pub struct RecordReader {
644    reader: JournalReader,
645}
646
647impl RecordReader {
648    pub async fn next_item(&mut self) -> Result<SuperBlockRecord, Error> {
649        loop {
650            match self.reader.deserialize().await? {
651                ReadResult::Reset(_) => bail!("Unexpected reset"),
652                ReadResult::ChecksumMismatch => bail!("Checksum mismatch"),
653                ReadResult::Some(SuperBlockRecord::Extent(extent)) => {
654                    ensure!(extent.is_valid(), FxfsError::Inconsistent);
655                    self.reader.handle().push_extent(0, extent)
656                }
657                ReadResult::Some(x) => return Ok(x),
658            }
659        }
660    }
661}
662
663#[cfg(test)]
664mod tests {
665    use super::{
666        MIN_SUPER_BLOCK_SIZE, SUPER_BLOCK_CHUNK_SIZE, SUPER_BLOCK_MAGIC, SuperBlockHeader,
667        SuperBlockInstance, SuperBlockManager, SuperBlockRecord, UuidWrapper, compact_root_parent,
668        write,
669    };
670    use crate::filesystem::{FxFilesystem, OpenFxFilesystem, SyncOptions};
671    use crate::object_handle::ReadObjectHandle;
672    use crate::object_store::journal::JournalCheckpoint;
673    use crate::object_store::journal::writer::JournalWriter;
674    use crate::object_store::transaction::{Options, lock_keys};
675    use crate::object_store::{
676        DataObjectHandle, HandleOptions, ObjectHandle, ObjectKey, ObjectStore,
677    };
678    use crate::serialized_types::{LATEST_VERSION, Versioned, VersionedLatest};
679    use anyhow::bail;
680    use std::io::Write;
681    use std::sync::Arc;
682    use std::sync::atomic::{AtomicBool, Ordering};
683    use storage_device::DeviceHolder;
684    use storage_device::fake_device::{FakeDevice, Op};
685    use storage_units::BlockSize;
686
687    // We require 512kiB each for A/B super-blocks, 256kiB for the journal (128kiB before flush)
688    // and compactions require double the layer size to complete.
689    const TEST_DEVICE_BLOCK_SIZE: BlockSize = BlockSize::SIZE_512B;
690    const TEST_DEVICE_BLOCK_COUNT: u64 = 16384;
691
692    async fn filesystem_and_super_block_handles()
693    -> (OpenFxFilesystem, DataObjectHandle<ObjectStore>, DataObjectHandle<ObjectStore>) {
694        let device = DeviceHolder::new(FakeDevice::new(
695            TEST_DEVICE_BLOCK_COUNT,
696            TEST_DEVICE_BLOCK_SIZE.get() as u32,
697        ));
698        let fs = FxFilesystem::new_empty(device).await.expect("new_empty failed");
699        fs.close().await.expect("Close failed");
700        let device = fs.take_device().await;
701        device.reopen(false);
702        let fs = FxFilesystem::open(device).await.expect("open failed");
703
704        let handle_a = ObjectStore::open_object(
705            &fs.object_manager().root_store(),
706            SuperBlockInstance::A.object_id(),
707            HandleOptions::default(),
708            None,
709        )
710        .await
711        .expect("open superblock failed");
712
713        let handle_b = ObjectStore::open_object(
714            &fs.object_manager().root_store(),
715            SuperBlockInstance::B.object_id(),
716            HandleOptions::default(),
717            None,
718        )
719        .await
720        .expect("open superblock failed");
721        (fs, handle_a, handle_b)
722    }
723
724    #[fuchsia::test]
725    async fn test_read_written_super_block() {
726        let (fs, _handle_a, _handle_b) = filesystem_and_super_block_handles().await;
727        const JOURNAL_OBJECT_ID: u64 = 5;
728
729        // Confirm that the (first) super-block is expected size.
730        // It should be MIN_SUPER_BLOCK_SIZE + SUPER_BLOCK_CHUNK_SIZE.
731        assert_eq!(
732            ObjectStore::open_object(
733                &fs.root_store(),
734                SuperBlockInstance::A.object_id(),
735                HandleOptions::default(),
736                None,
737            )
738            .await
739            .expect("open_object failed")
740            .get_size(),
741            MIN_SUPER_BLOCK_SIZE + SUPER_BLOCK_CHUNK_SIZE
742        );
743
744        // Create a large number of objects in the root parent store so that we test growing
745        // of the super-block file, requiring us to add extents.
746        let mut created_object_ids = vec![];
747        const NUM_ENTRIES: u64 = 16384;
748        for _ in 0..NUM_ENTRIES {
749            let mut transaction = fs
750                .root_store()
751                .new_transaction(lock_keys![], Options::default())
752                .await
753                .expect("new_transaction failed");
754            created_object_ids.push(
755                ObjectStore::create_object(
756                    &fs.object_manager().root_parent_store(),
757                    &mut transaction,
758                    HandleOptions::default(),
759                    None,
760                )
761                .await
762                .expect("create_object failed")
763                .object_id(),
764            );
765            transaction.commit().await.expect("commit failed");
766        }
767
768        // Note here that DataObjectHandle caches the size given to it at construction.
769        // If we want to know the true size after a super-block has been written, we need
770        // a new handle.
771        assert!(
772            ObjectStore::open_object(
773                &fs.root_store(),
774                SuperBlockInstance::A.object_id(),
775                HandleOptions::default(),
776                None,
777            )
778            .await
779            .expect("open_object failed")
780            .get_size()
781                > MIN_SUPER_BLOCK_SIZE + SUPER_BLOCK_CHUNK_SIZE
782        );
783
784        let written_super_block_a =
785            SuperBlockHeader::read_header(fs.device(), SuperBlockInstance::A)
786                .await
787                .expect("read failed");
788        let written_super_block_b =
789            SuperBlockHeader::read_header(fs.device(), SuperBlockInstance::B)
790                .await
791                .expect("read failed");
792
793        // Check that a non-zero GUID has been assigned.
794        assert!(!written_super_block_a.0.guid.0.is_nil());
795
796        // Depending on specific offsets is fragile so we just validate the fields we believe
797        // to be stable.
798        assert_eq!(written_super_block_a.0.guid, written_super_block_b.0.guid);
799        assert_eq!(written_super_block_a.0.guid, written_super_block_b.0.guid);
800        assert!(written_super_block_a.0.generation != written_super_block_b.0.generation);
801        assert_eq!(
802            written_super_block_a.0.root_parent_store_object_id,
803            written_super_block_b.0.root_parent_store_object_id
804        );
805        assert_eq!(
806            written_super_block_a.0.root_parent_graveyard_directory_object_id,
807            written_super_block_b.0.root_parent_graveyard_directory_object_id
808        );
809        assert_eq!(written_super_block_a.0.root_store_object_id, fs.root_store().store_object_id());
810        assert_eq!(
811            written_super_block_a.0.root_store_object_id,
812            written_super_block_b.0.root_store_object_id
813        );
814        assert_eq!(written_super_block_a.0.allocator_object_id, fs.allocator().object_id());
815        assert_eq!(
816            written_super_block_a.0.allocator_object_id,
817            written_super_block_b.0.allocator_object_id
818        );
819        assert_eq!(written_super_block_a.0.journal_object_id, JOURNAL_OBJECT_ID);
820        assert_eq!(
821            written_super_block_a.0.journal_object_id,
822            written_super_block_b.0.journal_object_id
823        );
824        assert!(
825            written_super_block_a.0.journal_checkpoint.file_offset
826                != written_super_block_b.0.journal_checkpoint.file_offset
827        );
828        assert!(
829            written_super_block_a.0.super_block_journal_file_offset
830                != written_super_block_b.0.super_block_journal_file_offset
831        );
832        // Nb: We skip journal_file_offsets and borrowed metadata space checks.
833        assert_eq!(written_super_block_a.0.earliest_version, LATEST_VERSION);
834        assert_eq!(
835            written_super_block_a.0.earliest_version,
836            written_super_block_b.0.earliest_version
837        );
838
839        // Nb: Skip comparison of root_parent store contents because we have no way of anticipating
840        // the extent offsets and it is reasonable that a/b differ.
841
842        // Delete all the objects we just made.
843        for object_id in created_object_ids {
844            let mut transaction = fs
845                .root_store()
846                .new_transaction(lock_keys![], Options::default())
847                .await
848                .expect("new_transaction failed");
849            fs.object_manager()
850                .root_parent_store()
851                .adjust_refs(&mut transaction, object_id, -1)
852                .await
853                .expect("adjust_refs failed");
854            transaction.commit().await.expect("commit failed");
855            fs.object_manager()
856                .root_parent_store()
857                .tombstone_object(object_id, Options::default(), None)
858                .await
859                .expect("tombstone failed");
860        }
861        // Write some stuff to the root store to ensure we rotate the journal and produce new
862        // super blocks.
863        for _ in 0..NUM_ENTRIES {
864            let mut transaction = fs
865                .root_store()
866                .new_transaction(lock_keys![], Options::default())
867                .await
868                .expect("new_transaction failed");
869            ObjectStore::create_object(
870                &fs.object_manager().root_store(),
871                &mut transaction,
872                HandleOptions::default(),
873                None,
874            )
875            .await
876            .expect("create_object failed");
877            transaction.commit().await.expect("commit failed");
878        }
879
880        assert_eq!(
881            ObjectStore::open_object(
882                &fs.root_store(),
883                SuperBlockInstance::A.object_id(),
884                HandleOptions::default(),
885                None,
886            )
887            .await
888            .expect("open_object failed")
889            .get_size(),
890            MIN_SUPER_BLOCK_SIZE + SUPER_BLOCK_CHUNK_SIZE
891        );
892    }
893
894    #[fuchsia::test]
895    async fn test_generation_comparison_wrapping() {
896        const BLOCK_SIZE: BlockSize = BlockSize::new(MIN_SUPER_BLOCK_SIZE as u32).unwrap();
897        let device =
898            DeviceHolder::new(FakeDevice::new(TEST_DEVICE_BLOCK_COUNT, BLOCK_SIZE.get() as u32));
899        let fs = FxFilesystem::new_empty(device).await.expect("new_empty failed");
900        fs.close().await.expect("close");
901        let device = fs.take_device().await;
902        device.reopen(false);
903
904        // Helper to write a superblock with a specific generation to a specific instance.
905        // We need to clone the inner Arc to pass to the closure.
906        let device_arc = (*device).clone();
907        let write_sb = |instance: SuperBlockInstance, generation: u64| {
908            let device = device_arc.clone();
909            async move {
910                let mut super_block_header = SuperBlockHeader::new(
911                    1, // generation
912                    3, // root_parent_store_object_id
913                    4, // root_parent_graveyard_directory_object_id
914                    5, // root_store_object_id
915                    6, // allocator_object_id
916                    7, // journal_object_id
917                    JournalCheckpoint::default(),
918                    LATEST_VERSION,
919                );
920                super_block_header.generation = generation;
921                super_block_header.journal_checkpoint.version = LATEST_VERSION;
922
923                let mut writer = JournalWriter::new(BLOCK_SIZE, 0);
924                writer.write_all(SUPER_BLOCK_MAGIC).unwrap();
925                super_block_header.serialize_with_version(&mut writer).unwrap();
926                SuperBlockRecord::End.serialize_into(&mut writer).unwrap();
927                writer.pad_to_block().unwrap();
928
929                let mut buf = device.allocate_buffer(writer.flushable_bytes()).await;
930                writer.take_flushable(buf.as_mut());
931                device
932                    .write(instance.first_extent().start, buf.as_ref())
933                    .await
934                    .expect("write failed");
935            }
936        };
937
938        // Case 1: A has MAX, B has 0. B should be selected.
939        write_sb(SuperBlockInstance::A, u64::MAX).await;
940        write_sb(SuperBlockInstance::B, 0).await;
941        let manager = SuperBlockManager::new();
942        let (header, _) = manager.load((*device).clone(), BLOCK_SIZE).await.expect("load failed");
943        assert_eq!(header.generation, 0);
944
945        // Case 2: A has 0, B has MAX. A should be selected.
946        write_sb(SuperBlockInstance::A, 0).await;
947        write_sb(SuperBlockInstance::B, u64::MAX).await;
948        let manager = SuperBlockManager::new();
949        let (header, _) = manager.load((*device).clone(), BLOCK_SIZE).await.expect("load failed");
950        assert_eq!(header.generation, 0);
951
952        // Case 3: A has 100, B has 200. B should be selected.
953        write_sb(SuperBlockInstance::A, 100).await;
954        write_sb(SuperBlockInstance::B, 200).await;
955        let manager = SuperBlockManager::new();
956        let (header, _) = manager.load((*device).clone(), BLOCK_SIZE).await.expect("load failed");
957        assert_eq!(header.generation, 200);
958    }
959
960    #[fuchsia::test]
961    async fn test_generation_wrapping_on_flush() {
962        const BLOCK_SIZE: BlockSize = BlockSize::SIZE_4KIB;
963        let mut device =
964            DeviceHolder::new(FakeDevice::new(TEST_DEVICE_BLOCK_COUNT, BLOCK_SIZE.get() as u32));
965        {
966            let fs = FxFilesystem::new_empty(device).await.expect("new_empty failed");
967            let root_store = fs.root_store();
968            let mut transaction = fs
969                .root_store()
970                .new_transaction(lock_keys![], Options::default())
971                .await
972                .expect("new_transaction failed");
973            ObjectStore::create_object(
974                &root_store,
975                &mut transaction,
976                HandleOptions::default(),
977                None,
978            )
979            .await
980            .expect("create_object failed");
981            transaction.commit().await.expect("commit failed");
982            fs.sync(SyncOptions::default()).await.expect("sync failed");
983            fs.close().await.expect("close failed");
984            device = fs.take_device().await;
985        }
986        device.reopen(false);
987
988        let manager = SuperBlockManager::new();
989        let (mut header, _) =
990            manager.load((*device).clone(), BLOCK_SIZE).await.expect("load failed");
991
992        {
993            let fs = FxFilesystem::open(device).await.expect("open failed");
994            // To test wrapping, we need to get into a state where the current generation is
995            // u64::MAX. Since we have A and B, and wrapping comparison is used, we need to set them
996            // up carefully. We will set A to u64::MAX - 1, and B to u64::MAX.
997            // Then the next write will be to A, and should be 0.
998            header.generation = u64::MAX - 1;
999            manager
1000                .save(header.clone(), (*fs).clone(), fs.root_parent_store().tree().layer_set())
1001                .await
1002                .expect("save 1 failed");
1003            header.generation = u64::MAX;
1004            manager
1005                .save(header, (*fs).clone(), fs.root_parent_store().tree().layer_set())
1006                .await
1007                .expect("save 2 failed");
1008            fs.close().await.expect("close failed");
1009            device = fs.take_device().await;
1010            device.reopen(false);
1011
1012            let fs = FxFilesystem::open(device).await.expect("open failed");
1013
1014            let root_store = fs.root_store();
1015            for _ in 0..6000 {
1016                let mut transaction = fs
1017                    .root_store()
1018                    .new_transaction(lock_keys![], Options::default())
1019                    .await
1020                    .expect("new_transaction failed");
1021                ObjectStore::create_object(
1022                    &root_store,
1023                    &mut transaction,
1024                    HandleOptions::default(),
1025                    None,
1026                )
1027                .await
1028                .expect("create_object failed");
1029                transaction.commit().await.expect("commit failed");
1030            }
1031            fs.sync(SyncOptions::default()).await.expect("sync failed");
1032            fs.close().await.expect("close failed");
1033            device = fs.take_device().await;
1034        }
1035        device.reopen(false);
1036
1037        let (header, _) = manager.load((*device).clone(), BLOCK_SIZE).await.expect("load failed");
1038        assert!(header.generation < 10);
1039    }
1040
1041    #[fuchsia::test]
1042    async fn test_guid_assign_on_read() {
1043        let (fs, handle_a, _handle_b) = filesystem_and_super_block_handles().await;
1044        const JOURNAL_OBJECT_ID: u64 = 5;
1045        let mut super_block_header_a = SuperBlockHeader::new(
1046            1,
1047            fs.object_manager().root_parent_store().store_object_id(),
1048            /* root_parent_graveyard_directory_object_id: */ 1000,
1049            fs.root_store().store_object_id(),
1050            fs.allocator().object_id(),
1051            JOURNAL_OBJECT_ID,
1052            JournalCheckpoint { file_offset: 1234, checksum: 5678, version: LATEST_VERSION },
1053            /* earliest_version: */ LATEST_VERSION,
1054        );
1055        // Ensure the superblock has no set GUID.
1056        super_block_header_a.guid = UuidWrapper::nil();
1057        write(
1058            &super_block_header_a,
1059            compact_root_parent(fs.object_manager().root_parent_store().as_ref())
1060                .expect("scan failed"),
1061            handle_a,
1062        )
1063        .await
1064        .expect("write failed");
1065        let super_block_header = SuperBlockHeader::read_header(fs.device(), SuperBlockInstance::A)
1066            .await
1067            .expect("read failed");
1068        // Ensure a GUID has been assigned.
1069        assert!(!super_block_header.0.guid.0.is_nil());
1070    }
1071
1072    #[fuchsia::test]
1073    async fn test_init_wipes_superblocks() {
1074        let device = DeviceHolder::new(FakeDevice::new(8192, TEST_DEVICE_BLOCK_SIZE.get() as u32));
1075
1076        let fs = FxFilesystem::new_empty(device).await.expect("new_empty failed");
1077        let root_store = fs.root_store();
1078        // Generate enough work to induce a journal flush and thus a new superblock being written.
1079        for _ in 0..6000 {
1080            let mut transaction = fs
1081                .root_store()
1082                .new_transaction(lock_keys![], Options::default())
1083                .await
1084                .expect("new_transaction failed");
1085            ObjectStore::create_object(
1086                &root_store,
1087                &mut transaction,
1088                HandleOptions::default(),
1089                None,
1090            )
1091            .await
1092            .expect("create_object failed");
1093            transaction.commit().await.expect("commit failed");
1094        }
1095        fs.close().await.expect("Close failed");
1096        let device = fs.take_device().await;
1097        device.reopen(false);
1098
1099        SuperBlockHeader::read_header(device.clone(), SuperBlockInstance::A)
1100            .await
1101            .expect("read failed");
1102        let header = SuperBlockHeader::read_header(device.clone(), SuperBlockInstance::B)
1103            .await
1104            .expect("read failed");
1105
1106        let old_guid = header.0.guid;
1107
1108        // Re-initialize the filesystem.  The A and B blocks should be for the new FS.
1109        let fs = FxFilesystem::new_empty(device).await.expect("new_empty failed");
1110        fs.close().await.expect("Close failed");
1111        let device = fs.take_device().await;
1112        device.reopen(false);
1113
1114        let a = SuperBlockHeader::read_header(device.clone(), SuperBlockInstance::A)
1115            .await
1116            .expect("read failed");
1117        let b = SuperBlockHeader::read_header(device.clone(), SuperBlockInstance::B)
1118            .await
1119            .expect("read failed");
1120
1121        assert_eq!(a.0.guid, b.0.guid);
1122        assert_ne!(old_guid, a.0.guid);
1123    }
1124
1125    #[fuchsia::test]
1126    async fn test_alternating_super_blocks() {
1127        let device = DeviceHolder::new(FakeDevice::new(8192, TEST_DEVICE_BLOCK_SIZE.get() as u32));
1128
1129        let fs = FxFilesystem::new_empty(device).await.expect("new_empty failed");
1130        fs.close().await.expect("Close failed");
1131        let device = fs.take_device().await;
1132        device.reopen(false);
1133
1134        let (super_block_header_a, _) =
1135            SuperBlockHeader::read_header(device.clone(), SuperBlockInstance::A)
1136                .await
1137                .expect("read failed");
1138
1139        // The second super-block won't be valid at this time so there's no point reading it.
1140
1141        let fs = FxFilesystem::open(device).await.expect("open failed");
1142        let root_store = fs.root_store();
1143        // Generate enough work to induce a journal flush.
1144        for _ in 0..6000 {
1145            let mut transaction = fs
1146                .root_store()
1147                .new_transaction(lock_keys![], Options::default())
1148                .await
1149                .expect("new_transaction failed");
1150            ObjectStore::create_object(
1151                &root_store,
1152                &mut transaction,
1153                HandleOptions::default(),
1154                None,
1155            )
1156            .await
1157            .expect("create_object failed");
1158            transaction.commit().await.expect("commit failed");
1159        }
1160        fs.close().await.expect("Close failed");
1161        let device = fs.take_device().await;
1162        device.reopen(false);
1163
1164        let (super_block_header_a_after, _) =
1165            SuperBlockHeader::read_header(device.clone(), SuperBlockInstance::A)
1166                .await
1167                .expect("read failed");
1168        let (super_block_header_b_after, _) =
1169            SuperBlockHeader::read_header(device.clone(), SuperBlockInstance::B)
1170                .await
1171                .expect("read failed");
1172
1173        // It's possible that multiple super-blocks were written, so cater for that.
1174
1175        // The generations should be one apart.
1176        assert_eq!(
1177            (super_block_header_b_after.generation as i64
1178                - super_block_header_a_after.generation as i64)
1179                .abs(),
1180            1
1181        );
1182
1183        // At least one super-block should have been written.
1184        assert!(
1185            std::cmp::max(
1186                super_block_header_a_after.generation,
1187                super_block_header_b_after.generation
1188            ) > super_block_header_a.generation
1189        );
1190
1191        // They should have the same oddness.
1192        assert_eq!(super_block_header_a_after.generation & 1, super_block_header_a.generation & 1);
1193    }
1194
1195    #[fuchsia::test]
1196    async fn test_root_parent_is_compacted() {
1197        let device = DeviceHolder::new(FakeDevice::new(8192, TEST_DEVICE_BLOCK_SIZE.get() as u32));
1198
1199        let fs = FxFilesystem::new_empty(device).await.expect("new_empty failed");
1200
1201        let mut transaction = fs
1202            .root_store()
1203            .new_transaction(lock_keys![], Options::default())
1204            .await
1205            .expect("new_transaction failed");
1206        let store = fs.root_parent_store();
1207        let handle =
1208            ObjectStore::create_object(&store, &mut transaction, HandleOptions::default(), None)
1209                .await
1210                .expect("create_object failed");
1211        store.add_to_graveyard(&mut transaction, handle.object_id());
1212        transaction.commit().await.expect("commit failed");
1213
1214        store
1215            .tombstone_object(handle.object_id(), Options::default(), None)
1216            .await
1217            .expect("tombstone failed");
1218
1219        // Generate enough work to induce a journal flush.
1220        let root_store = fs.root_store();
1221        for _ in 0..6000 {
1222            let mut transaction = fs
1223                .root_store()
1224                .new_transaction(lock_keys![], Options::default())
1225                .await
1226                .expect("new_transaction failed");
1227            ObjectStore::create_object(
1228                &root_store,
1229                &mut transaction,
1230                HandleOptions::default(),
1231                None,
1232            )
1233            .await
1234            .expect("create_object failed");
1235            transaction.commit().await.expect("commit failed");
1236        }
1237
1238        // The root parent store should have been compacted, so we shouldn't be able to find any
1239        // record referring to the object we tombstoned.
1240        assert!(
1241            !store
1242                .tree()
1243                .exists(&ObjectKey::object(handle.object_id()))
1244                .await
1245                .expect("exists failed")
1246        );
1247    }
1248
1249    #[fuchsia::test]
1250    async fn test_invalid_object_ids_validation() {
1251        const BLOCK_SIZE: BlockSize = BlockSize::new(MIN_SUPER_BLOCK_SIZE as u32).unwrap();
1252        let device =
1253            DeviceHolder::new(FakeDevice::new(TEST_DEVICE_BLOCK_COUNT, BLOCK_SIZE.get() as u32));
1254        let fs = FxFilesystem::new_empty(device).await.expect("new_empty failed");
1255        fs.close().await.expect("close");
1256        let device = fs.take_device().await;
1257        device.reopen(false);
1258
1259        // Helper to write a superblock with specific object IDs.
1260        let device_arc = (*device).clone();
1261        let write_sb = |instance: SuperBlockInstance,
1262                        root_parent_store_object_id: u64,
1263                        root_parent_graveyard_directory_object_id: u64,
1264                        root_store_object_id: u64,
1265                        allocator_object_id: u64,
1266                        journal_object_id: u64| {
1267            let device = device_arc.clone();
1268            async move {
1269                let mut super_block_header = SuperBlockHeader::new(
1270                    1, // generation
1271                    root_parent_store_object_id,
1272                    root_parent_graveyard_directory_object_id,
1273                    root_store_object_id,
1274                    allocator_object_id,
1275                    journal_object_id,
1276                    JournalCheckpoint::default(),
1277                    LATEST_VERSION,
1278                );
1279                super_block_header.journal_checkpoint.version = LATEST_VERSION;
1280
1281                let mut writer = JournalWriter::new(BLOCK_SIZE, 0);
1282                writer.write_all(SUPER_BLOCK_MAGIC).unwrap();
1283                super_block_header.serialize_with_version(&mut writer).unwrap();
1284                SuperBlockRecord::End.serialize_into(&mut writer).unwrap();
1285                writer.pad_to_block().unwrap();
1286
1287                let mut buf = device.allocate_buffer(writer.flushable_bytes()).await;
1288                writer.take_flushable(buf.as_mut());
1289                device
1290                    .write(instance.first_extent().start, buf.as_ref())
1291                    .await
1292                    .expect("write failed");
1293            }
1294        };
1295
1296        let manager = SuperBlockManager::new();
1297
1298        // Case 1: Duplicate store IDs (3, 3)
1299        write_sb(SuperBlockInstance::A, 3, 4, 3, 5, 6).await;
1300        write_sb(SuperBlockInstance::B, 3, 4, 3, 5, 6).await;
1301        assert!(manager.load((*device).clone(), BLOCK_SIZE).await.is_err());
1302
1303        // Case 2: Allocator matches root_parent_store_object_id (3, 3)
1304        write_sb(SuperBlockInstance::A, 3, 4, 5, 3, 6).await;
1305        write_sb(SuperBlockInstance::B, 3, 4, 5, 3, 6).await;
1306        assert!(manager.load((*device).clone(), BLOCK_SIZE).await.is_err());
1307
1308        // Case 3: Allocator matches root_store_object_id (5, 5)
1309        write_sb(SuperBlockInstance::A, 3, 4, 5, 5, 6).await;
1310        write_sb(SuperBlockInstance::B, 3, 4, 5, 5, 6).await;
1311        assert!(manager.load((*device).clone(), BLOCK_SIZE).await.is_err());
1312
1313        // Case 4: Duplicate objects in root_parent_store (graveyard 4, journal 4)
1314        write_sb(SuperBlockInstance::A, 3, 4, 5, 6, 4).await;
1315        write_sb(SuperBlockInstance::B, 3, 4, 5, 6, 4).await;
1316        assert!(manager.load((*device).clone(), BLOCK_SIZE).await.is_err());
1317
1318        // Case 5: Valid configuration
1319        write_sb(SuperBlockInstance::A, 3, 4, 5, 6, 7).await;
1320        write_sb(SuperBlockInstance::B, 3, 4, 5, 6, 7).await;
1321        assert!(manager.load((*device).clone(), BLOCK_SIZE).await.is_ok());
1322    }
1323
1324    #[fuchsia::test]
1325    async fn test_save_failure_does_not_advance_next_instance() {
1326        const BLOCK_SIZE: BlockSize = BlockSize::SIZE_4KIB;
1327        let fail_writes = Arc::new(AtomicBool::new(false));
1328        let fail_writes_clone = fail_writes.clone();
1329        let mut fake_device = FakeDevice::new(TEST_DEVICE_BLOCK_COUNT, BLOCK_SIZE.get() as u32);
1330        fake_device.set_op_callback(move |op| match op {
1331            Op::Write if fail_writes_clone.load(Ordering::Relaxed) => {
1332                bail!("Injected write error");
1333            }
1334            _ => Ok(()),
1335        });
1336
1337        let device = DeviceHolder::new(fake_device);
1338        let fs = FxFilesystem::new_empty(device).await.expect("new_empty failed");
1339        fs.close().await.expect("close failed");
1340        let device = fs.take_device().await;
1341        device.reopen(false);
1342
1343        let manager = SuperBlockManager::new();
1344        let (header, _) = manager.load((*device).clone(), BLOCK_SIZE).await.expect("load failed");
1345        let fs = FxFilesystem::open(device).await.expect("open failed");
1346
1347        // The loaded superblock is B (highest generation), so next_instance should be A.
1348        assert_eq!(*manager.next_instance.lock(), SuperBlockInstance::A);
1349
1350        // Make write operations fail on the device.
1351        fail_writes.store(true, Ordering::Relaxed);
1352
1353        // Attempting to save should fail.
1354        assert!(
1355            manager
1356                .save(header.clone(), (*fs).clone(), fs.root_parent_store().tree().layer_set())
1357                .await
1358                .is_err()
1359        );
1360
1361        // next_instance must not have been advanced to B because the write failed.
1362        assert_eq!(*manager.next_instance.lock(), SuperBlockInstance::A);
1363
1364        // Clear the error condition so writes succeed.
1365        fail_writes.store(false, Ordering::Relaxed);
1366
1367        // Saving should now succeed and write to A, advancing next_instance to B.
1368        manager
1369            .save(header, (*fs).clone(), fs.root_parent_store().tree().layer_set())
1370            .await
1371            .expect("save failed");
1372        assert_eq!(*manager.next_instance.lock(), SuperBlockInstance::B);
1373    }
1374}