Skip to main content

ext4fs/forensic/
superblock_verify.rs

1#![forbid(unsafe_code)]
2
3use crate::error::Result;
4use crate::inode::InodeReader;
5use crate::ondisk::superblock::Superblock;
6use std::io::{Read, Seek};
7
8/// Result of comparing a backup superblock against the primary.
9#[derive(Debug, Clone)]
10pub struct SuperblockComparison {
11    /// Block group number containing the backup.
12    pub group: u32,
13    /// Block number of the backup superblock.
14    pub block: u64,
15    /// Whether it matches the primary superblock on key fields.
16    pub matches_primary: bool,
17    /// List of field names that differ from primary.
18    pub differences: Vec<String>,
19}
20
21/// Compute which block groups contain superblock backups.
22///
23/// ext4 stores backups at group 0 (primary), group 1, and groups that are
24/// powers of 3, 5, or 7 (e.g., 3, 5, 7, 9, 25, 27, 49, 125, ...).
25fn backup_groups(group_count: u32) -> Vec<u32> {
26    let mut groups = vec![0, 1]; // primary + first backup
27    for base in [3u32, 5, 7] {
28        let mut power = base;
29        while power < group_count {
30            if !groups.contains(&power) {
31                groups.push(power);
32            }
33            power = match power.checked_mul(base) {
34                Some(p) => p,
35                None => break,
36            };
37        }
38    }
39    groups.sort_unstable();
40    groups.retain(|&g| g < group_count);
41    groups
42}
43
44/// Compare a backup superblock against the primary on key fields.
45fn compare_superblocks(primary: &Superblock, backup: &Superblock) -> Vec<String> {
46    let mut diffs = Vec::new();
47
48    if primary.magic != backup.magic {
49        diffs.push("magic".to_string());
50    }
51    if primary.block_size != backup.block_size {
52        diffs.push("block_size".to_string());
53    }
54    if primary.blocks_count != backup.blocks_count {
55        diffs.push("blocks_count".to_string());
56    }
57    if primary.inodes_count != backup.inodes_count {
58        diffs.push("inodes_count".to_string());
59    }
60    if primary.blocks_per_group != backup.blocks_per_group {
61        diffs.push("blocks_per_group".to_string());
62    }
63    if primary.inodes_per_group != backup.inodes_per_group {
64        diffs.push("inodes_per_group".to_string());
65    }
66    if primary.uuid != backup.uuid {
67        diffs.push("uuid".to_string());
68    }
69    if primary.inode_size != backup.inode_size {
70        diffs.push("inode_size".to_string());
71    }
72    if primary.feature_compat != backup.feature_compat {
73        diffs.push("feature_compat".to_string());
74    }
75    if primary.feature_incompat != backup.feature_incompat {
76        diffs.push("feature_incompat".to_string());
77    }
78    if primary.feature_ro_compat != backup.feature_ro_compat {
79        diffs.push("feature_ro_compat".to_string());
80    }
81
82    diffs
83}
84
85/// Verify all superblock backups against the primary.
86pub fn verify_superblock_backups<R: Read + Seek>(
87    reader: &mut InodeReader<R>,
88) -> Result<Vec<SuperblockComparison>> {
89    let primary = reader.block_reader().superblock().clone();
90    let group_count = reader.block_reader().group_count();
91    let block_size = u64::from(primary.block_size);
92    let blocks_per_group = u64::from(primary.blocks_per_group);
93
94    let groups = backup_groups(group_count);
95    let mut results = Vec::new();
96
97    for &group in &groups {
98        if group == 0 {
99            continue;
100        } // skip primary
101
102        let block = u64::from(group) * blocks_per_group;
103
104        // Superblock is at byte offset 1024 within its block group,
105        // but for block_size >= 2048, it's at the start of the first block.
106        // For block_size == 1024, it's at block offset 1.
107        let sb_byte_offset = if block_size >= 2048 {
108            block * block_size
109        } else {
110            block * block_size + 1024
111        };
112
113        // Read 1024 bytes for superblock
114        let buf = match reader.block_reader_mut().read_bytes(sb_byte_offset, 1024) {
115            Ok(b) => b,
116            Err(_) => continue,
117        };
118
119        let backup_sb = if let Ok(sb) = Superblock::parse(&buf) {
120            sb
121        } else {
122            results.push(SuperblockComparison {
123                group,
124                block,
125                matches_primary: false,
126                differences: vec!["unparseable".to_string()],
127            });
128            continue;
129        };
130
131        let diffs = compare_superblocks(&primary, &backup_sb);
132        results.push(SuperblockComparison {
133            group,
134            block,
135            matches_primary: diffs.is_empty(),
136            differences: diffs,
137        });
138    }
139
140    Ok(results)
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146    use crate::block::BlockReader;
147    use std::io::Cursor;
148
149    fn open_forensic() -> Option<InodeReader<Cursor<Vec<u8>>>> {
150        let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../tests/data/forensic.img");
151        let data = std::fs::read(path).ok()?;
152        let br = BlockReader::open(Cursor::new(data)).ok()?;
153        Some(InodeReader::new(br))
154    }
155
156    #[test]
157    fn backup_groups_small_image() {
158        // 1 group — only group 0 (primary)
159        let groups = backup_groups(1);
160        assert_eq!(groups, vec![0]);
161    }
162
163    #[test]
164    fn backup_groups_medium_image() {
165        // 10 groups — should include 0, 1, 3, 5, 7, 9
166        let groups = backup_groups(10);
167        assert!(groups.contains(&0));
168        assert!(groups.contains(&1));
169        assert!(groups.contains(&3));
170        assert!(groups.contains(&5));
171        assert!(groups.contains(&7));
172        assert!(groups.contains(&9)); // 3^2
173    }
174
175    #[test]
176    fn backup_groups_large_image() {
177        let groups = backup_groups(200);
178        assert!(groups.contains(&0));
179        assert!(groups.contains(&1));
180        assert!(groups.contains(&25)); // 5^2
181        assert!(groups.contains(&27)); // 3^3
182        assert!(groups.contains(&49)); // 7^2
183        assert!(groups.contains(&125)); // 5^3
184        assert!(!groups.contains(&200)); // beyond count
185    }
186
187    #[test]
188    fn verify_backups_on_forensic_img() {
189        let mut reader = if let Some(r) = open_forensic() {
190            r
191        } else {
192            eprintln!("skip");
193            return;
194        };
195        let results = verify_superblock_backups(&mut reader).unwrap();
196        // forensic.img is 32MB / 4096 = 8192 blocks / 32768 bpg = 1 group
197        // So no backups to verify (only primary at group 0)
198        // The function should return empty or handle gracefully
199        eprintln!("Backup verification results: {} entries", results.len());
200        for r in &results {
201            eprintln!(
202                "  group {}: matches={}, diffs={:?}",
203                r.group, r.matches_primary, r.differences
204            );
205        }
206    }
207
208    #[test]
209    fn compare_identical_superblocks() {
210        let mut buf = vec![0u8; 1024];
211        buf[0x38..0x3A].copy_from_slice(&0xEF53u16.to_le_bytes());
212        buf[0x18..0x1C].copy_from_slice(&2u32.to_le_bytes()); // log_block_size
213        buf[0x28..0x2C].copy_from_slice(&64u32.to_le_bytes()); // inodes_per_group
214        buf[0x04..0x08].copy_from_slice(&100u32.to_le_bytes()); // blocks_count
215        buf[0x58..0x5A].copy_from_slice(&256u16.to_le_bytes()); // inode_size
216        buf[0x4C..0x50].copy_from_slice(&1u32.to_le_bytes()); // rev_level
217        let sb = Superblock::parse(&buf).unwrap();
218        let diffs = compare_superblocks(&sb, &sb);
219        assert!(
220            diffs.is_empty(),
221            "identical superblocks should have no differences"
222        );
223    }
224}