Skip to main content

btrfs_core/
superblock.rs

1//! btrfs superblock (`btrfs_super_block`) parse + `sys_chunk_array` bootstrap.
2//!
3//! The primary superblock sits at **physical byte offset 65536 (0x10000)** of
4//! the device (NOT offset 0), in a [`BTRFS_SUPER_INFO_SIZE`]-byte block. All
5//! fields are little-endian. The magic `_BHRfS_M` lives at **offset 0x40 within
6//! the superblock** (0x40 = past the 32-byte csum + 16-byte fsid).
7//!
8//! Field offsets below were transcribed from the kernel/btrfs-progs on-disk
9//! `struct btrfs_super_block` and then **verified byte-for-byte against
10//! `btrfs inspect-internal dump-super -f` on the minted oracle image** (see
11//! `tests/data/README.md`). Each offset comment marks the field it decodes.
12
13use crate::bytes::{le_u16, le_u32, le_u64, u8_at};
14use crate::chunk::{SysChunk, SYS_CHUNK_ARRAY_OFFSET};
15use crate::crc::{superblock_crc_status, CsumType, BTRFS_CSUM_SIZE};
16use crate::error::BtrfsError;
17
18/// `BTRFS_SUPER_INFO_OFFSET` — the primary superblock's physical byte offset.
19pub const BTRFS_SUPER_INFO_OFFSET: u64 = 65536;
20
21/// `BTRFS_SUPER_INFO_SIZE` — the superblock block size (4096 bytes).
22pub const BTRFS_SUPER_INFO_SIZE: usize = 4096;
23
24/// The btrfs magic, ASCII `"_BHRfS_M"`, at superblock offset 0x40 (little-endian
25/// `u64` = `0x4D5F_5366_5248_425F`).
26pub const BTRFS_MAGIC: [u8; 8] = *b"_BHRfS_M";
27
28/// Offset of the magic within the superblock.
29const MAGIC_OFFSET: usize = 0x40;
30
31/// `BTRFS_FSID_SIZE` — the filesystem UUID width.
32pub const BTRFS_FSID_SIZE: usize = 16;
33
34/// `BTRFS_LABEL_SIZE` — the filesystem label field width.
35pub const BTRFS_LABEL_SIZE: usize = 256;
36
37/// Minimum bytes required to parse every scalar field this reader extracts. The
38/// last scalar is `log_root_level` at 0xc8 (+1 = 0xc9); the label and
39/// `sys_chunk_array` extend further but are bounds-checked individually so a
40/// short-but-past-0xc9 buffer still yields the geometry.
41const SB_MIN_LEN: usize = 0xc9;
42
43/// A parsed btrfs superblock — the filesystem geometry, tree-root logical
44/// addresses, checksum descriptor, and the decoded `sys_chunk_array` bootstrap
45/// map (the P1 seed for logical→physical translation).
46///
47/// `#[non_exhaustive]` so later phases add fields without a breaking change.
48#[derive(Debug, Clone, PartialEq, Eq)]
49#[non_exhaustive]
50pub struct Superblock {
51    /// The 8-byte magic at offset 0x40 — validated to equal [`BTRFS_MAGIC`].
52    pub magic: [u8; 8],
53    /// `csum` (offset 0x0) — the first 4 bytes of the 32-byte checksum field
54    /// (the crc32c digest for a crc32c filesystem), verbatim.
55    pub csum: [u8; 4],
56    /// `fsid` (offset 0x20) — the filesystem UUID.
57    pub fsid: [u8; BTRFS_FSID_SIZE],
58    /// `bytenr` (offset 0x30) — the physical byte address of THIS superblock
59    /// copy (65536 for the primary).
60    pub bytenr: u64,
61    /// `flags` (offset 0x38).
62    pub flags: u64,
63    /// `generation` (offset 0x48) — the transaction id this superblock committed
64    /// (the highest valid copy is the latest committed state — the CoW lever).
65    pub generation: u64,
66    /// `root` (offset 0x50) — logical address of the root tree.
67    pub root: u64,
68    /// `chunk_root` (offset 0x58) — logical address of the chunk tree.
69    pub chunk_root: u64,
70    /// `log_root` (offset 0x60) — logical address of the log tree (0 = none).
71    pub log_root: u64,
72    /// `total_bytes` (offset 0x70) — filesystem size in bytes.
73    pub total_bytes: u64,
74    /// `bytes_used` (offset 0x78) — bytes in use.
75    pub bytes_used: u64,
76    /// `root_dir_objectid` (offset 0x80) — the root directory objectid (6).
77    pub root_dir_objectid: u64,
78    /// `num_devices` (offset 0x88).
79    pub num_devices: u64,
80    /// `sectorsize` (offset 0x90) — data block size in bytes.
81    pub sectorsize: u32,
82    /// `nodesize` (offset 0x94) — B-tree node size in bytes.
83    pub nodesize: u32,
84    /// `stripesize` (offset 0x9c).
85    pub stripesize: u32,
86    /// `sys_chunk_array_size` (offset 0xa0) — valid byte length of the
87    /// `sys_chunk_array` bootstrap region.
88    pub sys_chunk_array_size: u32,
89    /// `chunk_root_generation` (offset 0xa4).
90    pub chunk_root_generation: u64,
91    /// `compat_flags` (offset 0xac).
92    pub compat_flags: u64,
93    /// `compat_ro_flags` (offset 0xb4).
94    pub compat_ro_flags: u64,
95    /// `incompat_flags` (offset 0xbc).
96    pub incompat_flags: u64,
97    /// `csum_type` (offset 0xc4) — decoded checksum algorithm.
98    pub csum_type: CsumType,
99    /// `root_level` (offset 0xc6) — B-tree level of the root tree (0 = leaf).
100    pub root_level: u8,
101    /// `chunk_root_level` (offset 0xc7) — B-tree level of the chunk tree.
102    pub chunk_root_level: u8,
103    /// `log_root_level` (offset 0xc8).
104    pub log_root_level: u8,
105    /// `label` (offset 0xab within `dev_item`… → absolute 0x12b) — the
106    /// NUL-trimmed filesystem label, lossily decoded from up to 256 bytes.
107    pub label: String,
108    /// The decoded `sys_chunk_array` bootstrap chunk map — the SYSTEM
109    /// block-group logical→physical entries needed to read the chunk tree in P1.
110    /// Full chunk-tree walking is P1; P0 decodes the bootstrap structs only.
111    pub sys_chunks: Vec<SysChunk>,
112    /// The crc32c status of this superblock over its `sectorsize`-byte block:
113    /// `Some(true)` if the stored csum verifies, `Some(false)` if it does not
114    /// (corrupt/tampered), or `None` for a non-crc32c checksum type (deferred).
115    /// **Non-fatal** — a bad csum does not fail the parse.
116    pub crc_valid: Option<bool>,
117}
118
119impl Superblock {
120    /// Parse a btrfs superblock from the start of `block`.
121    ///
122    /// `block` must begin at the superblock (the caller slices the 4096-byte
123    /// block at physical offset 65536; passing the whole `sectorsize` block lets
124    /// the crc32c cover the correct range). The magic is validated at offset
125    /// 0x40 (not 0) — a wrong-image identity error names the offending 8 bytes.
126    ///
127    /// # Errors
128    ///
129    /// - [`BtrfsError::BadMagic`] if offset 0x40 is not `_BHRfS_M` — the eight
130    ///   offending bytes are carried in the error.
131    /// - [`BtrfsError::Truncated`] if `block` is shorter than the scalar fields.
132    pub fn parse(block: &[u8]) -> Result<Self, BtrfsError> {
133        // Validate magic (at 0x40) before length so a wrong-image identity error
134        // names the offending bytes (fail loud with the value).
135        let mut magic = [0u8; 8];
136        for (i, b) in magic.iter_mut().enumerate() {
137            *b = u8_at(block, MAGIC_OFFSET + i);
138        }
139        if magic != BTRFS_MAGIC {
140            return Err(BtrfsError::BadMagic { bytes: magic });
141        }
142
143        if block.len() < SB_MIN_LEN {
144            return Err(BtrfsError::Truncated {
145                structure: "superblock",
146                need: SB_MIN_LEN,
147                have: block.len(),
148            });
149        }
150
151        // Offsets VERIFIED against `dump-super -f` on the oracle (README).
152        let mut csum = [0u8; 4];
153        for (i, b) in csum.iter_mut().enumerate() {
154            *b = u8_at(block, i);
155        }
156        let mut fsid = [0u8; BTRFS_FSID_SIZE];
157        for (i, b) in fsid.iter_mut().enumerate() {
158            *b = u8_at(block, 0x20 + i);
159        }
160
161        let sectorsize = le_u32(block, 0x90);
162        let csum_type = CsumType::from_code(le_u16(block, 0xc4));
163
164        // The csum covers `[BTRFS_CSUM_SIZE .. sectorsize]`. Clamp the covered
165        // length to the block we were actually handed so a short buffer verifies
166        // as `Some(false)` rather than reaching out of range.
167        let covered = (sectorsize as usize).min(block.len()).max(BTRFS_CSUM_SIZE);
168        let crc_valid = superblock_crc_status(csum_type, block, covered);
169
170        let sys_chunk_array_size = le_u32(block, 0xa0);
171        let sys_chunks = SysChunk::parse_array(block, SYS_CHUNK_ARRAY_OFFSET, sys_chunk_array_size);
172
173        let label = decode_label(block, 0x12b);
174
175        Ok(Self {
176            magic,
177            csum,
178            fsid,
179            bytenr: le_u64(block, 0x30),
180            flags: le_u64(block, 0x38),
181            generation: le_u64(block, 0x48),
182            root: le_u64(block, 0x50),
183            chunk_root: le_u64(block, 0x58),
184            log_root: le_u64(block, 0x60),
185            total_bytes: le_u64(block, 0x70),
186            bytes_used: le_u64(block, 0x78),
187            root_dir_objectid: le_u64(block, 0x80),
188            num_devices: le_u64(block, 0x88),
189            sectorsize,
190            nodesize: le_u32(block, 0x94),
191            stripesize: le_u32(block, 0x9c),
192            sys_chunk_array_size,
193            chunk_root_generation: le_u64(block, 0xa4),
194            compat_flags: le_u64(block, 0xac),
195            compat_ro_flags: le_u64(block, 0xb4),
196            incompat_flags: le_u64(block, 0xbc),
197            csum_type,
198            root_level: u8_at(block, 0xc6),
199            chunk_root_level: u8_at(block, 0xc7),
200            log_root_level: u8_at(block, 0xc8),
201            label,
202            sys_chunks,
203            crc_valid,
204        })
205    }
206
207    /// The filesystem UUID rendered as the canonical `8-4-4-4-12` string.
208    #[must_use]
209    pub fn fsid_string(&self) -> String {
210        uuid_string(&self.fsid)
211    }
212}
213
214/// Decode the NUL-terminated btrfs label at `off` (up to [`BTRFS_LABEL_SIZE`]
215/// bytes), lossily as UTF-8 and trimmed at the first NUL. Out-of-range yields an
216/// empty string (never panics).
217fn decode_label(block: &[u8], off: usize) -> String {
218    let end = off.saturating_add(BTRFS_LABEL_SIZE).min(block.len());
219    let raw = block.get(off..end).unwrap_or(&[]);
220    let trimmed = raw.split(|&b| b == 0).next().unwrap_or(&[]);
221    String::from_utf8_lossy(trimmed).into_owned()
222}
223
224/// Render a 16-byte UUID as the canonical `8-4-4-4-12` lower-hex string.
225fn uuid_string(u: &[u8; 16]) -> String {
226    let h: Vec<String> = u.iter().map(|b| format!("{b:02x}")).collect();
227    format!(
228        "{}{}{}{}-{}{}-{}{}-{}{}-{}{}{}{}{}{}",
229        h[0],
230        h[1],
231        h[2],
232        h[3],
233        h[4],
234        h[5],
235        h[6],
236        h[7],
237        h[8],
238        h[9],
239        h[10],
240        h[11],
241        h[12],
242        h[13],
243        h[14],
244        h[15]
245    )
246}