Skip to main content

fs_ext4/
superblock.rs

1//! ext4 superblock parsing.
2//!
3//! Spec: docs/ext4-spec/superblock.md
4//! Located at byte offset 1024, 1024 bytes long. Magic 0xEF53 at offset 56.
5
6use crate::block_io::BlockDevice;
7use crate::error::{Error, Result};
8
9pub const SUPERBLOCK_OFFSET: u64 = 1024;
10pub const SUPERBLOCK_SIZE: usize = 1024;
11pub const EXT4_MAGIC: u16 = 0xEF53;
12
13/// `s_state` bits (byte offset 0x3A). The kernel sets `VALID_FS` when a
14/// clean unmount completes and clears it on mount; a dirty value on a
15/// not-currently-mounted image therefore indicates an unclean shutdown
16/// and signals the caller that journal replay (or `fsck`) is required
17/// before writes are safe.
18pub const EXT4_VALID_FS: u16 = 0x0001;
19pub const EXT4_ERROR_FS: u16 = 0x0002;
20
21/// Parsed in-memory representation of the ext4 superblock.
22/// Field names mirror the kernel's `struct ext4_super_block` (s_ prefix dropped).
23#[derive(Debug, Clone)]
24pub struct Superblock {
25    pub inodes_count: u32,
26    pub blocks_count: u64, // combined lo + hi
27    pub free_blocks_count: u64,
28    pub free_inodes_count: u32,
29    /// `s_r_blocks_count` (lo at 0x08, hi at 0x154 — 64-bit on
30    /// INCOMPAT_64BIT volumes). Blocks reserved for the superuser
31    /// — explains the gap between `free_blocks` and what `df`
32    /// reports as available to a normal user.
33    pub r_blocks_count: u64,
34    pub first_data_block: u32,
35    pub log_block_size: u32,
36    pub blocks_per_group: u32,
37    pub inodes_per_group: u32,
38    pub magic: u16,
39    /// `s_state` (0x3A). `EXT4_VALID_FS` = cleanly unmounted. Any other
40    /// value means the FS was mounted and not cleanly unmounted (dirty)
41    /// or that the kernel marked the FS as having errors.
42    pub state: u16,
43    /// `s_errors` (0x3C). Kernel error policy: 1=continue, 2=remount-ro,
44    /// 3=panic. Informational from a Swift host's POV but useful in
45    /// diagnostics output.
46    pub errors_behavior: u16,
47    /// `s_minor_rev_level` (0x3E). Bumped by filesystem admin tools for minor format
48    /// tweaks within a major rev_level.
49    pub minor_rev_level: u16,
50    pub rev_level: u32,
51    pub inode_size: u16,
52    /// `s_first_ino` (0x54, dynamic-rev only). First non-reserved
53    /// inode number; defaults to 11 on rev_level=1+ filesystems.
54    pub first_inode: u32,
55    pub feature_compat: u32,
56    pub feature_incompat: u32,
57    pub feature_ro_compat: u32,
58    pub uuid: [u8; 16],
59    pub volume_name: String,
60    /// `s_last_mounted` (0x88, 64 bytes). Last directory the FS was
61    /// mounted at — handy for diagnostics ("when was this disk last
62    /// in another machine?").
63    pub last_mounted: String,
64    pub desc_size: u16, // BGD size: 32 or 64
65    /// `s_reserved_gdt_blocks` — blocks held back after the group
66    /// descriptor table so the filesystem can be grown online.
67    ///
68    /// They sit between the GDT and the block bitmap in every group that
69    /// carries a superblock backup, and they are **not** free space:
70    /// anything that rebuilds a block bitmap has to mark them used or
71    /// the next allocation writes over the filesystem's own room to
72    /// expand.
73    pub reserved_gdt_blocks: u16,
74    /// `s_backup_bgs` — the only two groups carrying backups when
75    /// `SPARSE_SUPER2` is set. Meaningless without that feature.
76    pub backup_bgs: [u32; 2],
77    pub hash_seed: [u32; 4],
78    pub default_hash_version: u8,
79    pub checksum_seed: u32, // s_checksum_seed (used when INCOMPAT_CSUM_SEED)
80    pub journal_inode: u32,
81    /// `s_last_orphan` (0xE8). Head of the orphan-inode list — inodes
82    /// whose link count reached zero while still open. The kernel
83    /// inserts unlink-while-open targets here so that, on the next
84    /// mount, recovery can reclaim them. Each inode's `i_dtime` field
85    /// is overloaded to point at the next orphan in the chain; the
86    /// chain terminates with a zero `dtime`.
87    pub last_orphan: u32,
88    /// `s_mtime` (0x2C). Timestamp the FS was last mounted.
89    pub mtime: u32,
90    /// `s_wtime` (0x30). Timestamp the FS was last written to.
91    pub wtime: u32,
92    /// `s_mnt_count` (0x34). Mounts since last fsck.
93    pub mnt_count: u16,
94    /// `s_max_mnt_count` (0x36). Forced fsck after this many mounts;
95    /// 0 disables.
96    pub max_mnt_count: u16,
97    /// `s_lastcheck` (0x40). Timestamp of the last fsck pass.
98    pub lastcheck: u32,
99    /// `s_checkinterval` (0x44). Seconds between forced fscks; 0
100    /// disables time-based forced fsck.
101    pub checkinterval: u32,
102    /// `s_creator_os` (0x48). 0=Linux, 1=Hurd, 2=Masix, 3=FreeBSD,
103    /// 4=Lites.
104    pub creator_os: u32,
105    /// `s_def_resuid` (0x50). UID with access to reserved blocks.
106    pub def_resuid: u16,
107    /// `s_def_resgid` (0x52). GID with access to reserved blocks.
108    pub def_resgid: u16,
109    pub raw: Vec<u8>, // keep raw bytes for re-checksum on writes (future)
110}
111
112/// The classic `RO_COMPAT_SPARSE_SUPER` layout: groups 0 and 1, and
113/// every power of 3, 5 or 7.
114///
115/// Separate from [`Superblock::group_has_super`] because `mkfs` needs the
116/// rule without a filesystem to ask — it is deciding what to write, not
117/// reading what someone else wrote.
118pub(crate) fn classic_sparse_super(g: u64) -> bool {
119    fn is_power_of(mut g: u64, base: u64) -> bool {
120        while g.is_multiple_of(base) {
121            g /= base;
122        }
123        g == 1
124    }
125    g <= 1 || is_power_of(g, 3) || is_power_of(g, 5) || is_power_of(g, 7)
126}
127
128/// The largest block ext4 defines, as its base-2 log over 1024.
129///
130/// 6, which is a 64 KiB block.
131pub const MAX_LOG_BLOCK_SIZE: u32 = 6;
132
133/// The first inode a filesystem may hand out, before `s_first_ino`.
134///
135/// 11. Inodes 1 through 10 are the filesystem's own -- 2 is the root
136/// directory and 8 the journal -- and the kernel refuses a superblock
137/// whose `s_first_ino` is below it.
138pub const GOOD_OLD_FIRST_INODE: u32 = 11;
139
140impl Superblock {
141    /// Read and parse the superblock from a block device.
142    pub fn read<D: BlockDevice + ?Sized>(dev: &D) -> Result<Self> {
143        let mut buf = vec![0u8; SUPERBLOCK_SIZE];
144        dev.read_at(SUPERBLOCK_OFFSET, &mut buf)?;
145        Self::parse(buf)
146    }
147
148    pub fn parse(raw: Vec<u8>) -> Result<Self> {
149        if raw.len() < SUPERBLOCK_SIZE {
150            return Err(Error::Corrupt("superblock buffer too small"));
151        }
152
153        let magic = u16::from_le_bytes([raw[0x38], raw[0x39]]);
154        if magic != EXT4_MAGIC {
155            return Err(Error::BadMagic {
156                found: magic,
157                expected: EXT4_MAGIC,
158            });
159        }
160
161        let inodes_count = u32::from_le_bytes(raw[0x00..0x04].try_into().unwrap());
162        let blocks_count_lo = u32::from_le_bytes(raw[0x04..0x08].try_into().unwrap());
163        let r_blocks_count_lo = u32::from_le_bytes(raw[0x08..0x0C].try_into().unwrap());
164        let free_blocks_count_lo = u32::from_le_bytes(raw[0x0C..0x10].try_into().unwrap());
165        let free_inodes_count = u32::from_le_bytes(raw[0x10..0x14].try_into().unwrap());
166        let first_data_block = u32::from_le_bytes(raw[0x14..0x18].try_into().unwrap());
167        let log_block_size = u32::from_le_bytes(raw[0x18..0x1C].try_into().unwrap());
168        let blocks_per_group = u32::from_le_bytes(raw[0x20..0x24].try_into().unwrap());
169        let inodes_per_group = u32::from_le_bytes(raw[0x28..0x2C].try_into().unwrap());
170        let mtime = u32::from_le_bytes(raw[0x2C..0x30].try_into().unwrap());
171        let wtime = u32::from_le_bytes(raw[0x30..0x34].try_into().unwrap());
172        let mnt_count = u16::from_le_bytes(raw[0x34..0x36].try_into().unwrap());
173        let max_mnt_count = u16::from_le_bytes(raw[0x36..0x38].try_into().unwrap());
174        let state = u16::from_le_bytes(raw[0x3A..0x3C].try_into().unwrap());
175        let errors_behavior = u16::from_le_bytes(raw[0x3C..0x3E].try_into().unwrap());
176        let minor_rev_level = u16::from_le_bytes(raw[0x3E..0x40].try_into().unwrap());
177        let lastcheck = u32::from_le_bytes(raw[0x40..0x44].try_into().unwrap());
178        let checkinterval = u32::from_le_bytes(raw[0x44..0x48].try_into().unwrap());
179        let creator_os = u32::from_le_bytes(raw[0x48..0x4C].try_into().unwrap());
180        let rev_level = u32::from_le_bytes(raw[0x4C..0x50].try_into().unwrap());
181        let def_resuid = u16::from_le_bytes(raw[0x50..0x52].try_into().unwrap());
182        let def_resgid = u16::from_le_bytes(raw[0x52..0x54].try_into().unwrap());
183
184        // Dynamic-rev fields (rev_level >= 1). Pre-rev1 filesystems
185        // pin sensible defaults — `inode_size = 128` (the historical
186        // ext2 size), `first_inode = 11` (the spec-defined start of
187        // user-visible inodes; lower numbers are reserved).
188        // A `s_first_ino` below 11 would hand the reserved inodes -- 2
189        // is the root directory, 8 the journal -- to the allocator, so
190        // the kernel refuses one and this takes the floor rather than
191        // the field. Nothing legitimate writes a lower value; a
192        // filesystem that does is claiming its own root is available.
193        let first_inode = if rev_level >= 1 {
194            u32::from_le_bytes(raw[0x54..0x58].try_into().unwrap()).max(GOOD_OLD_FIRST_INODE)
195        } else {
196            GOOD_OLD_FIRST_INODE
197        };
198        let inode_size = if rev_level >= 1 {
199            u16::from_le_bytes(raw[0x58..0x5A].try_into().unwrap())
200        } else {
201            128
202        };
203        let feature_compat = if rev_level >= 1 {
204            u32::from_le_bytes(raw[0x5C..0x60].try_into().unwrap())
205        } else {
206            0
207        };
208        let feature_incompat = if rev_level >= 1 {
209            u32::from_le_bytes(raw[0x60..0x64].try_into().unwrap())
210        } else {
211            0
212        };
213        let feature_ro_compat = if rev_level >= 1 {
214            u32::from_le_bytes(raw[0x64..0x68].try_into().unwrap())
215        } else {
216            0
217        };
218
219        // s_reserved_gdt_blocks at 0xCE, and s_backup_bgs at 0x274. Both
220        // are zero on a revision-0 filesystem, which has neither.
221        let reserved_gdt_blocks = if rev_level >= 1 {
222            u16::from_le_bytes(raw[0xCE..0xD0].try_into().unwrap())
223        } else {
224            0
225        };
226        let backup_bgs = if rev_level >= 1 && raw.len() >= 0x27C {
227            [
228                u32::from_le_bytes(raw[0x274..0x278].try_into().unwrap()),
229                u32::from_le_bytes(raw[0x278..0x27C].try_into().unwrap()),
230            ]
231        } else {
232            [0, 0]
233        };
234
235        let mut uuid = [0u8; 16];
236        uuid.copy_from_slice(&raw[0x68..0x78]);
237
238        let volume_name_bytes = &raw[0x78..0x88];
239        let nul = volume_name_bytes.iter().position(|&b| b == 0).unwrap_or(16);
240        let volume_name = String::from_utf8_lossy(&volume_name_bytes[..nul]).into_owned();
241
242        // s_last_mounted at 0x88, 64 bytes. The kernel writes the path
243        // here on every successful mount; a freshly mkfs'd filesystem
244        // leaves it zero-padded.
245        let last_mounted_bytes = &raw[0x88..0xC8];
246        let nul = last_mounted_bytes
247            .iter()
248            .position(|&b| b == 0)
249            .unwrap_or(64);
250        let last_mounted = String::from_utf8_lossy(&last_mounted_bytes[..nul]).into_owned();
251
252        let desc_size = u16::from_le_bytes(raw[0xFE..0x100].try_into().unwrap());
253        // If desc_size is 0, default to 32 (legacy); spec says 32 or 64
254        let desc_size = if desc_size == 0 { 32 } else { desc_size };
255
256        let mut hash_seed = [0u32; 4];
257        for (i, slot) in hash_seed.iter_mut().enumerate() {
258            let off = 0xEC + i * 4;
259            *slot = u32::from_le_bytes(raw[off..off + 4].try_into().unwrap());
260        }
261        let default_hash_version = raw[0xFC];
262
263        // 64-bit fields (only valid when INCOMPAT_64BIT). Pre-64bit
264        // filesystems leave the high halves zero so combining is safe
265        // unconditionally.
266        let blocks_count_hi = u32::from_le_bytes(raw[0x150..0x154].try_into().unwrap());
267        let r_blocks_count_hi = u32::from_le_bytes(raw[0x154..0x158].try_into().unwrap());
268        let free_blocks_count_hi = u32::from_le_bytes(raw[0x158..0x15C].try_into().unwrap());
269
270        let blocks_count = ((blocks_count_hi as u64) << 32) | (blocks_count_lo as u64);
271        let r_blocks_count = ((r_blocks_count_hi as u64) << 32) | (r_blocks_count_lo as u64);
272        let free_blocks_count =
273            ((free_blocks_count_hi as u64) << 32) | (free_blocks_count_lo as u64);
274
275        let checksum_seed = u32::from_le_bytes(raw[0x270..0x274].try_into().unwrap());
276        let journal_inode = u32::from_le_bytes(raw[0xE0..0xE4].try_into().unwrap());
277        let last_orphan = u32::from_le_bytes(raw[0xE8..0xEC].try_into().unwrap());
278
279        // Reject impossible geometry early so downstream arithmetic never
280        // divides by zero. All three are required for the filesystem to
281        // name even a single block or inode.
282        if blocks_per_group == 0 {
283            return Err(Error::Corrupt("superblock: blocks_per_group == 0"));
284        }
285        if inodes_per_group == 0 {
286            return Err(Error::Corrupt("superblock: inodes_per_group == 0"));
287        }
288        if inode_size == 0 {
289            return Err(Error::Corrupt("superblock: inode_size == 0"));
290        }
291        // ext4 tops out at a 64 KiB block, which is `log_block_size = 6`.
292        // The guard used to admit 20, a 1 GiB block, on the argument that
293        // anything larger was "certainly a corrupt field" -- which is
294        // true of 20 as well. Every `vec![0u8; block_size]` in this
295        // crate is sized by this field, and `mount_inner` wraps the
296        // device in a 256-entry cache, so a 1 GiB block is 256 GiB of
297        // resident memory from a sparse image; three `read_block` calls
298        // allocated and zero-filled 3 GiB in five seconds. It is also
299        // what makes `ppb * ppb * ppb` overflow in the indirect-block
300        // map.
301        if log_block_size > MAX_LOG_BLOCK_SIZE {
302            return Err(Error::Corrupt(
303                "superblock: log_block_size exceeds the largest ext4 block",
304            ));
305        }
306        // 32 bytes without the 64BIT feature, 64 or more with it, and a
307        // power of two either way -- the kernel's own rule. The parser
308        // reads fixed offsets up to 0x20, and up to 0x3C when the field
309        // says 64, so a smaller value indexes past the buffer: 8 gave
310        // "range end index 12 out of range for slice of length 8" during
311        // mount.
312        let sixty_four_bit = feature_incompat & crate::features::Incompat::BIT64.bits() != 0;
313        let smallest = if sixty_four_bit { 64 } else { 32 };
314        if desc_size < smallest || !desc_size.is_power_of_two() {
315            return Err(Error::Corrupt(
316                "superblock: desc_size is not a group descriptor size",
317            ));
318        }
319        if blocks_count == 0 {
320            return Err(Error::Corrupt("superblock: blocks_count == 0"));
321        }
322
323        Ok(Self {
324            inodes_count,
325            blocks_count,
326            free_blocks_count,
327            free_inodes_count,
328            r_blocks_count,
329            first_data_block,
330            log_block_size,
331            blocks_per_group,
332            inodes_per_group,
333            magic,
334            state,
335            errors_behavior,
336            minor_rev_level,
337            rev_level,
338            inode_size,
339            first_inode,
340            feature_compat,
341            feature_incompat,
342            feature_ro_compat,
343            uuid,
344            volume_name,
345            last_mounted,
346            desc_size,
347            reserved_gdt_blocks,
348            backup_bgs,
349            hash_seed,
350            default_hash_version,
351            checksum_seed,
352            journal_inode,
353            last_orphan,
354            mtime,
355            wtime,
356            mnt_count,
357            max_mnt_count,
358            lastcheck,
359            checkinterval,
360            creator_os,
361            def_resuid,
362            def_resgid,
363            raw,
364        })
365    }
366
367    /// Whether the filesystem was cleanly unmounted. `false` here means
368    /// the FS was not cleanly unmounted and a journal replay (or fsck)
369    /// is required before writes are safe. Read-only consumers can
370    /// still mount a dirty FS; callers that intend to write should
371    /// surface this to the user and either run fsck or refuse to
372    /// mount read-write.
373    pub fn is_clean(&self) -> bool {
374        self.state & EXT4_VALID_FS != 0
375    }
376
377    /// Whether block group `g` carries a superblock and group-descriptor
378    /// backup.
379    ///
380    /// Three layouts, and the filesystem's own flags decide which:
381    ///
382    /// - **`SPARSE_SUPER2`**: group 0, and the two groups named by
383    ///   `s_backup_bgs`. Nothing else.
384    /// - **`SPARSE_SUPER` clear**: every group. This is the old ext2
385    ///   layout, and the one that costs most to get wrong — assuming the
386    ///   sparse rule leaves real backups looking like free space.
387    /// - **otherwise**: the classic sparse rule — groups 0 and 1, and
388    ///   every power of 3, 5 or 7.
389    ///
390    /// Answering unconditionally with the classic rule is wrong in both
391    /// of the first two cases, and wrong in the direction that matters:
392    /// on a filesystem without `SPARSE_SUPER` it reports "no backup here"
393    /// for groups that have one, so a rebuilt bitmap offers the backup
394    /// superblock and its descriptor table as free blocks.
395    pub fn group_has_super(&self, g: u64) -> bool {
396        use crate::features::{Compat, RoCompat};
397
398        if g == 0 {
399            return true;
400        }
401        if self.feature_compat & Compat::SPARSE_SUPER2.bits() != 0 {
402            return self.backup_bgs.iter().any(|&b| u64::from(b) == g);
403        }
404        if self.feature_ro_compat & RoCompat::SPARSE_SUPER.bits() == 0 {
405            return true;
406        }
407        classic_sparse_super(g)
408    }
409
410    /// Block size in bytes: 1024 << log_block_size.
411    pub fn block_size(&self) -> u32 {
412        1024u32 << self.log_block_size
413    }
414
415    /// Number of block groups.
416    pub fn block_group_count(&self) -> u64 {
417        self.blocks_count.div_ceil(self.blocks_per_group as u64)
418    }
419
420    /// Whether the 64BIT incompat feature is enabled.
421    pub fn is_64bit(&self) -> bool {
422        self.feature_incompat & crate::features::Incompat::BIT64.bits() != 0
423    }
424}
425
426#[cfg(test)]
427mod backup_layout_tests {
428    use super::*;
429    use crate::features::{Compat, RoCompat};
430
431    /// A superblock with only the fields the backup-layout rule reads.
432    fn sb_with(compat: u32, ro_compat: u32, backup_bgs: [u32; 2], reserved_gdt: u16) -> Superblock {
433        let mut raw = vec![0u8; SUPERBLOCK_SIZE];
434        raw[0x38..0x3A].copy_from_slice(&EXT4_MAGIC.to_le_bytes());
435        raw[0x00..0x04].copy_from_slice(&8192u32.to_le_bytes()); // inodes_count
436        raw[0x04..0x08].copy_from_slice(&65536u32.to_le_bytes()); // blocks_count
437        raw[0x14..0x18].copy_from_slice(&1u32.to_le_bytes()); // first_data_block
438        raw[0x20..0x24].copy_from_slice(&8192u32.to_le_bytes()); // blocks_per_group
439        raw[0x28..0x2C].copy_from_slice(&2048u32.to_le_bytes()); // inodes_per_group
440        raw[0x4C..0x50].copy_from_slice(&1u32.to_le_bytes()); // rev_level
441        raw[0x58..0x5A].copy_from_slice(&256u16.to_le_bytes()); // inode_size
442        raw[0x5C..0x60].copy_from_slice(&compat.to_le_bytes());
443        raw[0x64..0x68].copy_from_slice(&ro_compat.to_le_bytes());
444        raw[0xCE..0xD0].copy_from_slice(&reserved_gdt.to_le_bytes());
445        raw[0xFE..0x100].copy_from_slice(&64u16.to_le_bytes()); // desc_size
446        raw[0x274..0x278].copy_from_slice(&backup_bgs[0].to_le_bytes());
447        raw[0x278..0x27C].copy_from_slice(&backup_bgs[1].to_le_bytes());
448        Superblock::parse(raw).expect("superblock")
449    }
450
451    /// The layout nearly every filesystem has: groups 0 and 1, then the
452    /// powers of 3, 5 and 7.
453    #[test]
454    fn the_classic_sparse_layout() {
455        let sb = sb_with(0, RoCompat::SPARSE_SUPER.bits(), [0, 0], 0);
456        for g in [0, 1, 3, 5, 7, 9, 25, 27, 49, 81, 125] {
457            assert!(sb.group_has_super(g), "group {g} should carry a backup");
458        }
459        for g in [2, 4, 6, 8, 10, 11, 26, 50, 100] {
460            assert!(!sb.group_has_super(g), "group {g} should not");
461        }
462    }
463
464    /// Without `SPARSE_SUPER`, every group carries one.
465    ///
466    /// This is the case that costs data rather than capacity: answering
467    /// with the sparse rule reports "no backup here" for groups that have
468    /// one, so rebuilding a bitmap offers a live backup superblock and its
469    /// descriptor table as free space.
470    #[test]
471    fn without_sparse_super_every_group_carries_a_backup() {
472        let sb = sb_with(0, 0, [0, 0], 0);
473        for g in 0..40 {
474            assert!(sb.group_has_super(g), "group {g} should carry a backup");
475        }
476    }
477
478    /// `SPARSE_SUPER2` names exactly two groups, and no rule applies
479    /// beyond them.
480    #[test]
481    fn sparse_super2_names_its_own_groups() {
482        let sb = sb_with(
483            Compat::SPARSE_SUPER2.bits(),
484            RoCompat::SPARSE_SUPER.bits(),
485            [4, 17],
486            0,
487        );
488        assert!(sb.group_has_super(0), "group 0 always carries one");
489        assert!(sb.group_has_super(4));
490        assert!(sb.group_has_super(17));
491        // Powers of 3, 5 and 7 are not special here, and group 1 is not
492        // either — which is what distinguishes this from the classic rule
493        // rather than merely narrowing it.
494        for g in [1, 3, 5, 7, 9, 25, 49] {
495            assert!(
496                !sb.group_has_super(g),
497                "group {g} is not named by s_backup_bgs and must not be treated as \
498                 carrying a backup"
499            );
500        }
501    }
502
503    /// The field that says how much room the filesystem keeps to grow.
504    #[test]
505    fn reserved_gdt_blocks_is_read() {
506        assert_eq!(
507            sb_with(0, RoCompat::SPARSE_SUPER.bits(), [0, 0], 1024).reserved_gdt_blocks,
508            1024
509        );
510        assert_eq!(
511            sb_with(0, RoCompat::SPARSE_SUPER.bits(), [0, 0], 0).reserved_gdt_blocks,
512            0
513        );
514    }
515}