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}