Skip to main content

fs_ext4/
checksum.rs

1//! Metadata checksum verification (RO_COMPAT_METADATA_CSUM).
2//!
3//! Spec: kernel.org/doc/html/latest/filesystems/ext4/checksums.html
4//!
5//! When `RO_COMPAT_METADATA_CSUM` is set, ext4 stores a CRC32C of every
6//! metadata structure to detect on-disk corruption. The checksum is salted
7//! by the **filesystem UUID** (or by `s_checksum_seed` when
8//! `INCOMPAT_CSUM_SEED` is also set) so the same byte pattern in two
9//! different filesystems hashes differently.
10//!
11//! The spec uses a chained CRC pattern:
12//!
13//! 1. Start with the seed (UUID or s_checksum_seed).
14//! 2. For per-group/per-inode structures, fold in the group number / inode
15//!    number / generation as a "context" prefix.
16//! 3. Then CRC the actual structure with the checksum field zeroed.
17//!
18//! This module exposes two helpers:
19//!   - [`Checksummer::seed`] — derived once at mount time
20//!   - [`Checksummer::superblock`], [`Checksummer::inode`], etc.
21//!
22//! Verification is enforced, not informational. A superblock or group
23//! descriptor whose checksum does not match aborts the mount with
24//! `Error::BadChecksum`, as does an inode read through
25//! `Filesystem::read_inode_verified`, and extent and directory blocks are
26//! checked as they are read. Every write recomputes the checksums of what it
27//! rewrites -- inodes in `finalize_inode_raw`, group descriptors and the
28//! superblock in `buffer_patch_bgd_counters` / `buffer_patch_sb_counters`,
29//! and the tails of directory and extent blocks -- so a new write path that
30//! rewrites a metadata block must recompute its checksum too (#90).
31
32use crate::features::{Incompat, RoCompat};
33use crate::superblock::Superblock;
34
35/// CRC32C as ext4 and JBD2 chain it: the seed goes in as the register
36/// value and nothing is inverted at either end. The `crc32c` crate's
37/// `crc32c_append(s, d)` is `~iterate(~s, d)`; the chaining form is
38/// `iterate(c, d)`. Wrap to get that form out of the crate.
39///
40/// Public so write-path callers that rewrite a metadata block (dir, BGD, SB)
41/// can recompute the tail checksum inline without rebuilding a `Checksummer`.
42#[inline]
43pub fn linux_crc32c(seed: u32, data: &[u8]) -> u32 {
44    !crc32c::crc32c_append(!seed, data)
45}
46
47/// CRC-16 with polynomial 0x8005, reflected, no final XOR (the parameters
48/// catalogued as CRC-16/ARC). Only `GDT_CSUM` group descriptors use it; the
49/// format documentation's checksum table names it as their `crc16`.
50pub fn crc16(mut crc: u16, data: &[u8]) -> u16 {
51    for &b in data {
52        crc ^= u16::from(b);
53        for _ in 0..8 {
54            crc = if crc & 1 != 0 {
55                (crc >> 1) ^ 0xA001
56            } else {
57                crc >> 1
58            };
59        }
60    }
61    crc
62}
63
64/// The checksum group descriptor `group` must carry at 0x1E, or `None`
65/// when the volume asks for none.
66///
67/// The ingredients are the "Group Descriptors" row of the checksum table
68/// in the kernel.org ext4 format documentation. With `METADATA_CSUM` it is
69/// the low 16 bits of `crc32c(seed, group_le32 || desc)`. Otherwise, with
70/// `GDT_CSUM`, it is `crc16(~0, uuid || group_le32 || desc[..0x1E])`, plus
71/// the bytes past the checksum field only when `INCOMPAT_64BIT` is set.
72/// Either way the checksum field itself counts as zero. `desc` must hold
73/// at least `sb.desc_size` bytes; `None` is also returned when it doesn't.
74pub fn group_desc_csum(
75    sb: &Superblock,
76    csum: &Checksummer,
77    group: u32,
78    desc: &[u8],
79) -> Option<u16> {
80    let n = sb.desc_size as usize;
81    if desc.len() < n || n < 0x20 {
82        return None;
83    }
84    let le_group = group.to_le_bytes();
85    if sb.feature_ro_compat & RoCompat::METADATA_CSUM.bits() != 0 {
86        let mut c = linux_crc32c(csum.seed, &le_group);
87        c = linux_crc32c(c, &desc[..0x1E]);
88        c = linux_crc32c(c, &[0, 0]);
89        c = linux_crc32c(c, &desc[0x20..n]);
90        return Some(c as u16);
91    }
92    if sb.feature_ro_compat & RoCompat::GDT_CSUM.bits() == 0 {
93        return None;
94    }
95    let mut c = crc16(!0, &sb.uuid);
96    c = crc16(c, &le_group);
97    c = crc16(c, &desc[..0x1E]);
98    if sb.feature_incompat & Incompat::BIT64.bits() != 0 {
99        c = crc16(c, &desc[0x20..n]);
100    }
101    Some(c)
102}
103
104/// Per-mount checksum context: the seed and "is it enabled" flag.
105#[derive(Debug, Clone, Copy)]
106pub struct Checksummer {
107    pub seed: u32,
108    pub enabled: bool,
109}
110
111/// `file_type` of the fake dirent that ends a checksummed directory
112/// block: the `det_reserved_ft` value the format documentation gives for
113/// `struct ext4_dir_entry_tail`.
114///
115/// Not a real file type: it is out of range for one, which is how a
116/// reader tells the tail apart from an entry.
117pub const DIR_ENTRY_TAIL_FILE_TYPE: u8 = 0xDE;
118
119impl Checksummer {
120    /// Derive the checksum context from a parsed superblock.
121    ///
122    /// Per spec: if `INCOMPAT_CSUM_SEED` is set, use the explicit
123    /// `s_checksum_seed` field. Otherwise, the seed is crc32c(~0, UUID)
124    /// (format documentation, `s_checksum_seed`) — i.e. our
125    /// `linux_crc32c(!0, UUID)`.
126    pub fn from_superblock(sb: &Superblock) -> Self {
127        let enabled = (sb.feature_ro_compat & RoCompat::METADATA_CSUM.bits()) != 0;
128        let seed = if (sb.feature_incompat & Incompat::CSUM_SEED.bits()) != 0 {
129            sb.checksum_seed
130        } else {
131            linux_crc32c(!0, &sb.uuid)
132        };
133        Self { seed, enabled }
134    }
135
136    /// Linux-semantics CRC32C of a buffer using the mount-wide seed.
137    pub fn crc(&self, data: &[u8]) -> u32 {
138        linux_crc32c(self.seed, data)
139    }
140
141    /// Linux-semantics CRC32C with a 32-bit context prefix folded in first.
142    pub fn crc_with_prefix(&self, prefix: u32, data: &[u8]) -> u32 {
143        let mid = linux_crc32c(self.seed, &prefix.to_le_bytes());
144        linux_crc32c(mid, data)
145    }
146
147    /// Verify the superblock checksum. Stored at byte offset 0x3FC; CRC
148    /// covers the first 0x3FC bytes. Initial seed is `~0` (NOT the per-FS
149    /// seed — superblock checksum is special since the seed lives inside it).
150    pub fn verify_superblock(&self, sb_raw: &[u8]) -> bool {
151        if !self.enabled {
152            return true;
153        }
154        if sb_raw.len() < 1024 {
155            return false;
156        }
157        let stored = u32::from_le_bytes(sb_raw[0x3FC..0x400].try_into().unwrap());
158        let computed = linux_crc32c(!0, &sb_raw[..0x3FC]);
159        stored == computed
160    }
161
162    /// Verify a block group descriptor's checksum.
163    ///
164    /// Per spec (`ext4/group_descr.html`), when `RO_COMPAT_METADATA_CSUM` is
165    /// set the GDT checksum is computed as:
166    ///
167    /// ```text
168    ///   crc32c(seed, group_no_le_u32 || bgd_with_csum_zeroed) & 0xFFFF
169    /// ```
170    ///
171    /// `desc_size` is the on-disk descriptor size (32 or 64).
172    /// `bgd_raw` must be at least `desc_size` bytes; the stored checksum at
173    /// offset 0x1E is treated as zero for the computation.
174    pub fn verify_bgd(&self, group_no: u32, bgd_raw: &[u8], desc_size: u16) -> bool {
175        if !self.enabled {
176            return true;
177        }
178        let n = desc_size as usize;
179        if bgd_raw.len() < n || n < 0x20 {
180            return false;
181        }
182        let stored = u16::from_le_bytes(bgd_raw[0x1E..0x20].try_into().unwrap());
183
184        let mut tmp = bgd_raw[..n].to_vec();
185        tmp[0x1E] = 0;
186        tmp[0x1F] = 0;
187
188        let computed16 = self.crc_with_prefix(group_no, &tmp) as u16;
189        computed16 == stored
190    }
191
192    /// Verify a directory block's trailing `ext4_dir_entry_tail` checksum.
193    ///
194    /// Linear directory blocks with `metadata_csum` enabled end in a 12-byte
195    /// `struct ext4_dir_entry_tail { u32 det_reserved_zero1; u16 det_rec_len;
196    /// u8 det_reserved_zero2; u8 det_reserved_ft; u32 det_checksum; }`.
197    ///
198    /// The format documentation's checksum table ("Directory Entries"): the
199    /// CRC covers **`block[0..block_size - 12]`** — i.e. everything BEFORE
200    /// the fake entry that holds it.
201    /// The tail's own bytes (including `det_checksum`) are excluded:
202    ///
203    /// ```text
204    ///   crc32c(seed, ino_le) → crc32c(., gen_le) → crc32c(., block[..len-12])
205    /// ```
206    ///
207    /// `block` is the whole directory block including the trailing tail.
208    pub fn verify_dir_entry_tail(&self, ino: u32, generation: u32, block: &[u8]) -> bool {
209        if !self.enabled {
210            return true;
211        }
212        if block.len() < 12 {
213            return false;
214        }
215        let end = block.len();
216        let stored = u32::from_le_bytes(block[end - 4..end].try_into().unwrap());
217
218        let mut c = linux_crc32c(self.seed, &ino.to_le_bytes());
219        c = linux_crc32c(c, &generation.to_le_bytes());
220        c = linux_crc32c(c, &block[..end - 12]);
221        c == stored
222    }
223
224    /// Plant the `ext4_dir_entry_tail` and checksum a directory block.
225    ///
226    /// The mirror of [`Self::verify_dir_entry_tail`], and the sibling
227    /// that was missing: `patch_extent_tail` and `patch_xattr_block`
228    /// both existed, so the one recipe written most often was the one
229    /// with no helper. It was hand-rolled at **sixteen** sites across
230    /// `fs.rs`, `fsck.rs`, `mkfs.rs` and the tests, in two different
231    /// addressing idioms (`bs - 12` with `+4/+6/+7`, and
232    /// `block.len()` with `-8/-6/-5`) that a reader has to prove
233    /// equivalent at each one.
234    ///
235    /// The tail is a **fake directory entry** occupying the last 12
236    /// bytes: `inode = 0` so no scan mistakes it for a real one,
237    /// `rec_len = 12` so a walk steps over it, `name_len = 0`, and
238    /// `file_type = 0xDE` as the marker `has_csum_tail` looks for. The
239    /// CRC then covers `block[..len - 12]` — the whole block *except*
240    /// the tail entry, which is what separates this from
241    /// [`Self::patch_extent_tail`], where only the trailing 4 bytes are
242    /// excluded.
243    ///
244    /// Planting is idempotent: a block that already carries the tail
245    /// gets the same twelve bytes back, so callers that only need the
246    /// checksum recomputed can use this too rather than keeping a
247    /// second recipe for that case.
248    ///
249    /// Every constant here has to agree with e2fsprogs, and a
250    /// disagreement produces no error until the volume is mounted
251    /// somewhere else — which is the argument for there being one copy
252    /// of them.
253    ///
254    /// No-op when checksums are disabled or the block is under 12
255    /// bytes; returns true when it patched.
256    pub fn patch_dir_entry_tail(&self, ino: u32, generation: u32, block: &mut [u8]) -> bool {
257        if !self.enabled || block.len() < 12 {
258            return false;
259        }
260        let end = block.len();
261        block[end - 12..end - 8].copy_from_slice(&0u32.to_le_bytes()); // inode = 0
262        block[end - 8..end - 6].copy_from_slice(&12u16.to_le_bytes()); // rec_len
263        block[end - 6] = 0; // name_len
264        block[end - 5] = DIR_ENTRY_TAIL_FILE_TYPE;
265
266        let mut c = linux_crc32c(self.seed, &ino.to_le_bytes());
267        c = linux_crc32c(c, &generation.to_le_bytes());
268        c = linux_crc32c(c, &block[..end - 12]);
269        block[end - 4..end].copy_from_slice(&c.to_le_bytes());
270        true
271    }
272
273    /// Whether an htree block's `dx_tail` checksum is the one its contents
274    /// give: `Some(true)` or `Some(false)`, or `None` when checksums are off
275    /// or the count/limit pair leaves no room for a tail to check.
276    ///
277    /// The same computation as [`Checksummer::patch_dx_tail`], compared
278    /// rather than written.
279    pub fn verify_dx_tail(
280        &self,
281        ino: u32,
282        generation: u32,
283        block: &[u8],
284        count_offset: usize,
285    ) -> Option<bool> {
286        let mut copy = block.to_vec();
287        if !self.patch_dx_tail(ino, generation, &mut copy, count_offset) {
288            return None;
289        }
290        Some(copy == block)
291    }
292
293    /// Recompute the checksum in an htree block's `dx_tail`.
294    ///
295    /// The "HTREE Nodes" row of the format documentation's checksum table:
296    /// crc32c over the inode number and
297    /// generation, the block up to the last used `dx_entry`, and the tail's
298    /// reserved word, with the checksum field itself counted as zero. The
299    /// tail sits right after `limit` entries. `count_offset` is where the
300    /// count/limit pair starts: 32 in a `dx_root`, 8 in an interior node.
301    /// Returns `false` (and writes nothing) when checksums are off or the
302    /// count/limit pair does not leave room for a tail in this block.
303    pub fn patch_dx_tail(
304        &self,
305        ino: u32,
306        generation: u32,
307        block: &mut [u8],
308        count_offset: usize,
309    ) -> bool {
310        if !self.enabled || block.len() < count_offset + 4 {
311            return false;
312        }
313        let limit = u16::from_le_bytes([block[count_offset], block[count_offset + 1]]) as usize;
314        let count = u16::from_le_bytes([block[count_offset + 2], block[count_offset + 3]]) as usize;
315        let tail = count_offset + limit * 8;
316        if count > limit || tail + 8 > block.len() {
317            return false;
318        }
319        let mut c = linux_crc32c(self.seed, &ino.to_le_bytes());
320        c = linux_crc32c(c, &generation.to_le_bytes());
321        c = linux_crc32c(c, &block[..count_offset + count * 8]);
322        c = linux_crc32c(c, &block[tail..tail + 4]);
323        c = linux_crc32c(c, &[0; 4]);
324        block[tail + 4..tail + 8].copy_from_slice(&c.to_le_bytes());
325        true
326    }
327
328    /// Verify an extent-block tail checksum.
329    ///
330    /// Extent index/leaf blocks (those read off-inode when the tree has
331    /// internal nodes) end in a 4-byte `struct ext4_extent_tail
332    /// { u32 et_checksum; }`. Per the "Extents" row of the format
333    /// documentation's checksum table the CRC covers **`block[0..len-4]`**
334    /// — only the trailing `et_checksum` field is excluded:
335    ///
336    /// ```text
337    ///   crc32c(seed, ino_le) → crc32c(., gen_le) → crc32c(., block[..len-4])
338    /// ```
339    ///
340    /// Different from `verify_dir_entry_tail`, which excludes the full
341    /// 12-byte tail entry.
342    pub fn verify_extent_tail(&self, ino: u32, generation: u32, block: &[u8]) -> bool {
343        if !self.enabled {
344            return true;
345        }
346        if block.len() < 4 {
347            return false;
348        }
349        let end = block.len();
350        let stored = u32::from_le_bytes(block[end - 4..end].try_into().unwrap());
351
352        let mut c = linux_crc32c(self.seed, &ino.to_le_bytes());
353        c = linux_crc32c(c, &generation.to_le_bytes());
354        c = linux_crc32c(c, &block[..end - 4]);
355        c == stored
356    }
357
358    /// Verify a parsed inode's checksum.
359    /// Chained: seed → ino_le → gen_le → inode_bytes (with checksum slots zeroed).
360    pub fn verify_inode(&self, ino: u32, generation: u32, inode_raw: &[u8]) -> bool {
361        if !self.enabled {
362            return true;
363        }
364        // A truncated read is a refusal, not a pass — the same answer
365        // `verify_superblock`, `verify_dir_entry_tail` and
366        // `verify_extent_tail` give, and for the same reason: the
367        // caller got fewer bytes than it asked for, so a checksum
368        // computed here would cover bytes that are not the ones on
369        // disk. Reporting `true` says an inode verified when nothing
370        // verified it.
371        //
372        // This used to read `if !self.enabled || inode_raw.len() < 128`,
373        // one `||` instead of two `if`s — a difference invisible unless
374        // you read all four verifiers together.
375        if inode_raw.len() < crate::inode::GOOD_OLD_INODE_SIZE {
376            return false;
377        }
378        let stored_lo = u16::from_le_bytes(inode_raw[0x7C..0x7E].try_into().unwrap()) as u32;
379        // Only where `i_checksum_hi` exists. Below `i_extra_isize = 4` the
380        // bytes at 0x82 are ordinary inode bytes the checksum covers, and
381        // the kernel and libext2fs compare the low 16 bits alone (#380).
382        let stored_hi = if checksum_hi_fits(inode_raw) {
383            u16::from_le_bytes(inode_raw[0x82..0x84].try_into().unwrap()) as u32
384        } else {
385            0
386        };
387        let stored = (stored_hi << 16) | stored_lo;
388        match self.compute_inode_checksum(ino, generation, inode_raw) {
389            Some((lo, hi)) => ((hi as u32) << 16 | lo as u32) == stored,
390            None => true, // disabled / too short — accept
391        }
392    }
393
394    /// Write the `ext4_extent_tail.et_checksum` u32 at the end of a freshly-
395    /// built extent index/leaf block. Mirrors `verify_extent_tail`: the CRC
396    /// covers `block[..len-4]`, chained seed → ino → generation → body.
397    /// No-op when checksums are disabled; returns true when it patched.
398    pub fn patch_extent_tail(&self, ino: u32, generation: u32, block: &mut [u8]) -> bool {
399        if !self.enabled || block.len() < 4 {
400            return false;
401        }
402        let end = block.len();
403        let mut c = linux_crc32c(self.seed, &ino.to_le_bytes());
404        c = linux_crc32c(c, &generation.to_le_bytes());
405        c = linux_crc32c(c, &block[..end - 4]);
406        block[end - 4..end].copy_from_slice(&c.to_le_bytes());
407        true
408    }
409
410    /// Verify an external xattr block's checksum.
411    ///
412    /// The format documentation's checksum table gives the block with its
413    /// checksum field zeroed; the block number goes in ahead of it as well,
414    /// and blocks the kernel and e2fsprogs write verify this way. The
415    /// recipe is:
416    ///
417    /// ```text
418    ///   crc32c(seed, block_nr_le_u64)
419    ///   → crc32c(., block[0x00..0x10])      // magic, refcount, blocks, hash
420    ///   → crc32c(., [0u32])                  // h_checksum slot zeroed (4 bytes)
421    ///   → crc32c(., block[0x14..end])        // rest of block
422    /// ```
423    ///
424    /// The stored u32 lives at offset 0x10 of the block.
425    pub fn verify_xattr_block(&self, block_nr: u64, block: &[u8]) -> bool {
426        if !self.enabled {
427            return true;
428        }
429        if block.len() < 0x20 {
430            return false;
431        }
432        let stored = u32::from_le_bytes(block[0x10..0x14].try_into().unwrap());
433        let computed = self.compute_xattr_block_csum(block_nr, block);
434        stored == computed
435    }
436
437    /// Patch the `h_checksum` field of an external xattr block in place.
438    /// Mirrors [`verify_xattr_block`]. No-op when checksums are disabled.
439    /// Returns `true` when the block was patched.
440    pub fn patch_xattr_block(&self, block_nr: u64, block: &mut [u8]) -> bool {
441        if !self.enabled || block.len() < 0x20 {
442            return false;
443        }
444        let csum = self.compute_xattr_block_csum(block_nr, block);
445        block[0x10..0x14].copy_from_slice(&csum.to_le_bytes());
446        true
447    }
448
449    fn compute_xattr_block_csum(&self, block_nr: u64, block: &[u8]) -> u32 {
450        let mut c = linux_crc32c(self.seed, &block_nr.to_le_bytes());
451        c = linux_crc32c(c, &block[0x00..0x10]);
452        c = linux_crc32c(c, &0u32.to_le_bytes());
453        c = linux_crc32c(c, &block[0x14..]);
454        c
455    }
456
457    /// Compute the inode checksum as two u16 halves (lo=checksum_lo at 0x7C,
458    /// hi=checksum_hi at 0x82). Returns `None` when checksums are disabled
459    /// or the buffer is too short to patch. Callers use this after mutating
460    /// an inode image to restore the checksum before writing back.
461    pub fn compute_inode_checksum(
462        &self,
463        ino: u32,
464        generation: u32,
465        inode_raw: &[u8],
466    ) -> Option<(u16, u16)> {
467        if !self.enabled || inode_raw.len() < crate::inode::GOOD_OLD_INODE_SIZE {
468            return None;
469        }
470        // i_checksum_hi (0x82) is part of the checksum only when i_extra_isize
471        // (0x80) is large enough to cover it: a field past the 128-byte base
472        // exists only if i_extra_isize reaches its end, which here means
473        // i_extra_isize >= 4 (the format documentation's inode table puts
474        // i_checksum_hi at 0x82). On a zeroed freed inode
475        // (i_extra_isize = 0) the kernel uses ONLY the 16-bit lo checksum and
476        // treats 0x82 as ordinary (zero) data; zeroing hi and storing a full
477        // 32-bit value there mismatches ("checksum does not match inode").
478        let fits_hi = checksum_hi_fits(inode_raw);
479        let mut tmp = inode_raw.to_vec();
480        tmp[0x7C] = 0;
481        tmp[0x7D] = 0;
482        if fits_hi {
483            tmp[0x82] = 0;
484            tmp[0x83] = 0;
485        }
486        let mut c = linux_crc32c(self.seed, &ino.to_le_bytes());
487        c = linux_crc32c(c, &generation.to_le_bytes());
488        c = linux_crc32c(c, &tmp);
489        let lo = (c & 0xFFFF) as u16;
490        let hi = if fits_hi {
491            ((c >> 16) & 0xFFFF) as u16
492        } else {
493            0
494        };
495        Some((lo, hi))
496    }
497
498    /// Checksum `inode_raw` and store it: `i_checksum_lo` always, and
499    /// `i_checksum_hi` only where the inode has one.
500    ///
501    /// EVERY WRITER USED TO STORE BOTH HALVES. With `i_extra_isize < 4`
502    /// [`compute_inode_checksum`](Self::compute_inode_checksum) covers
503    /// 0x82..0x84 as ordinary bytes and returns a `hi` of 0, and storing
504    /// that 0 at 0x82 afterwards changed a byte the checksum had just
505    /// covered -- and on an inode whose in-inode xattr area was placed at
506    /// 0x80, erased the area's magic (#380). No-op when checksums are off.
507    pub(crate) fn patch_inode_checksum(&self, ino: u32, generation: u32, inode_raw: &mut [u8]) {
508        if let Some((lo, hi)) = self.compute_inode_checksum(ino, generation, inode_raw) {
509            inode_raw[0x7C..0x7E].copy_from_slice(&lo.to_le_bytes());
510            if checksum_hi_fits(inode_raw) {
511                inode_raw[0x82..0x84].copy_from_slice(&hi.to_le_bytes());
512            }
513        }
514    }
515}
516
517/// Whether the inode has an `i_checksum_hi`: `i_extra_isize` (0x80) covers
518/// 0x82..0x84. A field in the extended part of the inode exists only when
519/// `i_extra_isize` reaches its end (format documentation, "Inode Size").
520fn checksum_hi_fits(inode_raw: &[u8]) -> bool {
521    inode_raw.len() >= 0x84 && u16::from_le_bytes(inode_raw[0x80..0x82].try_into().unwrap()) >= 4
522}
523
524#[cfg(test)]
525mod tests {
526
527    // --- a short buffer is a refusal, on every verifier ------------------
528    //
529    // Written before the fix. `verify_inode` passed a truncated buffer;
530    // its three siblings refused one. Nothing in the crate noticed the
531    // difference, because the short-buffer axis had no coverage at all.
532
533    /// A checksummer that is switched on, with an arbitrary seed.
534    fn enabled() -> Checksummer {
535        Checksummer {
536            enabled: true,
537            seed: 0xDEAD_BEEF,
538        }
539    }
540
541    /// Every verifier refuses a buffer too short to hold the field it
542    /// checks.
543    ///
544    /// This is a policy, not four separate decisions, and it is the
545    /// safe direction: a truncated read means the caller got less than
546    /// it asked for, and the checksum it would compute covers bytes
547    /// that are not the ones on disk. Returning `true` there reports a
548    /// structure as verified when nothing verified it.
549    ///
550    /// Asserted as a set so a fifth verifier cannot quietly pick the
551    /// other answer — which is exactly how `verify_inode` came to
552    /// differ from the other three.
553    #[test]
554    fn every_verifier_refuses_a_buffer_too_short_to_check() {
555        let c = enabled();
556        assert!(
557            !c.verify_superblock(&[0u8; 64]),
558            "superblock: a 64-byte buffer cannot hold a checksum at 0x3FC"
559        );
560        assert!(
561            !c.verify_dir_entry_tail(2, 0, &[0u8; 8]),
562            "dir entry tail: an 8-byte buffer cannot hold a 12-byte tail"
563        );
564        assert!(
565            !c.verify_extent_tail(2, 0, &[0u8; 2]),
566            "extent tail: a 2-byte buffer cannot hold a 4-byte checksum"
567        );
568        assert!(
569            !c.verify_inode(2, 0, &[0u8; 64]),
570            "inode: a 64-byte buffer cannot hold a checksum at 0x7C"
571        );
572    }
573
574    /// And every one of them still passes everything when checksums are
575    /// off, short buffer included.
576    ///
577    /// The two conditions are separate for a reason: "we do not check"
578    /// and "we checked and it failed" are different answers, and a
579    /// filesystem without `metadata_csum` must not start failing reads
580    /// because a buffer was short.
581    #[test]
582    fn a_disabled_checksummer_passes_even_a_short_buffer() {
583        let c = Checksummer {
584            enabled: false,
585            seed: 0,
586        };
587        assert!(c.verify_superblock(&[0u8; 4]));
588        assert!(c.verify_dir_entry_tail(2, 0, &[0u8; 4]));
589        assert!(c.verify_extent_tail(2, 0, &[0u8; 1]));
590        assert!(c.verify_inode(2, 0, &[0u8; 4]));
591    }
592
593    /// The boundary itself: 127 bytes is refused, 128 is checked.
594    ///
595    /// 128 is not where `verify_inode`'s own field ends — `i_checksum_lo`
596    /// sits at 0x7C..0x7E, so 126 bytes would hold it. It is the whole
597    /// fixed ext2 inode, every byte of which the checksum covers, so a
598    /// shorter buffer cannot be checked even when it holds the field.
599    #[test]
600    fn the_inode_length_boundary_is_the_whole_fixed_inode() {
601        let c = enabled();
602        assert!(!c.verify_inode(2, 0, &[0u8; 127]), "127 is too short");
603        // 128 bytes of zeros is a real check that simply fails: the
604        // stored checksum is zero and the computed one is not.
605        assert!(
606            !c.verify_inode(2, 0, &[0u8; 128]),
607            "128 is checked, and all-zero bytes do not verify"
608        );
609
610        // A 128-byte inode with a correct checksum verifies.
611        let mut raw: Vec<u8> = (0..128u32).map(|i| (i * 7 + 3) as u8).collect();
612        let (lo, _) = c.compute_inode_checksum(2, 5, &raw).unwrap();
613        raw[0x7C..0x7E].copy_from_slice(&lo.to_le_bytes());
614        assert!(c.verify_inode(2, 5, &raw), "a correct checksum verifies");
615
616        // The bytes past the field are covered: 0x7E..0x80 is after
617        // `i_checksum_lo` and still inside the fixed inode.
618        let mut past_the_field = raw.clone();
619        past_the_field[0x7F] ^= 0x01;
620        assert!(
621            !c.verify_inode(2, 5, &past_the_field),
622            "a byte after i_checksum_lo is part of the checksum"
623        );
624
625        // 126 bytes hold the field, and carry a checksum that is correct
626        // over those 126 bytes — and are still refused, because they are
627        // not the whole fixed inode the checksum is defined over.
628        let mut holds_the_field = raw[..0x7E].to_vec();
629        holds_the_field[0x7C..0x7E].fill(0);
630        let mut crc = linux_crc32c(c.seed, &2u32.to_le_bytes());
631        crc = linux_crc32c(crc, &5u32.to_le_bytes());
632        crc = linux_crc32c(crc, &holds_the_field);
633        holds_the_field[0x7C..0x7E].copy_from_slice(&(crc as u16).to_le_bytes());
634        assert!(
635            !c.verify_inode(2, 5, &holds_the_field),
636            "126 bytes hold i_checksum_lo and are still too short"
637        );
638    }
639
640    use super::*;
641
642    #[test]
643    fn disabled_when_feature_off() {
644        // crc32c of an empty seed always yields 0; not interesting.
645        let c = Checksummer {
646            seed: 0,
647            enabled: false,
648        };
649        assert!(c.verify_superblock(&[]));
650        assert!(c.verify_inode(2, 0, &[]));
651        assert!(c.verify_dir_entry_tail(2, 0, &[0u8; 12]));
652        assert!(c.verify_extent_tail(2, 0, &[0u8; 64]));
653    }
654
655    #[test]
656    fn dir_tail_roundtrip_and_tamper() {
657        let c = Checksummer {
658            seed: 0xCAFEBABE,
659            enabled: true,
660        };
661        let ino = 42u32;
662        let gen = 0xDEADBEEFu32;
663        let mut block = vec![0u8; 4096];
664        // Plant a fake `ext4_dir_entry_tail` at the last 12 bytes — the spec
665        // reserves these and the CRC excludes them entirely.
666        let end = block.len();
667        block[end - 12..end - 8].copy_from_slice(&0u32.to_le_bytes()); // det_reserved_zero1
668        block[end - 8..end - 6].copy_from_slice(&12u16.to_le_bytes()); // det_rec_len
669        block[end - 6] = 0; // det_reserved_zero2
670        block[end - 5] = 0xDE; // det_reserved_ft
671                               // Some plausible directory content bytes (BEFORE the tail).
672        block[0..8].copy_from_slice(&[2, 0, 0, 0, 12, 0, 1, 2]);
673        block[100] = 0x5A;
674        // CRC covers block[..len-12]; tail's last 4 bytes hold the result.
675        let mut expected = linux_crc32c(c.seed, &ino.to_le_bytes());
676        expected = linux_crc32c(expected, &gen.to_le_bytes());
677        expected = linux_crc32c(expected, &block[..end - 12]);
678        block[end - 4..end].copy_from_slice(&expected.to_le_bytes());
679        assert!(c.verify_dir_entry_tail(ino, gen, &block));
680
681        // Tamper one byte inside the covered region, expect failure.
682        block[100] ^= 0xFF;
683        assert!(!c.verify_dir_entry_tail(ino, gen, &block));
684    }
685
686    #[test]
687    fn extent_tail_excludes_only_last_4_bytes() {
688        // The extent tail recipe excludes only the final u32 et_checksum,
689        // unlike dir_entry_tail which excludes the full 12-byte tail.
690        let c = Checksummer {
691            seed: 0x12345678,
692            enabled: true,
693        };
694        let mut block = vec![0u8; 1024];
695        block[0..12].copy_from_slice(&[0x0A, 0xF3, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0]);
696        block[500] = 0xAB;
697        let end = block.len();
698        let mut expected = linux_crc32c(c.seed, &7u32.to_le_bytes());
699        expected = linux_crc32c(expected, &9u32.to_le_bytes());
700        expected = linux_crc32c(expected, &block[..end - 4]);
701        block[end - 4..end].copy_from_slice(&expected.to_le_bytes());
702        assert!(c.verify_extent_tail(7, 9, &block));
703    }
704
705    #[test]
706    fn patch_extent_tail_is_verify_inverse() {
707        let c = Checksummer {
708            seed: 0xFEEDFACE,
709            enabled: true,
710        };
711        let mut block = vec![0u8; 4096];
712        // Synthetic leaf header + one entry so the body is interesting.
713        block[0..12].copy_from_slice(&[0x0A, 0xF3, 1, 0, 0x54, 0x01, 0, 0, 1, 2, 3, 4]);
714        block[12..24].copy_from_slice(&[0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0xE8, 0x03]);
715        let patched = c.patch_extent_tail(42, 0xABCDEF01, &mut block);
716        assert!(patched);
717        assert!(c.verify_extent_tail(42, 0xABCDEF01, &block));
718        // Tampering with body invalidates.
719        block[200] ^= 0xFF;
720        assert!(!c.verify_extent_tail(42, 0xABCDEF01, &block));
721    }
722
723    #[test]
724    fn patch_extent_tail_disabled_is_noop() {
725        let c = Checksummer {
726            seed: 0,
727            enabled: false,
728        };
729        let mut block = vec![0u8; 64];
730        assert!(!c.patch_extent_tail(1, 1, &mut block));
731        assert_eq!(&block[..], &[0u8; 64]);
732    }
733
734    #[test]
735    fn dir_tail_rejects_too_short_block_when_enabled() {
736        let c = Checksummer {
737            seed: 0,
738            enabled: true,
739        };
740        // Less than 12 bytes cannot even hold the tail struct.
741        assert!(!c.verify_dir_entry_tail(2, 0, &[0u8; 8]));
742    }
743
744    #[test]
745    fn crc_helpers_are_deterministic() {
746        let c = Checksummer {
747            seed: 0xDEAD_BEEF,
748            enabled: true,
749        };
750        let a = c.crc(b"hello");
751        let b = c.crc(b"hello");
752        assert_eq!(a, b);
753        let p1 = c.crc_with_prefix(1, b"hello");
754        let p2 = c.crc_with_prefix(2, b"hello");
755        assert_ne!(p1, p2, "prefix changes hash");
756    }
757
758    /// Tests that read a fixture from `test-disks/` (`chore fixtures`).
759    mod needs_host {
760        use super::*;
761
762        /// Verify our superblock-checksum routine against a real ext4-basic.img
763        /// (which has metadata_csum enabled).
764        #[test]
765        fn verifies_real_superblock() {
766            use crate::block_io::FileDevice;
767
768            let path = fs_ext4_test_support::fixture(env!("CARGO_MANIFEST_DIR"), "ext4-basic.img");
769            let dev = FileDevice::open(&path).unwrap_or_else(|e| panic!("open {path}: {e}"));
770            let sb = Superblock::read(&dev).expect("parse sb");
771            let csum = Checksummer::from_superblock(&sb);
772            assert!(
773                csum.enabled,
774                "ext4-basic.img is built with metadata_csum (test-disks/guest-build-images.sh)"
775            );
776            assert!(
777                csum.verify_superblock(&sb.raw),
778                "superblock checksum mismatch on {path}"
779            );
780        }
781
782        /// Verify our BGD checksum against a real image — every group must pass.
783        #[test]
784        fn verifies_real_bgd() {
785            use crate::block_io::{BlockDevice, FileDevice};
786
787            let path = fs_ext4_test_support::fixture(env!("CARGO_MANIFEST_DIR"), "ext4-basic.img");
788            let dev = FileDevice::open(&path).unwrap_or_else(|e| panic!("open {path}: {e}"));
789            let sb = Superblock::read(&dev).expect("parse sb");
790            let csum = Checksummer::from_superblock(&sb);
791            assert!(
792                csum.enabled,
793                "{path} is built with metadata_csum (test-disks/guest-build-images.sh)"
794            );
795            // Read raw BGT and verify each descriptor.
796            let block_size = sb.block_size() as u64;
797            let bgt_off = (sb.first_data_block as u64 + 1) * block_size;
798            let group_count = sb.block_group_count();
799            let total = group_count as usize * sb.desc_size as usize;
800            let mut buf = vec![0u8; total];
801            dev.read_at(bgt_off, &mut buf).expect("read bgt");
802            for i in 0..group_count as usize {
803                let off = i * sb.desc_size as usize;
804                let raw = &buf[off..off + sb.desc_size as usize];
805                assert!(
806                    csum.verify_bgd(i as u32, raw, sb.desc_size),
807                    "BGD {i} checksum mismatch on {path}"
808                );
809            }
810        }
811
812        /// Verify our inode checksum against a real image — root inode (2) must pass.
813        #[test]
814        fn verifies_real_inode() {
815            use crate::block_io::FileDevice;
816            use crate::fs::Filesystem;
817            use std::sync::Arc;
818
819            let path = fs_ext4_test_support::fixture(env!("CARGO_MANIFEST_DIR"), "ext4-basic.img");
820            let dev = FileDevice::open(&path).unwrap_or_else(|e| panic!("open {path}: {e}"));
821            let dev_dyn: Arc<dyn crate::block_io::BlockDevice> = Arc::new(dev);
822            let fs = Filesystem::mount(dev_dyn).expect("mount");
823            assert!(
824                fs.csum.enabled,
825                "{path} is built with metadata_csum (test-disks/guest-build-images.sh)"
826            );
827            // Inode 2 = root dir.
828            let (inode, raw) = fs.read_inode_verified(2).expect("read root inode");
829            assert!(inode.is_dir());
830            assert!(fs.csum.verify_inode(2, inode.generation, &raw));
831        }
832
833        /// Verify dir-block tail csum against a real image. Root dir on
834        /// ext4-basic.img is a single-block linear directory with a tail.
835        #[test]
836        fn verifies_real_dir_tail() {
837            use crate::block_io::{BlockDevice, FileDevice};
838            use crate::dir;
839            use crate::extent;
840            use crate::fs::Filesystem;
841            use std::sync::Arc;
842
843            let path = fs_ext4_test_support::fixture(env!("CARGO_MANIFEST_DIR"), "ext4-basic.img");
844            let dev = FileDevice::open(&path).unwrap_or_else(|e| panic!("open {path}: {e}"));
845            let dev_dyn: Arc<dyn BlockDevice> = Arc::new(dev);
846            let fs = Filesystem::mount(dev_dyn.clone()).expect("mount");
847            assert!(
848                fs.csum.enabled,
849                "{path} is built with metadata_csum (test-disks/guest-build-images.sh)"
850            );
851            let (root_inode, _raw) = fs.read_inode_verified(2).expect("root inode");
852            let bs = fs.sb.block_size();
853            let phys = extent::map_logical(&root_inode.block, dev_dyn.as_ref(), bs, 0)
854                .expect("map_logical")
855                .expect("dir block 0 mapped");
856            let mut block = vec![0u8; bs as usize];
857            dev_dyn.read_at(phys * bs as u64, &mut block).unwrap();
858            assert!(
859                dir::has_csum_tail(&block),
860                "expected tail on root dir block"
861            );
862            assert!(
863                fs.csum
864                    .verify_dir_entry_tail(2, root_inode.generation, &block),
865                "dir tail csum mismatch on {path} root dir"
866            );
867        }
868
869        /// Verify extent-block tail csum against ext4-deep-extents.img: any file
870        /// with depth > 0 has off-inode extent index/leaf blocks. We pick the
871        /// largest regular file and traverse one internal-node block.
872        #[test]
873        fn verifies_real_extent_tail() {
874            use crate::block_io::{BlockDevice, FileDevice};
875            use crate::extent::{self, ExtentHeader, ExtentIdx, EXT4_EXT_NODE_SIZE};
876            use crate::fs::Filesystem;
877            use std::sync::Arc;
878
879            let path =
880                fs_ext4_test_support::fixture(env!("CARGO_MANIFEST_DIR"), "ext4-deep-extents.img");
881            let dev = FileDevice::open(&path).unwrap_or_else(|e| panic!("open {path}: {e}"));
882            let dev_dyn: Arc<dyn BlockDevice> = Arc::new(dev);
883            let fs = Filesystem::mount(dev_dyn.clone()).expect("mount");
884            assert!(
885                fs.csum.enabled,
886                "{path} is built with metadata_csum (test-disks/guest-build-images.sh)"
887            );
888            // Walk first ~50 inodes looking for one with depth>0.
889            let bs = fs.sb.block_size();
890            let mut found = None;
891            for ino in 11..200u32 {
892                let (inode, _raw) = match fs.read_inode_verified(ino) {
893                    Ok(x) => x,
894                    Err(_) => continue,
895                };
896                if !inode.is_file() || !inode.has_extents() {
897                    continue;
898                }
899                let header = match ExtentHeader::parse(&inode.block) {
900                    Ok(h) => h,
901                    Err(_) => continue,
902                };
903                if header.depth > 0 && header.entries >= 1 {
904                    let idx =
905                        ExtentIdx::parse(&inode.block[EXT4_EXT_NODE_SIZE..2 * EXT4_EXT_NODE_SIZE])
906                            .expect("parse first idx");
907                    found = Some((ino, inode.generation, idx.leaf_block));
908                    break;
909                }
910            }
911            let (ino, gen, child_block) = found.unwrap_or_else(|| {
912                panic!(
913                    "no depth>0 inode in the first 200 of {path}: its sparse.bin is built \
914                 with a multi-level extent tree (test-disks/guest-build-images.sh)"
915                )
916            });
917            let mut buf = vec![0u8; bs as usize];
918            dev_dyn.read_at(child_block * bs as u64, &mut buf).unwrap();
919            assert!(
920                fs.csum.verify_extent_tail(ino, gen, &buf),
921                "extent block csum mismatch (ino={ino} child_block={child_block} on {path})"
922            );
923
924            // Also exercise the verified traversal API end-to-end.
925            let (inode, _) = fs.read_inode_verified(ino).unwrap();
926            let ctx = extent::ExtentVerifyCtx {
927                ino,
928                generation: gen,
929                csum: &fs.csum,
930            };
931            let _ = extent::lookup_verified(&inode.block, dev_dyn.as_ref(), bs, 0, Some(&ctx))
932                .expect("lookup_verified must accept valid extent blocks");
933        }
934    }
935}