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}