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
29/// Byte offsets of the split 32-bit counters inside one descriptor. The low
30/// halves sit in the 32-byte legacy descriptor; the high halves exist only
31/// when `desc_size >= 64`. Readers and writers share these so the two can
32/// never disagree about where a counter lives (#390).
33pub(crate) const OFF_FREE_BLOCKS_LO: usize = 0x0C;
34pub(crate) const OFF_FREE_INODES_LO: usize = 0x0E;
35pub(crate) const OFF_USED_DIRS_LO: usize = 0x10;
36pub(crate) const OFF_FREE_BLOCKS_HI: usize = 0x2C;
37pub(crate) const OFF_FREE_INODES_HI: usize = 0x2E;
38pub(crate) const OFF_USED_DIRS_HI: usize = 0x30;
39
40/// Read the little-endian u16 at `off`.
41fn u16_at(buf: &[u8], off: usize) -> u16 {
42    u16::from_le_bytes(buf[off..off + 2].try_into().unwrap())
43}
44
45bitflags::bitflags! {
46    /// BGD flags (`bg_flags`).
47    #[derive(Debug, Clone, Copy)]
48    pub struct BgdFlags: u16 {
49        /// Inode table not initialized — skip reading (treat as all-free).
50        const INODE_UNINIT = 0x0001;
51        /// Block bitmap not initialized — treat as all blocks free.
52        const BLOCK_UNINIT = 0x0002;
53        /// Inode table is fully zeroed on disk.
54        const ITABLE_ZEROED = 0x0004;
55    }
56}
57
58impl BlockGroupDescriptor {
59    /// Parse one descriptor from a buffer of at least `desc_size` bytes.
60    pub fn parse(buf: &[u8], desc_size: u16) -> Result<Self> {
61        if buf.len() < desc_size as usize {
62            return Err(Error::Corrupt("bgd buffer too small"));
63        }
64
65        let block_bitmap_lo = u32::from_le_bytes(buf[0x00..0x04].try_into().unwrap());
66        let inode_bitmap_lo = u32::from_le_bytes(buf[0x04..0x08].try_into().unwrap());
67        let inode_table_lo = u32::from_le_bytes(buf[0x08..0x0C].try_into().unwrap());
68        let free_blocks_lo = u16_at(buf, OFF_FREE_BLOCKS_LO);
69        let free_inodes_lo = u16_at(buf, OFF_FREE_INODES_LO);
70        let used_dirs_lo = u16_at(buf, OFF_USED_DIRS_LO);
71        let flags = u16::from_le_bytes(buf[0x12..0x14].try_into().unwrap());
72        let block_bitmap_csum_lo = u16::from_le_bytes(buf[0x18..0x1A].try_into().unwrap());
73        let inode_bitmap_csum_lo = u16::from_le_bytes(buf[0x1A..0x1C].try_into().unwrap());
74        let itable_unused_lo = u16::from_le_bytes(buf[0x1C..0x1E].try_into().unwrap());
75        let checksum = u16::from_le_bytes(buf[0x1E..0x20].try_into().unwrap());
76
77        let (
78            block_bitmap_hi,
79            inode_bitmap_hi,
80            inode_table_hi,
81            free_blocks_hi,
82            free_inodes_hi,
83            used_dirs_hi,
84            itable_unused_hi,
85            block_bitmap_csum_hi,
86            inode_bitmap_csum_hi,
87        ) = if desc_size >= 64 {
88            (
89                u32::from_le_bytes(buf[0x20..0x24].try_into().unwrap()),
90                u32::from_le_bytes(buf[0x24..0x28].try_into().unwrap()),
91                u32::from_le_bytes(buf[0x28..0x2C].try_into().unwrap()),
92                u16_at(buf, OFF_FREE_BLOCKS_HI),
93                u16_at(buf, OFF_FREE_INODES_HI),
94                u16_at(buf, OFF_USED_DIRS_HI),
95                u16::from_le_bytes(buf[0x32..0x34].try_into().unwrap()),
96                u16::from_le_bytes(buf[0x38..0x3A].try_into().unwrap()),
97                u16::from_le_bytes(buf[0x3A..0x3C].try_into().unwrap()),
98            )
99        } else {
100            (0, 0, 0, 0, 0, 0, 0, 0, 0)
101        };
102
103        Ok(Self {
104            block_bitmap: ((block_bitmap_hi as u64) << 32) | block_bitmap_lo as u64,
105            inode_bitmap: ((inode_bitmap_hi as u64) << 32) | inode_bitmap_lo as u64,
106            inode_table: ((inode_table_hi as u64) << 32) | inode_table_lo as u64,
107            free_blocks_count: ((free_blocks_hi as u32) << 16) | free_blocks_lo as u32,
108            free_inodes_count: ((free_inodes_hi as u32) << 16) | free_inodes_lo as u32,
109            used_dirs_count: ((used_dirs_hi as u32) << 16) | used_dirs_lo as u32,
110            flags,
111            itable_unused: ((itable_unused_hi as u32) << 16) | itable_unused_lo as u32,
112            block_bitmap_csum: ((block_bitmap_csum_hi as u32) << 16) | block_bitmap_csum_lo as u32,
113            inode_bitmap_csum: ((inode_bitmap_csum_hi as u32) << 16) | inode_bitmap_csum_lo as u32,
114            checksum,
115        })
116    }
117
118    pub fn flags(&self) -> BgdFlags {
119        BgdFlags::from_bits_truncate(self.flags)
120    }
121}
122
123/// Read all block group descriptors for the filesystem.
124///
125/// When `csum.enabled`, each descriptor's CRC32C is verified; a mismatch
126/// returns `Error::BadChecksum { what: "block group descriptor" }`.
127pub fn read_all<D: BlockDevice + ?Sized>(
128    dev: &D,
129    sb: &Superblock,
130    csum: &Checksummer,
131) -> Result<Vec<BlockGroupDescriptor>> {
132    let block_size = sb.block_size() as u64;
133
134    let group_count = sb.block_group_count();
135    // THE TABLE HAS TO BE INSIDE THE FILESYSTEM.
136    //
137    // `group_count` is `blocks_count / blocks_per_group`, both raw
138    // superblock fields, and `desc_size` is another. blocks_count 2^40
139    // with blocks_per_group 1 asked for 72 petabytes -- an allocation
140    // that aborts through `handle_alloc_error`, past `ffi_guard`'s
141    // `catch_unwind`, taking the host process with it. blocks_count
142    // u64::MAX wrapped the multiply instead and gave "capacity
143    // overflow". Both from a 4 MiB image, at mount. Every descriptor
144    // takes its own bytes of the device, wherever the table is laid out.
145    let total_bytes = group_count
146        .checked_mul(sb.desc_size as u64)
147        .ok_or(Error::Corrupt(
148            "superblock: the group descriptor table's size overflows",
149        ))?;
150    if total_bytes > dev.size_bytes() {
151        return Err(Error::Corrupt(
152            "superblock: the group descriptor table reaches past the end of the device",
153        ));
154    }
155
156    // Descriptor by descriptor, from wherever each lives: one table after
157    // the superblock, or under META_BG a block per meta group (#73). A
158    // block is read once for all the descriptors it holds.
159    let mut table_block: Option<(u64, Vec<u8>)> = None;
160    let group_count: usize = table_len(group_count)?;
161    let mut groups = Vec::with_capacity(group_count);
162    for i in 0..group_count {
163        let (block, off) = sb.descriptor_location(i as u64);
164        if table_block.as_ref().map(|(b, _)| *b) != Some(block) {
165            let end = block
166                .checked_add(1)
167                .and_then(|b| b.checked_mul(block_size))
168                .ok_or(Error::Corrupt(
169                    "superblock: the group descriptor table's extent overflows",
170                ))?;
171            if end > dev.size_bytes() {
172                return Err(Error::Corrupt(
173                    "superblock: the group descriptor table reaches past the end of the device",
174                ));
175            }
176            let mut bytes = vec![0u8; block_size as usize];
177            dev.read_at(block * block_size, &mut bytes)?;
178            table_block = Some((block, bytes));
179        }
180        let bytes = &table_block.as_ref().unwrap().1;
181        let raw = &bytes[off..off + sb.desc_size as usize];
182        let stored = u16::from_le_bytes([raw[0x1E], raw[0x1F]]);
183        if crate::checksum::group_desc_csum(sb, csum, i as u32, raw)
184            .is_some_and(|want| want != stored)
185        {
186            return Err(Error::BadChecksum {
187                what: "block group descriptor",
188            });
189        }
190        let bgd = BlockGroupDescriptor::parse(raw, sb.desc_size)?;
191        // THE THREE POINTERS ARE BLOCK NUMBERS IN THIS FILESYSTEM.
192        //
193        // `bg_block_bitmap`, `bg_inode_bitmap` and `bg_inode_table` are
194        // assembled lo|hi into a `u64` and were never checked against
195        // anything. Every one of them is a WRITE target:
196        // `set_block_run_used` and `free_block_run` write a whole block
197        // at the bitmaps, and `locate_inode` hands the table block to
198        // `write_inode_raw`, which writes an inode image there. A
199        // descriptor pointing outside the filesystem is trivially
200        // satisfied on an FSKit mount, where the device is larger than
201        // `s_blocks_count`, and `bg_block_bitmap_hi` set high also
202        // wrapped the unchecked `bitmap_block * block_size` at the use
203        // site. The only comparable check lived in fsck-only code.
204        //
205        // The inode table spans `inodes_per_group * inode_size` bytes,
206        // so it is the one that has to fit as a range rather than a
207        // point.
208        let table_blocks = (u64::from(sb.inodes_per_group) * u64::from(sb.inode_size))
209            .div_ceil(u64::from(sb.block_size()));
210        let past_the_end = bgd.block_bitmap >= sb.blocks_count
211            || bgd.inode_bitmap >= sb.blocks_count
212            || bgd
213                .inode_table
214                .checked_add(table_blocks)
215                .is_none_or(|end| end > sb.blocks_count);
216        if past_the_end {
217            return Err(Error::Corrupt(
218                "a block group descriptor points outside the filesystem",
219            ));
220        }
221        check_pointers_within(sb, i as u64, &bgd, table_blocks)?;
222        groups.push(bgd);
223    }
224    Ok(groups)
225}
226
227/// AND BOUNDED BELOW, AND BY THEIR OWN GROUP (#320).
228///
229/// The bound in [`read_all`] was from above only, so a pointer of 0 -- or 1
230/// on a 1 KiB volume -- was accepted, and a bitmap update or inode write then
231/// put a whole block over the primary superblock or the descriptor table.
232/// This is the rule the kernel enforces at mount, which refuses a volume
233/// whose descriptors break it:
234///
235/// - no pointer inside group 0's head: block 0 through the superblock, its
236///   descriptor table and the table's reserved growth
237///   ([`Superblock::group_head_metadata_blocks`]);
238/// - without `FLEX_BG`, every pointer, and the whole inode table, inside the
239///   descriptor's own group. With it, `mkfs.ext4` packs a flex group's
240///   bitmaps and tables together, so group N's may sit in group 0 and only
241///   the filesystem bounds them.
242///
243/// The caller has already bounded all three by the end of the filesystem.
244fn check_pointers_within(
245    sb: &Superblock,
246    group: u64,
247    bgd: &BlockGroupDescriptor,
248    table_blocks: u64,
249) -> Result<()> {
250    // The superblock's own block: `first_data_block` on every volume but a
251    // 1 KiB bigalloc one, whose groups start at block 0 while the superblock
252    // is still in block 1.
253    let superblock_block = crate::superblock::SUPERBLOCK_OFFSET / u64::from(sb.block_size());
254    let group0_metadata_end = superblock_block + sb.group_head_metadata_blocks(0);
255    let table_last = bgd.inode_table + table_blocks.max(1) - 1;
256    if bgd.block_bitmap < group0_metadata_end
257        || bgd.inode_bitmap < group0_metadata_end
258        || bgd.inode_table < group0_metadata_end
259    {
260        return Err(Error::Corrupt(
261            "a block group descriptor points into group 0's superblock or descriptor table",
262        ));
263    }
264    if sb.feature_incompat & crate::features::Incompat::FLEX_BG.bits() != 0 {
265        return Ok(());
266    }
267    let first = u64::from(sb.first_data_block) + group * u64::from(sb.blocks_per_group);
268    let last = first
269        .saturating_add(u64::from(sb.blocks_per_group))
270        .min(sb.blocks_count)
271        - 1;
272    let within = |b: u64| (first..=last).contains(&b);
273    if !(within(bgd.block_bitmap) && within(bgd.inode_bitmap) && within(bgd.inode_table))
274        || table_last > last
275    {
276        return Err(Error::Corrupt(
277            "a block group descriptor points outside its own group, without flex_bg",
278        ));
279    }
280    Ok(())
281}
282
283/// The group count as a table length on this target.
284///
285/// `group_count as usize` truncated on a 32-bit target: 2^32 groups became
286/// an empty table, and every later index into it panicked (#327). Generic
287/// so the conversion can be tested at 32 bits on any host.
288fn table_len<T: TryFrom<u64>>(group_count: u64) -> Result<T> {
289    T::try_from(group_count).map_err(|_| {
290        Error::Corrupt("superblock: the group count does not fit in this target's address space")
291    })
292}
293
294/// Locate the inode table block + offset for a given inode number.
295/// Returns (physical block containing the inode, byte offset within block).
296pub fn locate_inode(
297    sb: &Superblock,
298    groups: &[BlockGroupDescriptor],
299    ino: u32,
300) -> Result<(u64, u32)> {
301    if ino == 0 || ino > sb.inodes_count {
302        return Err(Error::InvalidInode(ino));
303    }
304    if sb.inodes_per_group == 0 || sb.inode_size == 0 {
305        return Err(Error::Corrupt(
306            "superblock: zero inodes_per_group or inode_size",
307        ));
308    }
309    let group_idx = ((ino - 1) / sb.inodes_per_group) as usize;
310    let local_idx = ((ino - 1) % sb.inodes_per_group) as u64;
311
312    let bgd = groups.get(group_idx).ok_or(Error::InvalidInode(ino))?;
313    let block_size = sb.block_size() as u64;
314    let inode_size = sb.inode_size as u64;
315    if inode_size > block_size {
316        return Err(Error::Corrupt(
317            "superblock: inode_size larger than block_size",
318        ));
319    }
320    let inodes_per_block = block_size / inode_size;
321    if inodes_per_block == 0 {
322        return Err(Error::Corrupt("superblock: inode_size exceeds block_size"));
323    }
324
325    let block = bgd
326        .inode_table
327        .checked_add(local_idx / inodes_per_block)
328        .ok_or(Error::Corrupt("inode table block number overflow"))?;
329    let off_bytes = (local_idx % inodes_per_block) * inode_size;
330    let offset_in_block: u32 = off_bytes
331        .try_into()
332        .map_err(|_| Error::Corrupt("inode offset exceeds u32"))?;
333
334    Ok((block, offset_in_block))
335}
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340
341    /// The group count is a `u64`; `as usize` truncated it on a 32-bit
342    /// target, so a device of 2^32 groups built an empty table (#327).
343    /// Generic over the target width so a 64-bit host checks the 32-bit
344    /// case: `u32` stands in for a 32-bit `usize`.
345    #[test]
346    fn a_group_count_past_the_target_width_is_refused() {
347        assert!(table_len::<u32>(u64::from(u32::MAX) + 1).is_err());
348        assert!(table_len::<u32>(u64::MAX).is_err());
349        assert_eq!(table_len::<u32>(u64::from(u32::MAX)).unwrap(), u32::MAX);
350        assert_eq!(table_len::<usize>(3).unwrap(), 3);
351    }
352}