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//! Phase 1: read-only verification. We do NOT recompute checksums on writes
23//! (no writes yet). Verification is currently INFORMATIONAL — corrupt
24//! metadata would still parse; this module just lets callers decide whether
25//! to trust the result.
26
27use crate::features::{Incompat, RoCompat};
28use crate::superblock::Superblock;
29
30/// Linux-semantics CRC32C: no final XOR at either end. The `crc32c` crate's
31/// `crc32c_append(s, d)` is `~iterate(~s, d)`; the kernel's `__crc32c_le(c, d, l)`
32/// is `iterate(c, d)`. Wrap to get the kernel's semantics out of the crate.
33///
34/// Public so write-path callers that rewrite a metadata block (dir, BGD, SB)
35/// can recompute the tail checksum inline without rebuilding a `Checksummer`.
36#[inline]
37pub fn linux_crc32c(seed: u32, data: &[u8]) -> u32 {
38 !crc32c::crc32c_append(!seed, data)
39}
40
41/// Per-mount checksum context: the seed and "is it enabled" flag.
42#[derive(Debug, Clone, Copy)]
43pub struct Checksummer {
44 pub seed: u32,
45 pub enabled: bool,
46}
47
48/// `file_type` of the fake dirent that ends a checksummed directory
49/// block — `EXT4_FT_DIR_CSUM` in e2fsprogs.
50///
51/// Not a real file type: it is out of range for one, which is how a
52/// reader tells the tail apart from an entry.
53pub const DIR_ENTRY_TAIL_FILE_TYPE: u8 = 0xDE;
54
55impl Checksummer {
56 /// Derive the checksum context from a parsed superblock.
57 ///
58 /// Per spec: if `INCOMPAT_CSUM_SEED` is set, use the explicit
59 /// `s_checksum_seed` field. Otherwise, the seed is the kernel's
60 /// `__crc32c_le(~0, UUID, 16)` — i.e. our `linux_crc32c(!0, UUID)`.
61 pub fn from_superblock(sb: &Superblock) -> Self {
62 let enabled = (sb.feature_ro_compat & RoCompat::METADATA_CSUM.bits()) != 0;
63 let seed = if (sb.feature_incompat & Incompat::CSUM_SEED.bits()) != 0 {
64 sb.checksum_seed
65 } else {
66 linux_crc32c(!0, &sb.uuid)
67 };
68 Self { seed, enabled }
69 }
70
71 /// Linux-semantics CRC32C of a buffer using the mount-wide seed.
72 pub fn crc(&self, data: &[u8]) -> u32 {
73 linux_crc32c(self.seed, data)
74 }
75
76 /// Linux-semantics CRC32C with a 32-bit context prefix folded in first.
77 pub fn crc_with_prefix(&self, prefix: u32, data: &[u8]) -> u32 {
78 let mid = linux_crc32c(self.seed, &prefix.to_le_bytes());
79 linux_crc32c(mid, data)
80 }
81
82 /// Verify the superblock checksum. Stored at byte offset 0x3FC; CRC
83 /// covers the first 0x3FC bytes. Initial seed is `~0` (NOT the per-FS
84 /// seed — superblock checksum is special since the seed lives inside it).
85 pub fn verify_superblock(&self, sb_raw: &[u8]) -> bool {
86 if !self.enabled {
87 return true;
88 }
89 if sb_raw.len() < 1024 {
90 return false;
91 }
92 let stored = u32::from_le_bytes(sb_raw[0x3FC..0x400].try_into().unwrap());
93 let computed = linux_crc32c(!0, &sb_raw[..0x3FC]);
94 stored == computed
95 }
96
97 /// Verify a block group descriptor's checksum.
98 ///
99 /// Per spec (`ext4/group_descr.html`), when `RO_COMPAT_METADATA_CSUM` is
100 /// set the GDT checksum is computed as:
101 ///
102 /// ```text
103 /// crc32c(seed, group_no_le_u32 || bgd_with_csum_zeroed) & 0xFFFF
104 /// ```
105 ///
106 /// `desc_size` is the on-disk descriptor size (32 or 64).
107 /// `bgd_raw` must be at least `desc_size` bytes; the stored checksum at
108 /// offset 0x1E is treated as zero for the computation.
109 pub fn verify_bgd(&self, group_no: u32, bgd_raw: &[u8], desc_size: u16) -> bool {
110 if !self.enabled {
111 return true;
112 }
113 let n = desc_size as usize;
114 if bgd_raw.len() < n || n < 0x20 {
115 return false;
116 }
117 let stored = u16::from_le_bytes(bgd_raw[0x1E..0x20].try_into().unwrap());
118
119 let mut tmp = bgd_raw[..n].to_vec();
120 tmp[0x1E] = 0;
121 tmp[0x1F] = 0;
122
123 let computed16 = self.crc_with_prefix(group_no, &tmp) as u16;
124 computed16 == stored
125 }
126
127 /// Verify a directory block's trailing `ext4_dir_entry_tail` checksum.
128 ///
129 /// Linear directory blocks with `metadata_csum` enabled end in a 12-byte
130 /// `struct ext4_dir_entry_tail { u32 det_reserved_zero1; u16 det_rec_len;
131 /// u8 det_reserved_zero2; u8 det_reserved_ft; u32 det_checksum; }`.
132 ///
133 /// Per Linux `fs/ext4/dir.c::ext4_dirent_csum_set` the CRC covers
134 /// **`block[0..block_size - 12]`** — i.e. everything BEFORE the tail.
135 /// The tail's own bytes (including `det_checksum`) are excluded:
136 ///
137 /// ```text
138 /// crc32c(seed, ino_le) → crc32c(., gen_le) → crc32c(., block[..len-12])
139 /// ```
140 ///
141 /// `block` is the whole directory block including the trailing tail.
142 pub fn verify_dir_entry_tail(&self, ino: u32, generation: u32, block: &[u8]) -> bool {
143 if !self.enabled {
144 return true;
145 }
146 if block.len() < 12 {
147 return false;
148 }
149 let end = block.len();
150 let stored = u32::from_le_bytes(block[end - 4..end].try_into().unwrap());
151
152 let mut c = linux_crc32c(self.seed, &ino.to_le_bytes());
153 c = linux_crc32c(c, &generation.to_le_bytes());
154 c = linux_crc32c(c, &block[..end - 12]);
155 c == stored
156 }
157
158 /// Plant the `ext4_dir_entry_tail` and checksum a directory block.
159 ///
160 /// The mirror of [`Self::verify_dir_entry_tail`], and the sibling
161 /// that was missing: `patch_extent_tail` and `patch_xattr_block`
162 /// both existed, so the one recipe written most often was the one
163 /// with no helper. It was hand-rolled at **sixteen** sites across
164 /// `fs.rs`, `fsck.rs`, `mkfs.rs` and the tests, in two different
165 /// addressing idioms (`bs - 12` with `+4/+6/+7`, and
166 /// `block.len()` with `-8/-6/-5`) that a reader has to prove
167 /// equivalent at each one.
168 ///
169 /// The tail is a **fake directory entry** occupying the last 12
170 /// bytes: `inode = 0` so no scan mistakes it for a real one,
171 /// `rec_len = 12` so a walk steps over it, `name_len = 0`, and
172 /// `file_type = 0xDE` as the marker `has_csum_tail` looks for. The
173 /// CRC then covers `block[..len - 12]` — the whole block *except*
174 /// the tail entry, which is what separates this from
175 /// [`Self::patch_extent_tail`], where only the trailing 4 bytes are
176 /// excluded.
177 ///
178 /// Planting is idempotent: a block that already carries the tail
179 /// gets the same twelve bytes back, so callers that only need the
180 /// checksum recomputed can use this too rather than keeping a
181 /// second recipe for that case.
182 ///
183 /// Every constant here has to agree with e2fsprogs, and a
184 /// disagreement produces no error until the volume is mounted
185 /// somewhere else — which is the argument for there being one copy
186 /// of them.
187 ///
188 /// No-op when checksums are disabled or the block is under 12
189 /// bytes; returns true when it patched.
190 pub fn patch_dir_entry_tail(&self, ino: u32, generation: u32, block: &mut [u8]) -> bool {
191 if !self.enabled || block.len() < 12 {
192 return false;
193 }
194 let end = block.len();
195 block[end - 12..end - 8].copy_from_slice(&0u32.to_le_bytes()); // inode = 0
196 block[end - 8..end - 6].copy_from_slice(&12u16.to_le_bytes()); // rec_len
197 block[end - 6] = 0; // name_len
198 block[end - 5] = DIR_ENTRY_TAIL_FILE_TYPE;
199
200 let mut c = linux_crc32c(self.seed, &ino.to_le_bytes());
201 c = linux_crc32c(c, &generation.to_le_bytes());
202 c = linux_crc32c(c, &block[..end - 12]);
203 block[end - 4..end].copy_from_slice(&c.to_le_bytes());
204 true
205 }
206
207 /// Verify an extent-block tail checksum.
208 ///
209 /// Extent index/leaf blocks (those read off-inode when the tree has
210 /// internal nodes) end in a 4-byte `struct ext4_extent_tail
211 /// { u32 et_checksum; }`. Per Linux
212 /// `fs/ext4/extents.c::ext4_extent_block_csum_set` the CRC covers
213 /// **`block[0..len-4]`** — only the trailing `et_checksum` field is
214 /// excluded:
215 ///
216 /// ```text
217 /// crc32c(seed, ino_le) → crc32c(., gen_le) → crc32c(., block[..len-4])
218 /// ```
219 ///
220 /// Different from `verify_dir_entry_tail`, which excludes the full
221 /// 12-byte tail entry.
222 pub fn verify_extent_tail(&self, ino: u32, generation: u32, block: &[u8]) -> bool {
223 if !self.enabled {
224 return true;
225 }
226 if block.len() < 4 {
227 return false;
228 }
229 let end = block.len();
230 let stored = u32::from_le_bytes(block[end - 4..end].try_into().unwrap());
231
232 let mut c = linux_crc32c(self.seed, &ino.to_le_bytes());
233 c = linux_crc32c(c, &generation.to_le_bytes());
234 c = linux_crc32c(c, &block[..end - 4]);
235 c == stored
236 }
237
238 /// Verify a parsed inode's checksum.
239 /// Chained: seed → ino_le → gen_le → inode_bytes (with checksum slots zeroed).
240 pub fn verify_inode(&self, ino: u32, generation: u32, inode_raw: &[u8]) -> bool {
241 if !self.enabled {
242 return true;
243 }
244 // A truncated read is a refusal, not a pass — the same answer
245 // `verify_superblock`, `verify_dir_entry_tail` and
246 // `verify_extent_tail` give, and for the same reason: the
247 // caller got fewer bytes than it asked for, so a checksum
248 // computed here would cover bytes that are not the ones on
249 // disk. Reporting `true` says an inode verified when nothing
250 // verified it.
251 //
252 // This used to read `if !self.enabled || inode_raw.len() < 128`,
253 // one `||` instead of two `if`s — a difference invisible unless
254 // you read all four verifiers together.
255 if inode_raw.len() < crate::inode::GOOD_OLD_INODE_SIZE {
256 return false;
257 }
258 let stored_lo = u16::from_le_bytes(inode_raw[0x7C..0x7E].try_into().unwrap()) as u32;
259 let stored_hi = if inode_raw.len() >= 0x84 {
260 u16::from_le_bytes(inode_raw[0x82..0x84].try_into().unwrap()) as u32
261 } else {
262 0
263 };
264 let stored = (stored_hi << 16) | stored_lo;
265 match self.compute_inode_checksum(ino, generation, inode_raw) {
266 Some((lo, hi)) => ((hi as u32) << 16 | lo as u32) == stored,
267 None => true, // disabled / too short — accept
268 }
269 }
270
271 /// Write the `ext4_extent_tail.et_checksum` u32 at the end of a freshly-
272 /// built extent index/leaf block. Mirrors `verify_extent_tail`: the CRC
273 /// covers `block[..len-4]`, chained seed → ino → generation → body.
274 /// No-op when checksums are disabled; returns true when it patched.
275 pub fn patch_extent_tail(&self, ino: u32, generation: u32, block: &mut [u8]) -> bool {
276 if !self.enabled || block.len() < 4 {
277 return false;
278 }
279 let end = block.len();
280 let mut c = linux_crc32c(self.seed, &ino.to_le_bytes());
281 c = linux_crc32c(c, &generation.to_le_bytes());
282 c = linux_crc32c(c, &block[..end - 4]);
283 block[end - 4..end].copy_from_slice(&c.to_le_bytes());
284 true
285 }
286
287 /// Verify an external xattr block's checksum.
288 ///
289 /// Per Linux `fs/ext4/xattr.c::ext4_xattr_block_csum`, the recipe is:
290 ///
291 /// ```text
292 /// crc32c(seed, block_nr_le_u64)
293 /// → crc32c(., block[0x00..0x10]) // magic, refcount, blocks, hash
294 /// → crc32c(., [0u32]) // h_checksum slot zeroed (4 bytes)
295 /// → crc32c(., block[0x14..end]) // rest of block
296 /// ```
297 ///
298 /// The stored u32 lives at offset 0x10 of the block.
299 pub fn verify_xattr_block(&self, block_nr: u64, block: &[u8]) -> bool {
300 if !self.enabled {
301 return true;
302 }
303 if block.len() < 0x20 {
304 return false;
305 }
306 let stored = u32::from_le_bytes(block[0x10..0x14].try_into().unwrap());
307 let computed = self.compute_xattr_block_csum(block_nr, block);
308 stored == computed
309 }
310
311 /// Patch the `h_checksum` field of an external xattr block in place.
312 /// Mirrors [`verify_xattr_block`]. No-op when checksums are disabled.
313 /// Returns `true` when the block was patched.
314 pub fn patch_xattr_block(&self, block_nr: u64, block: &mut [u8]) -> bool {
315 if !self.enabled || block.len() < 0x20 {
316 return false;
317 }
318 let csum = self.compute_xattr_block_csum(block_nr, block);
319 block[0x10..0x14].copy_from_slice(&csum.to_le_bytes());
320 true
321 }
322
323 fn compute_xattr_block_csum(&self, block_nr: u64, block: &[u8]) -> u32 {
324 let mut c = linux_crc32c(self.seed, &block_nr.to_le_bytes());
325 c = linux_crc32c(c, &block[0x00..0x10]);
326 c = linux_crc32c(c, &0u32.to_le_bytes());
327 c = linux_crc32c(c, &block[0x14..]);
328 c
329 }
330
331 /// Compute the inode checksum as two u16 halves (lo=checksum_lo at 0x7C,
332 /// hi=checksum_hi at 0x82). Returns `None` when checksums are disabled
333 /// or the buffer is too short to patch. Callers use this after mutating
334 /// an inode image to restore the checksum before writing back.
335 pub fn compute_inode_checksum(
336 &self,
337 ino: u32,
338 generation: u32,
339 inode_raw: &[u8],
340 ) -> Option<(u16, u16)> {
341 if !self.enabled || inode_raw.len() < crate::inode::GOOD_OLD_INODE_SIZE {
342 return None;
343 }
344 // i_checksum_hi (0x82) is part of the checksum only when i_extra_isize
345 // (0x80) is large enough to cover it — the kernel's EXT4_FITS_IN_INODE
346 // test, which here means i_extra_isize >= 4. On a zeroed freed inode
347 // (i_extra_isize = 0) the kernel uses ONLY the 16-bit lo checksum and
348 // treats 0x82 as ordinary (zero) data; zeroing hi and storing a full
349 // 32-bit value there mismatches ("checksum does not match inode").
350 let fits_hi = inode_raw.len() >= 0x84
351 && u16::from_le_bytes(inode_raw[0x80..0x82].try_into().unwrap()) >= 4;
352 let mut tmp = inode_raw.to_vec();
353 tmp[0x7C] = 0;
354 tmp[0x7D] = 0;
355 if fits_hi {
356 tmp[0x82] = 0;
357 tmp[0x83] = 0;
358 }
359 let mut c = linux_crc32c(self.seed, &ino.to_le_bytes());
360 c = linux_crc32c(c, &generation.to_le_bytes());
361 c = linux_crc32c(c, &tmp);
362 let lo = (c & 0xFFFF) as u16;
363 let hi = if fits_hi {
364 ((c >> 16) & 0xFFFF) as u16
365 } else {
366 0
367 };
368 Some((lo, hi))
369 }
370}
371
372#[cfg(test)]
373mod tests {
374
375 // --- a short buffer is a refusal, on every verifier ------------------
376 //
377 // Written before the fix. `verify_inode` passed a truncated buffer;
378 // its three siblings refused one. Nothing in the crate noticed the
379 // difference, because the short-buffer axis had no coverage at all.
380
381 /// A checksummer that is switched on, with an arbitrary seed.
382 fn enabled() -> Checksummer {
383 Checksummer {
384 enabled: true,
385 seed: 0xDEAD_BEEF,
386 }
387 }
388
389 /// Every verifier refuses a buffer too short to hold the field it
390 /// checks.
391 ///
392 /// This is a policy, not four separate decisions, and it is the
393 /// safe direction: a truncated read means the caller got less than
394 /// it asked for, and the checksum it would compute covers bytes
395 /// that are not the ones on disk. Returning `true` there reports a
396 /// structure as verified when nothing verified it.
397 ///
398 /// Asserted as a set so a fifth verifier cannot quietly pick the
399 /// other answer — which is exactly how `verify_inode` came to
400 /// differ from the other three.
401 #[test]
402 fn every_verifier_refuses_a_buffer_too_short_to_check() {
403 let c = enabled();
404 assert!(
405 !c.verify_superblock(&[0u8; 64]),
406 "superblock: a 64-byte buffer cannot hold a checksum at 0x3FC"
407 );
408 assert!(
409 !c.verify_dir_entry_tail(2, 0, &[0u8; 8]),
410 "dir entry tail: an 8-byte buffer cannot hold a 12-byte tail"
411 );
412 assert!(
413 !c.verify_extent_tail(2, 0, &[0u8; 2]),
414 "extent tail: a 2-byte buffer cannot hold a 4-byte checksum"
415 );
416 assert!(
417 !c.verify_inode(2, 0, &[0u8; 64]),
418 "inode: a 64-byte buffer cannot hold a checksum at 0x7C"
419 );
420 }
421
422 /// And every one of them still passes everything when checksums are
423 /// off, short buffer included.
424 ///
425 /// The two conditions are separate for a reason: "we do not check"
426 /// and "we checked and it failed" are different answers, and a
427 /// filesystem without `metadata_csum` must not start failing reads
428 /// because a buffer was short.
429 #[test]
430 fn a_disabled_checksummer_passes_even_a_short_buffer() {
431 let c = Checksummer {
432 enabled: false,
433 seed: 0,
434 };
435 assert!(c.verify_superblock(&[0u8; 4]));
436 assert!(c.verify_dir_entry_tail(2, 0, &[0u8; 4]));
437 assert!(c.verify_extent_tail(2, 0, &[0u8; 1]));
438 assert!(c.verify_inode(2, 0, &[0u8; 4]));
439 }
440
441 /// The boundary itself: 127 bytes is refused, 128 is checked.
442 ///
443 /// 128 is where `verify_inode`'s own field lives — `i_checksum_lo`
444 /// at 0x7C..0x7E — so a buffer one byte shorter cannot hold it.
445 #[test]
446 fn the_inode_length_boundary_is_where_the_field_ends() {
447 let c = enabled();
448 assert!(!c.verify_inode(2, 0, &[0u8; 127]), "127 is too short");
449 // 128 bytes of zeros is a real check that simply fails: the
450 // stored checksum is zero and the computed one is not.
451 assert!(
452 !c.verify_inode(2, 0, &[0u8; 128]),
453 "128 is checked, and all-zero bytes do not verify"
454 );
455 }
456
457 use super::*;
458
459 #[test]
460 fn disabled_when_feature_off() {
461 // crc32c of an empty seed always yields 0; not interesting.
462 let c = Checksummer {
463 seed: 0,
464 enabled: false,
465 };
466 assert!(c.verify_superblock(&[]));
467 assert!(c.verify_inode(2, 0, &[]));
468 assert!(c.verify_dir_entry_tail(2, 0, &[0u8; 12]));
469 assert!(c.verify_extent_tail(2, 0, &[0u8; 64]));
470 }
471
472 #[test]
473 fn dir_tail_roundtrip_and_tamper() {
474 let c = Checksummer {
475 seed: 0xCAFEBABE,
476 enabled: true,
477 };
478 let ino = 42u32;
479 let gen = 0xDEADBEEFu32;
480 let mut block = vec![0u8; 4096];
481 // Plant a fake `ext4_dir_entry_tail` at the last 12 bytes — the spec
482 // reserves these and the CRC excludes them entirely.
483 let end = block.len();
484 block[end - 12..end - 8].copy_from_slice(&0u32.to_le_bytes()); // det_reserved_zero1
485 block[end - 8..end - 6].copy_from_slice(&12u16.to_le_bytes()); // det_rec_len
486 block[end - 6] = 0; // det_reserved_zero2
487 block[end - 5] = 0xDE; // det_reserved_ft
488 // Some plausible directory content bytes (BEFORE the tail).
489 block[0..8].copy_from_slice(&[2, 0, 0, 0, 12, 0, 1, 2]);
490 block[100] = 0x5A;
491 // CRC covers block[..len-12]; tail's last 4 bytes hold the result.
492 let mut expected = linux_crc32c(c.seed, &ino.to_le_bytes());
493 expected = linux_crc32c(expected, &gen.to_le_bytes());
494 expected = linux_crc32c(expected, &block[..end - 12]);
495 block[end - 4..end].copy_from_slice(&expected.to_le_bytes());
496 assert!(c.verify_dir_entry_tail(ino, gen, &block));
497
498 // Tamper one byte inside the covered region, expect failure.
499 block[100] ^= 0xFF;
500 assert!(!c.verify_dir_entry_tail(ino, gen, &block));
501 }
502
503 #[test]
504 fn extent_tail_excludes_only_last_4_bytes() {
505 // The extent tail recipe excludes only the final u32 et_checksum,
506 // unlike dir_entry_tail which excludes the full 12-byte tail.
507 let c = Checksummer {
508 seed: 0x12345678,
509 enabled: true,
510 };
511 let mut block = vec![0u8; 1024];
512 block[0..12].copy_from_slice(&[0x0A, 0xF3, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0]);
513 block[500] = 0xAB;
514 let end = block.len();
515 let mut expected = linux_crc32c(c.seed, &7u32.to_le_bytes());
516 expected = linux_crc32c(expected, &9u32.to_le_bytes());
517 expected = linux_crc32c(expected, &block[..end - 4]);
518 block[end - 4..end].copy_from_slice(&expected.to_le_bytes());
519 assert!(c.verify_extent_tail(7, 9, &block));
520 }
521
522 #[test]
523 fn patch_extent_tail_is_verify_inverse() {
524 let c = Checksummer {
525 seed: 0xFEEDFACE,
526 enabled: true,
527 };
528 let mut block = vec![0u8; 4096];
529 // Synthetic leaf header + one entry so the body is interesting.
530 block[0..12].copy_from_slice(&[0x0A, 0xF3, 1, 0, 0x54, 0x01, 0, 0, 1, 2, 3, 4]);
531 block[12..24].copy_from_slice(&[0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0xE8, 0x03]);
532 let patched = c.patch_extent_tail(42, 0xABCDEF01, &mut block);
533 assert!(patched);
534 assert!(c.verify_extent_tail(42, 0xABCDEF01, &block));
535 // Tampering with body invalidates.
536 block[200] ^= 0xFF;
537 assert!(!c.verify_extent_tail(42, 0xABCDEF01, &block));
538 }
539
540 #[test]
541 fn patch_extent_tail_disabled_is_noop() {
542 let c = Checksummer {
543 seed: 0,
544 enabled: false,
545 };
546 let mut block = vec![0u8; 64];
547 assert!(!c.patch_extent_tail(1, 1, &mut block));
548 assert_eq!(&block[..], &[0u8; 64]);
549 }
550
551 #[test]
552 fn dir_tail_rejects_too_short_block_when_enabled() {
553 let c = Checksummer {
554 seed: 0,
555 enabled: true,
556 };
557 // Less than 12 bytes cannot even hold the tail struct.
558 assert!(!c.verify_dir_entry_tail(2, 0, &[0u8; 8]));
559 }
560
561 #[test]
562 fn crc_helpers_are_deterministic() {
563 let c = Checksummer {
564 seed: 0xDEAD_BEEF,
565 enabled: true,
566 };
567 let a = c.crc(b"hello");
568 let b = c.crc(b"hello");
569 assert_eq!(a, b);
570 let p1 = c.crc_with_prefix(1, b"hello");
571 let p2 = c.crc_with_prefix(2, b"hello");
572 assert_ne!(p1, p2, "prefix changes hash");
573 }
574
575 /// Verify our superblock-checksum routine against a real ext4-basic.img
576 /// (which has metadata_csum enabled).
577 #[test]
578 fn verifies_real_superblock() {
579 use crate::block_io::FileDevice;
580
581 let path = "test-disks/ext4-basic.img";
582 let dev = match FileDevice::open(path) {
583 Ok(d) => d,
584 Err(_) => {
585 eprintln!("skip: {path} not present");
586 return;
587 }
588 };
589 let sb = Superblock::read(&dev).expect("parse sb");
590 let csum = Checksummer::from_superblock(&sb);
591 if !csum.enabled {
592 eprintln!("skip: metadata_csum not enabled in ext4-basic.img");
593 return;
594 }
595 assert!(
596 csum.verify_superblock(&sb.raw),
597 "superblock checksum mismatch on {path}"
598 );
599 }
600
601 /// Verify our BGD checksum against a real image — every group must pass.
602 #[test]
603 fn verifies_real_bgd() {
604 use crate::block_io::{BlockDevice, FileDevice};
605
606 let path = "test-disks/ext4-basic.img";
607 let dev = match FileDevice::open(path) {
608 Ok(d) => d,
609 Err(_) => {
610 eprintln!("skip: {path} not present");
611 return;
612 }
613 };
614 let sb = Superblock::read(&dev).expect("parse sb");
615 let csum = Checksummer::from_superblock(&sb);
616 if !csum.enabled {
617 eprintln!("skip: metadata_csum not enabled");
618 return;
619 }
620 // Read raw BGT and verify each descriptor.
621 let block_size = sb.block_size() as u64;
622 let bgt_off = (sb.first_data_block as u64 + 1) * block_size;
623 let group_count = sb.block_group_count();
624 let total = group_count as usize * sb.desc_size as usize;
625 let mut buf = vec![0u8; total];
626 dev.read_at(bgt_off, &mut buf).expect("read bgt");
627 for i in 0..group_count as usize {
628 let off = i * sb.desc_size as usize;
629 let raw = &buf[off..off + sb.desc_size as usize];
630 assert!(
631 csum.verify_bgd(i as u32, raw, sb.desc_size),
632 "BGD {i} checksum mismatch on {path}"
633 );
634 }
635 }
636
637 /// Verify our inode checksum against a real image — root inode (2) must pass.
638 #[test]
639 fn verifies_real_inode() {
640 use crate::block_io::FileDevice;
641 use crate::fs::Filesystem;
642 use std::sync::Arc;
643
644 let path = "test-disks/ext4-basic.img";
645 let dev = match FileDevice::open(path) {
646 Ok(d) => d,
647 Err(_) => {
648 eprintln!("skip: {path} not present");
649 return;
650 }
651 };
652 let dev_dyn: Arc<dyn crate::block_io::BlockDevice> = Arc::new(dev);
653 let fs = Filesystem::mount(dev_dyn).expect("mount");
654 if !fs.csum.enabled {
655 eprintln!("skip: metadata_csum not enabled");
656 return;
657 }
658 // Inode 2 = root dir.
659 let (inode, raw) = fs.read_inode_verified(2).expect("read root inode");
660 assert!(inode.is_dir());
661 assert!(fs.csum.verify_inode(2, inode.generation, &raw));
662 }
663
664 /// Verify dir-block tail csum against a real image. Root dir on
665 /// ext4-basic.img is a single-block linear directory with a tail.
666 #[test]
667 fn verifies_real_dir_tail() {
668 use crate::block_io::{BlockDevice, FileDevice};
669 use crate::dir;
670 use crate::extent;
671 use crate::fs::Filesystem;
672 use std::sync::Arc;
673
674 let path = "test-disks/ext4-basic.img";
675 let dev = match FileDevice::open(path) {
676 Ok(d) => d,
677 Err(_) => {
678 eprintln!("skip: {path} not present");
679 return;
680 }
681 };
682 let dev_dyn: Arc<dyn BlockDevice> = Arc::new(dev);
683 let fs = Filesystem::mount(dev_dyn.clone()).expect("mount");
684 if !fs.csum.enabled {
685 eprintln!("skip: metadata_csum not enabled");
686 return;
687 }
688 let (root_inode, _raw) = fs.read_inode_verified(2).expect("root inode");
689 let bs = fs.sb.block_size();
690 let phys = extent::map_logical(&root_inode.block, dev_dyn.as_ref(), bs, 0)
691 .expect("map_logical")
692 .expect("dir block 0 mapped");
693 let mut block = vec![0u8; bs as usize];
694 dev_dyn.read_at(phys * bs as u64, &mut block).unwrap();
695 assert!(
696 dir::has_csum_tail(&block),
697 "expected tail on root dir block"
698 );
699 assert!(
700 fs.csum
701 .verify_dir_entry_tail(2, root_inode.generation, &block),
702 "dir tail csum mismatch on {path} root dir"
703 );
704 }
705
706 /// Verify extent-block tail csum against ext4-deep-extents.img: any file
707 /// with depth > 0 has off-inode extent index/leaf blocks. We pick the
708 /// largest regular file and traverse one internal-node block.
709 #[test]
710 fn verifies_real_extent_tail() {
711 use crate::block_io::{BlockDevice, FileDevice};
712 use crate::extent::{self, ExtentHeader, ExtentIdx, EXT4_EXT_NODE_SIZE};
713 use crate::fs::Filesystem;
714 use std::sync::Arc;
715
716 let path = "test-disks/ext4-deep-extents.img";
717 let dev = match FileDevice::open(path) {
718 Ok(d) => d,
719 Err(_) => {
720 eprintln!("skip: {path} not present");
721 return;
722 }
723 };
724 let dev_dyn: Arc<dyn BlockDevice> = Arc::new(dev);
725 let fs = Filesystem::mount(dev_dyn.clone()).expect("mount");
726 if !fs.csum.enabled {
727 eprintln!("skip: metadata_csum not enabled");
728 return;
729 }
730 // Walk first ~50 inodes looking for one with depth>0.
731 let bs = fs.sb.block_size();
732 let mut found = None;
733 for ino in 11..200u32 {
734 let (inode, _raw) = match fs.read_inode_verified(ino) {
735 Ok(x) => x,
736 Err(_) => continue,
737 };
738 if !inode.is_file() || !inode.has_extents() {
739 continue;
740 }
741 let header = match ExtentHeader::parse(&inode.block) {
742 Ok(h) => h,
743 Err(_) => continue,
744 };
745 if header.depth > 0 && header.entries >= 1 {
746 let idx =
747 ExtentIdx::parse(&inode.block[EXT4_EXT_NODE_SIZE..2 * EXT4_EXT_NODE_SIZE])
748 .expect("parse first idx");
749 found = Some((ino, inode.generation, idx.leaf_block));
750 break;
751 }
752 }
753 let (ino, gen, child_block) = match found {
754 Some(x) => x,
755 None => {
756 eprintln!("skip: no depth>0 inode in first 200 of {path}");
757 return;
758 }
759 };
760 let mut buf = vec![0u8; bs as usize];
761 dev_dyn.read_at(child_block * bs as u64, &mut buf).unwrap();
762 assert!(
763 fs.csum.verify_extent_tail(ino, gen, &buf),
764 "extent block csum mismatch (ino={ino} child_block={child_block} on {path})"
765 );
766
767 // Also exercise the verified traversal API end-to-end.
768 let (inode, _) = fs.read_inode_verified(ino).unwrap();
769 let ctx = extent::ExtentVerifyCtx {
770 ino,
771 generation: gen,
772 csum: &fs.csum,
773 };
774 let _ = extent::lookup_verified(&inode.block, dev_dyn.as_ref(), bs, 0, Some(&ctx))
775 .expect("lookup_verified must accept valid extent blocks");
776 }
777}