Skip to main content

fs_ext4/
bgd.rs

1//! Block group descriptor (BGD) parsing.
2//!
3//! Spec: kernel.org/doc/html/latest/filesystems/ext4/group_descr.html
4//!
5//! BGDs live in the block(s) immediately following the primary superblock.
6//! Each BGD is `superblock.desc_size` bytes (32 legacy, 64 with INCOMPAT_64BIT).
7
8use crate::block_io::BlockDevice;
9use crate::checksum::Checksummer;
10use crate::error::{Error, Result};
11use crate::superblock::Superblock;
12
13/// Block group descriptor (post-parse, all 64-bit fields combined lo+hi).
14#[derive(Debug, Clone, Copy)]
15pub struct BlockGroupDescriptor {
16    pub block_bitmap: u64,
17    pub inode_bitmap: u64,
18    pub inode_table: u64,
19    pub free_blocks_count: u32,
20    pub free_inodes_count: u32,
21    pub used_dirs_count: u32,
22    pub flags: u16,
23    pub itable_unused: u32,
24    pub block_bitmap_csum: u32,
25    pub inode_bitmap_csum: u32,
26    pub checksum: u16,
27}
28
29bitflags::bitflags! {
30    /// BGD flags (`bg_flags`).
31    #[derive(Debug, Clone, Copy)]
32    pub struct BgdFlags: u16 {
33        /// Inode table not initialized — skip reading (treat as all-free).
34        const INODE_UNINIT = 0x0001;
35        /// Block bitmap not initialized — treat as all blocks free.
36        const BLOCK_UNINIT = 0x0002;
37        /// Inode table is fully zeroed on disk.
38        const ITABLE_ZEROED = 0x0004;
39    }
40}
41
42impl BlockGroupDescriptor {
43    /// Parse one descriptor from a buffer of at least `desc_size` bytes.
44    pub fn parse(buf: &[u8], desc_size: u16) -> Result<Self> {
45        if buf.len() < desc_size as usize {
46            return Err(Error::Corrupt("bgd buffer too small"));
47        }
48
49        let block_bitmap_lo = u32::from_le_bytes(buf[0x00..0x04].try_into().unwrap());
50        let inode_bitmap_lo = u32::from_le_bytes(buf[0x04..0x08].try_into().unwrap());
51        let inode_table_lo = u32::from_le_bytes(buf[0x08..0x0C].try_into().unwrap());
52        let free_blocks_lo = u16::from_le_bytes(buf[0x0C..0x0E].try_into().unwrap());
53        let free_inodes_lo = u16::from_le_bytes(buf[0x0E..0x10].try_into().unwrap());
54        let used_dirs_lo = u16::from_le_bytes(buf[0x10..0x12].try_into().unwrap());
55        let flags = u16::from_le_bytes(buf[0x12..0x14].try_into().unwrap());
56        let block_bitmap_csum_lo = u16::from_le_bytes(buf[0x18..0x1A].try_into().unwrap());
57        let inode_bitmap_csum_lo = u16::from_le_bytes(buf[0x1A..0x1C].try_into().unwrap());
58        let itable_unused_lo = u16::from_le_bytes(buf[0x1C..0x1E].try_into().unwrap());
59        let checksum = u16::from_le_bytes(buf[0x1E..0x20].try_into().unwrap());
60
61        let (
62            block_bitmap_hi,
63            inode_bitmap_hi,
64            inode_table_hi,
65            free_blocks_hi,
66            free_inodes_hi,
67            used_dirs_hi,
68            itable_unused_hi,
69            block_bitmap_csum_hi,
70            inode_bitmap_csum_hi,
71        ) = if desc_size >= 64 {
72            (
73                u32::from_le_bytes(buf[0x20..0x24].try_into().unwrap()),
74                u32::from_le_bytes(buf[0x24..0x28].try_into().unwrap()),
75                u32::from_le_bytes(buf[0x28..0x2C].try_into().unwrap()),
76                u16::from_le_bytes(buf[0x2C..0x2E].try_into().unwrap()),
77                u16::from_le_bytes(buf[0x2E..0x30].try_into().unwrap()),
78                u16::from_le_bytes(buf[0x30..0x32].try_into().unwrap()),
79                u16::from_le_bytes(buf[0x32..0x34].try_into().unwrap()),
80                u16::from_le_bytes(buf[0x38..0x3A].try_into().unwrap()),
81                u16::from_le_bytes(buf[0x3A..0x3C].try_into().unwrap()),
82            )
83        } else {
84            (0, 0, 0, 0, 0, 0, 0, 0, 0)
85        };
86
87        Ok(Self {
88            block_bitmap: ((block_bitmap_hi as u64) << 32) | block_bitmap_lo as u64,
89            inode_bitmap: ((inode_bitmap_hi as u64) << 32) | inode_bitmap_lo as u64,
90            inode_table: ((inode_table_hi as u64) << 32) | inode_table_lo as u64,
91            free_blocks_count: ((free_blocks_hi as u32) << 16) | free_blocks_lo as u32,
92            free_inodes_count: ((free_inodes_hi as u32) << 16) | free_inodes_lo as u32,
93            used_dirs_count: ((used_dirs_hi as u32) << 16) | used_dirs_lo as u32,
94            flags,
95            itable_unused: ((itable_unused_hi as u32) << 16) | itable_unused_lo as u32,
96            block_bitmap_csum: ((block_bitmap_csum_hi as u32) << 16) | block_bitmap_csum_lo as u32,
97            inode_bitmap_csum: ((inode_bitmap_csum_hi as u32) << 16) | inode_bitmap_csum_lo as u32,
98            checksum,
99        })
100    }
101
102    pub fn flags(&self) -> BgdFlags {
103        BgdFlags::from_bits_truncate(self.flags)
104    }
105}
106
107/// Read all block group descriptors for the filesystem.
108///
109/// When `csum.enabled`, each descriptor's CRC32C is verified; a mismatch
110/// returns `Error::BadChecksum { what: "block group descriptor" }`.
111pub fn read_all<D: BlockDevice + ?Sized>(
112    dev: &D,
113    sb: &Superblock,
114    csum: &Checksummer,
115) -> Result<Vec<BlockGroupDescriptor>> {
116    let block_size = sb.block_size() as u64;
117    // BGT starts at block (first_data_block + 1).
118    let bgt_block = sb.first_data_block as u64 + 1;
119    let bgt_offset = bgt_block * block_size;
120
121    let group_count = sb.block_group_count();
122    // THE TABLE HAS TO BE INSIDE THE FILESYSTEM.
123    //
124    // `group_count` is `blocks_count / blocks_per_group`, both raw
125    // superblock fields, and `desc_size` is another. blocks_count 2^40
126    // with blocks_per_group 1 asked for 72 petabytes -- an allocation
127    // that aborts through `handle_alloc_error`, past `ffi_guard`'s
128    // `catch_unwind`, taking the host process with it. blocks_count
129    // u64::MAX wrapped the multiply instead and gave "capacity
130    // overflow". Both from a 4 MiB image, at mount.
131    let total_bytes = group_count
132        .checked_mul(sb.desc_size as u64)
133        .ok_or(Error::Corrupt(
134            "superblock: the group descriptor table's size overflows",
135        ))?;
136    let end = bgt_offset.checked_add(total_bytes).ok_or(Error::Corrupt(
137        "superblock: the group descriptor table's extent overflows",
138    ))?;
139    if end > dev.size_bytes() {
140        return Err(Error::Corrupt(
141            "superblock: the group descriptor table reaches past the end of the device",
142        ));
143    }
144
145    let mut buf = vec![0u8; total_bytes as usize];
146    dev.read_at(bgt_offset, &mut buf)?;
147
148    let mut groups = Vec::with_capacity(group_count as usize);
149    for i in 0..(group_count as usize) {
150        let off = i * sb.desc_size as usize;
151        let raw = &buf[off..off + sb.desc_size as usize];
152        if csum.enabled && !csum.verify_bgd(i as u32, raw, sb.desc_size) {
153            return Err(Error::BadChecksum {
154                what: "block group descriptor",
155            });
156        }
157        let bgd = BlockGroupDescriptor::parse(raw, sb.desc_size)?;
158        // THE THREE POINTERS ARE BLOCK NUMBERS IN THIS FILESYSTEM.
159        //
160        // `bg_block_bitmap`, `bg_inode_bitmap` and `bg_inode_table` are
161        // assembled lo|hi into a `u64` and were never checked against
162        // anything. Every one of them is a WRITE target:
163        // `set_block_run_used` and `free_block_run` write a whole block
164        // at the bitmaps, and `locate_inode` hands the table block to
165        // `write_inode_raw`, which writes an inode image there. A
166        // descriptor pointing outside the filesystem is trivially
167        // satisfied on an FSKit mount, where the device is larger than
168        // `s_blocks_count`, and `bg_block_bitmap_hi` set high also
169        // wrapped the unchecked `bitmap_block * block_size` at the use
170        // site. The only comparable check lived in fsck-only code.
171        //
172        // The inode table spans `inodes_per_group * inode_size` bytes,
173        // so it is the one that has to fit as a range rather than a
174        // point.
175        let table_blocks = (u64::from(sb.inodes_per_group) * u64::from(sb.inode_size))
176            .div_ceil(u64::from(sb.block_size()));
177        let past_the_end = bgd.block_bitmap >= sb.blocks_count
178            || bgd.inode_bitmap >= sb.blocks_count
179            || bgd
180                .inode_table
181                .checked_add(table_blocks)
182                .is_none_or(|end| end > sb.blocks_count);
183        if past_the_end {
184            return Err(Error::Corrupt(
185                "a block group descriptor points outside the filesystem",
186            ));
187        }
188        groups.push(bgd);
189    }
190    Ok(groups)
191}
192
193/// Locate the inode table block + offset for a given inode number.
194/// Returns (physical block containing the inode, byte offset within block).
195pub fn locate_inode(
196    sb: &Superblock,
197    groups: &[BlockGroupDescriptor],
198    ino: u32,
199) -> Result<(u64, u32)> {
200    if ino == 0 || ino > sb.inodes_count {
201        return Err(Error::InvalidInode(ino));
202    }
203    if sb.inodes_per_group == 0 || sb.inode_size == 0 {
204        return Err(Error::Corrupt(
205            "superblock: zero inodes_per_group or inode_size",
206        ));
207    }
208    let group_idx = ((ino - 1) / sb.inodes_per_group) as usize;
209    let local_idx = ((ino - 1) % sb.inodes_per_group) as u64;
210
211    let bgd = groups.get(group_idx).ok_or(Error::InvalidInode(ino))?;
212    let block_size = sb.block_size() as u64;
213    let inode_size = sb.inode_size as u64;
214    if inode_size > block_size {
215        return Err(Error::Corrupt(
216            "superblock: inode_size larger than block_size",
217        ));
218    }
219    let inodes_per_block = block_size / inode_size;
220    if inodes_per_block == 0 {
221        return Err(Error::Corrupt("superblock: inode_size exceeds block_size"));
222    }
223
224    let block = bgd
225        .inode_table
226        .checked_add(local_idx / inodes_per_block)
227        .ok_or(Error::Corrupt("inode table block number overflow"))?;
228    let off_bytes = (local_idx % inodes_per_block) * inode_size;
229    let offset_in_block: u32 = off_bytes
230        .try_into()
231        .map_err(|_| Error::Corrupt("inode offset exceeds u32"))?;
232
233    Ok((block, offset_in_block))
234}