use crate::features::{Incompat, RoCompat};
use crate::superblock::Superblock;
#[inline]
pub fn linux_crc32c(seed: u32, data: &[u8]) -> u32 {
!crc32c::crc32c_append(!seed, data)
}
pub fn crc16(mut crc: u16, data: &[u8]) -> u16 {
for &b in data {
crc ^= u16::from(b);
for _ in 0..8 {
crc = if crc & 1 != 0 {
(crc >> 1) ^ 0xA001
} else {
crc >> 1
};
}
}
crc
}
pub fn group_desc_csum(
sb: &Superblock,
csum: &Checksummer,
group: u32,
desc: &[u8],
) -> Option<u16> {
let n = sb.desc_size as usize;
if desc.len() < n || n < 0x20 {
return None;
}
let le_group = group.to_le_bytes();
if sb.feature_ro_compat & RoCompat::METADATA_CSUM.bits() != 0 {
let mut c = linux_crc32c(csum.seed, &le_group);
c = linux_crc32c(c, &desc[..0x1E]);
c = linux_crc32c(c, &[0, 0]);
c = linux_crc32c(c, &desc[0x20..n]);
return Some(c as u16);
}
if sb.feature_ro_compat & RoCompat::GDT_CSUM.bits() == 0 {
return None;
}
let mut c = crc16(!0, &sb.uuid);
c = crc16(c, &le_group);
c = crc16(c, &desc[..0x1E]);
if sb.feature_incompat & Incompat::BIT64.bits() != 0 {
c = crc16(c, &desc[0x20..n]);
}
Some(c)
}
#[derive(Debug, Clone, Copy)]
pub struct Checksummer {
pub seed: u32,
pub enabled: bool,
}
pub const DIR_ENTRY_TAIL_FILE_TYPE: u8 = 0xDE;
impl Checksummer {
pub fn from_superblock(sb: &Superblock) -> Self {
let enabled = (sb.feature_ro_compat & RoCompat::METADATA_CSUM.bits()) != 0;
let seed = if (sb.feature_incompat & Incompat::CSUM_SEED.bits()) != 0 {
sb.checksum_seed
} else {
linux_crc32c(!0, &sb.uuid)
};
Self { seed, enabled }
}
pub fn crc(&self, data: &[u8]) -> u32 {
linux_crc32c(self.seed, data)
}
pub fn crc_with_prefix(&self, prefix: u32, data: &[u8]) -> u32 {
let mid = linux_crc32c(self.seed, &prefix.to_le_bytes());
linux_crc32c(mid, data)
}
pub fn verify_superblock(&self, sb_raw: &[u8]) -> bool {
if !self.enabled {
return true;
}
if sb_raw.len() < 1024 {
return false;
}
let stored = u32::from_le_bytes(sb_raw[0x3FC..0x400].try_into().unwrap());
let computed = linux_crc32c(!0, &sb_raw[..0x3FC]);
stored == computed
}
pub fn verify_bgd(&self, group_no: u32, bgd_raw: &[u8], desc_size: u16) -> bool {
if !self.enabled {
return true;
}
let n = desc_size as usize;
if bgd_raw.len() < n || n < 0x20 {
return false;
}
let stored = u16::from_le_bytes(bgd_raw[0x1E..0x20].try_into().unwrap());
let mut tmp = bgd_raw[..n].to_vec();
tmp[0x1E] = 0;
tmp[0x1F] = 0;
let computed16 = self.crc_with_prefix(group_no, &tmp) as u16;
computed16 == stored
}
pub fn verify_dir_entry_tail(&self, ino: u32, generation: u32, block: &[u8]) -> bool {
if !self.enabled {
return true;
}
if block.len() < 12 {
return false;
}
let end = block.len();
let stored = u32::from_le_bytes(block[end - 4..end].try_into().unwrap());
let mut c = linux_crc32c(self.seed, &ino.to_le_bytes());
c = linux_crc32c(c, &generation.to_le_bytes());
c = linux_crc32c(c, &block[..end - 12]);
c == stored
}
pub fn patch_dir_entry_tail(&self, ino: u32, generation: u32, block: &mut [u8]) -> bool {
if !self.enabled || block.len() < 12 {
return false;
}
let end = block.len();
block[end - 12..end - 8].copy_from_slice(&0u32.to_le_bytes()); block[end - 8..end - 6].copy_from_slice(&12u16.to_le_bytes()); block[end - 6] = 0; block[end - 5] = DIR_ENTRY_TAIL_FILE_TYPE;
let mut c = linux_crc32c(self.seed, &ino.to_le_bytes());
c = linux_crc32c(c, &generation.to_le_bytes());
c = linux_crc32c(c, &block[..end - 12]);
block[end - 4..end].copy_from_slice(&c.to_le_bytes());
true
}
pub fn verify_dx_tail(
&self,
ino: u32,
generation: u32,
block: &[u8],
count_offset: usize,
) -> Option<bool> {
let mut copy = block.to_vec();
if !self.patch_dx_tail(ino, generation, &mut copy, count_offset) {
return None;
}
Some(copy == block)
}
pub fn patch_dx_tail(
&self,
ino: u32,
generation: u32,
block: &mut [u8],
count_offset: usize,
) -> bool {
if !self.enabled || block.len() < count_offset + 4 {
return false;
}
let limit = u16::from_le_bytes([block[count_offset], block[count_offset + 1]]) as usize;
let count = u16::from_le_bytes([block[count_offset + 2], block[count_offset + 3]]) as usize;
let tail = count_offset + limit * 8;
if count > limit || tail + 8 > block.len() {
return false;
}
let mut c = linux_crc32c(self.seed, &ino.to_le_bytes());
c = linux_crc32c(c, &generation.to_le_bytes());
c = linux_crc32c(c, &block[..count_offset + count * 8]);
c = linux_crc32c(c, &block[tail..tail + 4]);
c = linux_crc32c(c, &[0; 4]);
block[tail + 4..tail + 8].copy_from_slice(&c.to_le_bytes());
true
}
pub fn verify_extent_tail(&self, ino: u32, generation: u32, block: &[u8]) -> bool {
if !self.enabled {
return true;
}
if block.len() < 4 {
return false;
}
let end = block.len();
let stored = u32::from_le_bytes(block[end - 4..end].try_into().unwrap());
let mut c = linux_crc32c(self.seed, &ino.to_le_bytes());
c = linux_crc32c(c, &generation.to_le_bytes());
c = linux_crc32c(c, &block[..end - 4]);
c == stored
}
pub fn verify_inode(&self, ino: u32, generation: u32, inode_raw: &[u8]) -> bool {
if !self.enabled {
return true;
}
if inode_raw.len() < crate::inode::GOOD_OLD_INODE_SIZE {
return false;
}
let stored_lo = u16::from_le_bytes(inode_raw[0x7C..0x7E].try_into().unwrap()) as u32;
let stored_hi = if checksum_hi_fits(inode_raw) {
u16::from_le_bytes(inode_raw[0x82..0x84].try_into().unwrap()) as u32
} else {
0
};
let stored = (stored_hi << 16) | stored_lo;
match self.compute_inode_checksum(ino, generation, inode_raw) {
Some((lo, hi)) => ((hi as u32) << 16 | lo as u32) == stored,
None => true, }
}
pub fn patch_extent_tail(&self, ino: u32, generation: u32, block: &mut [u8]) -> bool {
if !self.enabled || block.len() < 4 {
return false;
}
let end = block.len();
let mut c = linux_crc32c(self.seed, &ino.to_le_bytes());
c = linux_crc32c(c, &generation.to_le_bytes());
c = linux_crc32c(c, &block[..end - 4]);
block[end - 4..end].copy_from_slice(&c.to_le_bytes());
true
}
pub fn verify_xattr_block(&self, block_nr: u64, block: &[u8]) -> bool {
if !self.enabled {
return true;
}
if block.len() < 0x20 {
return false;
}
let stored = u32::from_le_bytes(block[0x10..0x14].try_into().unwrap());
let computed = self.compute_xattr_block_csum(block_nr, block);
stored == computed
}
pub fn patch_xattr_block(&self, block_nr: u64, block: &mut [u8]) -> bool {
if !self.enabled || block.len() < 0x20 {
return false;
}
let csum = self.compute_xattr_block_csum(block_nr, block);
block[0x10..0x14].copy_from_slice(&csum.to_le_bytes());
true
}
fn compute_xattr_block_csum(&self, block_nr: u64, block: &[u8]) -> u32 {
let mut c = linux_crc32c(self.seed, &block_nr.to_le_bytes());
c = linux_crc32c(c, &block[0x00..0x10]);
c = linux_crc32c(c, &0u32.to_le_bytes());
c = linux_crc32c(c, &block[0x14..]);
c
}
pub fn compute_inode_checksum(
&self,
ino: u32,
generation: u32,
inode_raw: &[u8],
) -> Option<(u16, u16)> {
if !self.enabled || inode_raw.len() < crate::inode::GOOD_OLD_INODE_SIZE {
return None;
}
let fits_hi = checksum_hi_fits(inode_raw);
let mut tmp = inode_raw.to_vec();
tmp[0x7C] = 0;
tmp[0x7D] = 0;
if fits_hi {
tmp[0x82] = 0;
tmp[0x83] = 0;
}
let mut c = linux_crc32c(self.seed, &ino.to_le_bytes());
c = linux_crc32c(c, &generation.to_le_bytes());
c = linux_crc32c(c, &tmp);
let lo = (c & 0xFFFF) as u16;
let hi = if fits_hi {
((c >> 16) & 0xFFFF) as u16
} else {
0
};
Some((lo, hi))
}
pub(crate) fn patch_inode_checksum(&self, ino: u32, generation: u32, inode_raw: &mut [u8]) {
if let Some((lo, hi)) = self.compute_inode_checksum(ino, generation, inode_raw) {
inode_raw[0x7C..0x7E].copy_from_slice(&lo.to_le_bytes());
if checksum_hi_fits(inode_raw) {
inode_raw[0x82..0x84].copy_from_slice(&hi.to_le_bytes());
}
}
}
}
fn checksum_hi_fits(inode_raw: &[u8]) -> bool {
inode_raw.len() >= 0x84 && u16::from_le_bytes(inode_raw[0x80..0x82].try_into().unwrap()) >= 4
}
#[cfg(test)]
mod tests {
fn enabled() -> Checksummer {
Checksummer {
enabled: true,
seed: 0xDEAD_BEEF,
}
}
#[test]
fn every_verifier_refuses_a_buffer_too_short_to_check() {
let c = enabled();
assert!(
!c.verify_superblock(&[0u8; 64]),
"superblock: a 64-byte buffer cannot hold a checksum at 0x3FC"
);
assert!(
!c.verify_dir_entry_tail(2, 0, &[0u8; 8]),
"dir entry tail: an 8-byte buffer cannot hold a 12-byte tail"
);
assert!(
!c.verify_extent_tail(2, 0, &[0u8; 2]),
"extent tail: a 2-byte buffer cannot hold a 4-byte checksum"
);
assert!(
!c.verify_inode(2, 0, &[0u8; 64]),
"inode: a 64-byte buffer cannot hold a checksum at 0x7C"
);
}
#[test]
fn a_disabled_checksummer_passes_even_a_short_buffer() {
let c = Checksummer {
enabled: false,
seed: 0,
};
assert!(c.verify_superblock(&[0u8; 4]));
assert!(c.verify_dir_entry_tail(2, 0, &[0u8; 4]));
assert!(c.verify_extent_tail(2, 0, &[0u8; 1]));
assert!(c.verify_inode(2, 0, &[0u8; 4]));
}
#[test]
fn the_inode_length_boundary_is_the_whole_fixed_inode() {
let c = enabled();
assert!(!c.verify_inode(2, 0, &[0u8; 127]), "127 is too short");
assert!(
!c.verify_inode(2, 0, &[0u8; 128]),
"128 is checked, and all-zero bytes do not verify"
);
let mut raw: Vec<u8> = (0..128u32).map(|i| (i * 7 + 3) as u8).collect();
let (lo, _) = c.compute_inode_checksum(2, 5, &raw).unwrap();
raw[0x7C..0x7E].copy_from_slice(&lo.to_le_bytes());
assert!(c.verify_inode(2, 5, &raw), "a correct checksum verifies");
let mut past_the_field = raw.clone();
past_the_field[0x7F] ^= 0x01;
assert!(
!c.verify_inode(2, 5, &past_the_field),
"a byte after i_checksum_lo is part of the checksum"
);
let mut holds_the_field = raw[..0x7E].to_vec();
holds_the_field[0x7C..0x7E].fill(0);
let mut crc = linux_crc32c(c.seed, &2u32.to_le_bytes());
crc = linux_crc32c(crc, &5u32.to_le_bytes());
crc = linux_crc32c(crc, &holds_the_field);
holds_the_field[0x7C..0x7E].copy_from_slice(&(crc as u16).to_le_bytes());
assert!(
!c.verify_inode(2, 5, &holds_the_field),
"126 bytes hold i_checksum_lo and are still too short"
);
}
use super::*;
#[test]
fn disabled_when_feature_off() {
let c = Checksummer {
seed: 0,
enabled: false,
};
assert!(c.verify_superblock(&[]));
assert!(c.verify_inode(2, 0, &[]));
assert!(c.verify_dir_entry_tail(2, 0, &[0u8; 12]));
assert!(c.verify_extent_tail(2, 0, &[0u8; 64]));
}
#[test]
fn dir_tail_roundtrip_and_tamper() {
let c = Checksummer {
seed: 0xCAFEBABE,
enabled: true,
};
let ino = 42u32;
let gen = 0xDEADBEEFu32;
let mut block = vec![0u8; 4096];
let end = block.len();
block[end - 12..end - 8].copy_from_slice(&0u32.to_le_bytes()); block[end - 8..end - 6].copy_from_slice(&12u16.to_le_bytes()); block[end - 6] = 0; block[end - 5] = 0xDE; block[0..8].copy_from_slice(&[2, 0, 0, 0, 12, 0, 1, 2]);
block[100] = 0x5A;
let mut expected = linux_crc32c(c.seed, &ino.to_le_bytes());
expected = linux_crc32c(expected, &gen.to_le_bytes());
expected = linux_crc32c(expected, &block[..end - 12]);
block[end - 4..end].copy_from_slice(&expected.to_le_bytes());
assert!(c.verify_dir_entry_tail(ino, gen, &block));
block[100] ^= 0xFF;
assert!(!c.verify_dir_entry_tail(ino, gen, &block));
}
#[test]
fn extent_tail_excludes_only_last_4_bytes() {
let c = Checksummer {
seed: 0x12345678,
enabled: true,
};
let mut block = vec![0u8; 1024];
block[0..12].copy_from_slice(&[0x0A, 0xF3, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0]);
block[500] = 0xAB;
let end = block.len();
let mut expected = linux_crc32c(c.seed, &7u32.to_le_bytes());
expected = linux_crc32c(expected, &9u32.to_le_bytes());
expected = linux_crc32c(expected, &block[..end - 4]);
block[end - 4..end].copy_from_slice(&expected.to_le_bytes());
assert!(c.verify_extent_tail(7, 9, &block));
}
#[test]
fn patch_extent_tail_is_verify_inverse() {
let c = Checksummer {
seed: 0xFEEDFACE,
enabled: true,
};
let mut block = vec![0u8; 4096];
block[0..12].copy_from_slice(&[0x0A, 0xF3, 1, 0, 0x54, 0x01, 0, 0, 1, 2, 3, 4]);
block[12..24].copy_from_slice(&[0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0xE8, 0x03]);
let patched = c.patch_extent_tail(42, 0xABCDEF01, &mut block);
assert!(patched);
assert!(c.verify_extent_tail(42, 0xABCDEF01, &block));
block[200] ^= 0xFF;
assert!(!c.verify_extent_tail(42, 0xABCDEF01, &block));
}
#[test]
fn patch_extent_tail_disabled_is_noop() {
let c = Checksummer {
seed: 0,
enabled: false,
};
let mut block = vec![0u8; 64];
assert!(!c.patch_extent_tail(1, 1, &mut block));
assert_eq!(&block[..], &[0u8; 64]);
}
#[test]
fn dir_tail_rejects_too_short_block_when_enabled() {
let c = Checksummer {
seed: 0,
enabled: true,
};
assert!(!c.verify_dir_entry_tail(2, 0, &[0u8; 8]));
}
#[test]
fn crc_helpers_are_deterministic() {
let c = Checksummer {
seed: 0xDEAD_BEEF,
enabled: true,
};
let a = c.crc(b"hello");
let b = c.crc(b"hello");
assert_eq!(a, b);
let p1 = c.crc_with_prefix(1, b"hello");
let p2 = c.crc_with_prefix(2, b"hello");
assert_ne!(p1, p2, "prefix changes hash");
}
mod needs_host {
use super::*;
#[test]
fn verifies_real_superblock() {
use crate::block_io::FileDevice;
let path = fs_ext4_test_support::fixture(env!("CARGO_MANIFEST_DIR"), "ext4-basic.img");
let dev = FileDevice::open(&path).unwrap_or_else(|e| panic!("open {path}: {e}"));
let sb = Superblock::read(&dev).expect("parse sb");
let csum = Checksummer::from_superblock(&sb);
assert!(
csum.enabled,
"ext4-basic.img is built with metadata_csum (test-disks/guest-build-images.sh)"
);
assert!(
csum.verify_superblock(&sb.raw),
"superblock checksum mismatch on {path}"
);
}
#[test]
fn verifies_real_bgd() {
use crate::block_io::{BlockDevice, FileDevice};
let path = fs_ext4_test_support::fixture(env!("CARGO_MANIFEST_DIR"), "ext4-basic.img");
let dev = FileDevice::open(&path).unwrap_or_else(|e| panic!("open {path}: {e}"));
let sb = Superblock::read(&dev).expect("parse sb");
let csum = Checksummer::from_superblock(&sb);
assert!(
csum.enabled,
"{path} is built with metadata_csum (test-disks/guest-build-images.sh)"
);
let block_size = sb.block_size() as u64;
let bgt_off = (sb.first_data_block as u64 + 1) * block_size;
let group_count = sb.block_group_count();
let total = group_count as usize * sb.desc_size as usize;
let mut buf = vec![0u8; total];
dev.read_at(bgt_off, &mut buf).expect("read bgt");
for i in 0..group_count as usize {
let off = i * sb.desc_size as usize;
let raw = &buf[off..off + sb.desc_size as usize];
assert!(
csum.verify_bgd(i as u32, raw, sb.desc_size),
"BGD {i} checksum mismatch on {path}"
);
}
}
#[test]
fn verifies_real_inode() {
use crate::block_io::FileDevice;
use crate::fs::Filesystem;
use std::sync::Arc;
let path = fs_ext4_test_support::fixture(env!("CARGO_MANIFEST_DIR"), "ext4-basic.img");
let dev = FileDevice::open(&path).unwrap_or_else(|e| panic!("open {path}: {e}"));
let dev_dyn: Arc<dyn crate::block_io::BlockDevice> = Arc::new(dev);
let fs = Filesystem::mount(dev_dyn).expect("mount");
assert!(
fs.csum.enabled,
"{path} is built with metadata_csum (test-disks/guest-build-images.sh)"
);
let (inode, raw) = fs.read_inode_verified(2).expect("read root inode");
assert!(inode.is_dir());
assert!(fs.csum.verify_inode(2, inode.generation, &raw));
}
#[test]
fn verifies_real_dir_tail() {
use crate::block_io::{BlockDevice, FileDevice};
use crate::dir;
use crate::extent;
use crate::fs::Filesystem;
use std::sync::Arc;
let path = fs_ext4_test_support::fixture(env!("CARGO_MANIFEST_DIR"), "ext4-basic.img");
let dev = FileDevice::open(&path).unwrap_or_else(|e| panic!("open {path}: {e}"));
let dev_dyn: Arc<dyn BlockDevice> = Arc::new(dev);
let fs = Filesystem::mount(dev_dyn.clone()).expect("mount");
assert!(
fs.csum.enabled,
"{path} is built with metadata_csum (test-disks/guest-build-images.sh)"
);
let (root_inode, _raw) = fs.read_inode_verified(2).expect("root inode");
let bs = fs.sb.block_size();
let phys = extent::map_logical(&root_inode.block, dev_dyn.as_ref(), bs, 0)
.expect("map_logical")
.expect("dir block 0 mapped");
let mut block = vec![0u8; bs as usize];
dev_dyn.read_at(phys * bs as u64, &mut block).unwrap();
assert!(
dir::has_csum_tail(&block),
"expected tail on root dir block"
);
assert!(
fs.csum
.verify_dir_entry_tail(2, root_inode.generation, &block),
"dir tail csum mismatch on {path} root dir"
);
}
#[test]
fn verifies_real_extent_tail() {
use crate::block_io::{BlockDevice, FileDevice};
use crate::extent::{self, ExtentHeader, ExtentIdx, EXT4_EXT_NODE_SIZE};
use crate::fs::Filesystem;
use std::sync::Arc;
let path =
fs_ext4_test_support::fixture(env!("CARGO_MANIFEST_DIR"), "ext4-deep-extents.img");
let dev = FileDevice::open(&path).unwrap_or_else(|e| panic!("open {path}: {e}"));
let dev_dyn: Arc<dyn BlockDevice> = Arc::new(dev);
let fs = Filesystem::mount(dev_dyn.clone()).expect("mount");
assert!(
fs.csum.enabled,
"{path} is built with metadata_csum (test-disks/guest-build-images.sh)"
);
let bs = fs.sb.block_size();
let mut found = None;
for ino in 11..200u32 {
let (inode, _raw) = match fs.read_inode_verified(ino) {
Ok(x) => x,
Err(_) => continue,
};
if !inode.is_file() || !inode.has_extents() {
continue;
}
let header = match ExtentHeader::parse(&inode.block) {
Ok(h) => h,
Err(_) => continue,
};
if header.depth > 0 && header.entries >= 1 {
let idx =
ExtentIdx::parse(&inode.block[EXT4_EXT_NODE_SIZE..2 * EXT4_EXT_NODE_SIZE])
.expect("parse first idx");
found = Some((ino, inode.generation, idx.leaf_block));
break;
}
}
let (ino, gen, child_block) = found.unwrap_or_else(|| {
panic!(
"no depth>0 inode in the first 200 of {path}: its sparse.bin is built \
with a multi-level extent tree (test-disks/guest-build-images.sh)"
)
});
let mut buf = vec![0u8; bs as usize];
dev_dyn.read_at(child_block * bs as u64, &mut buf).unwrap();
assert!(
fs.csum.verify_extent_tail(ino, gen, &buf),
"extent block csum mismatch (ino={ino} child_block={child_block} on {path})"
);
let (inode, _) = fs.read_inode_verified(ino).unwrap();
let ctx = extent::ExtentVerifyCtx {
ino,
generation: gen,
csum: &fs.csum,
};
let _ = extent::lookup_verified(&inode.block, dev_dyn.as_ref(), bs, 0, Some(&ctx))
.expect("lookup_verified must accept valid extent blocks");
}
}
}