Files inside extended SquashFS directories are recovered (R0.5) (#434)

Extended SquashFS directory inodes read their listing offset from the right field, so files inside them are recovered. Fixes #338.
This commit is contained in:
Afloat
2026-11-02 11:37:59 -05:00
committed by GitHub
parent 6e9fc1a3d2
commit d8314238ae
5 changed files with 71 additions and 27 deletions
+1 -1
View File
@@ -142,7 +142,7 @@ fn walk(data: &[u8], sb: &Sb, inodes: &Meta, dirs: &Meta, inode_ref: u64, parent
// Directory inode (basic=1, extended=8).
let (start_block, size, offset) = match itype {
1 => (r.u32_le(pos + 16), r.u16_le(pos + 24).map(u32::from), r.u16_le(pos + 26)),
8 => (r.u32_le(pos + 24), r.u32_le(pos + 20), r.u16_le(pos + 28)),
8 => (r.u32_le(pos + 24), r.u32_le(pos + 20), r.u16_le(pos + 34)),
_ => return,
};
let (Some(start_block), Some(size), Some(offset)) = (start_block, size, offset) else { return };
@@ -0,0 +1,28 @@
use craft_anything_formats::fsread::{self, Limits};
use craft_anything_testkit::firmware::{squashfs_image, squashfs_image_with_extended_directories};
fn check_directories(root: bool, nested: bool) {
let limits = Limits::default();
let expected = fsread::read("squashfs", &squashfs_image(), &limits).unwrap();
assert!(expected.complete);
assert_eq!(expected.entries.len(), 3);
let bytes = squashfs_image_with_extended_directories(root, nested);
let actual = fsread::read("squashfs", &bytes, &limits).unwrap();
// Compare the names, kinds, contents and completeness, not only the number of entries.
assert_eq!(actual, expected);
}
#[test]
fn reads_an_extended_root_directory() {
check_directories(true, false);
}
#[test]
fn reads_an_extended_nested_directory_at_a_nonzero_offset() {
check_directories(false, true);
}
#[test]
fn reads_extended_root_and_nested_directories_together() {
check_directories(true, true);
}
+34 -26
View File
@@ -436,6 +436,12 @@ pub const SQUASHFS_README: &[u8] = b"This is the squashfs readme.\n";
/// A small, walkable SquashFS 4.0 image with uncompressed metadata and data blocks (the reader
/// also handles gzip/xz/zstd): root holds `readme` and `etc/` (with `etc/motd`).
pub fn squashfs_image() -> Vec<u8> {
squashfs_image_with_extended_directories(false, false)
}
/// The same tree with independently selectable basic or extended root and nested directory inodes.
/// The nested directory's listing starts at a nonzero offset, distinct from its parent inode number.
pub fn squashfs_image_with_extended_directories(extended_root: bool, extended_nested: bool) -> Vec<u8> {
fn u16(v: &mut Vec<u8>, x: u16) {
v.extend_from_slice(&x.to_le_bytes());
}
@@ -449,7 +455,7 @@ pub fn squashfs_image() -> Vec<u8> {
let motd_start = 96 + r as u32;
let inode_table = 96 + r as u64 + motd.len() as u64;
// Inode metadata block (decompressed). Order: readme(0), motd(36), etc(72), root(104).
// Inode metadata block (decompressed). Order: readme(0), motd(36), etc(72), then root.
let mut inodes = Vec::new();
// readme: basic file (type 2)
u16(&mut inodes, 2);
@@ -475,30 +481,32 @@ pub fn squashfs_image() -> Vec<u8> {
u32(&mut inodes, 0);
u32(&mut inodes, motd.len() as u32);
u32(&mut inodes, (motd.len() as u32) | 0x0100_0000);
// etc: basic directory (type 1)
u16(&mut inodes, 1);
u16(&mut inodes, 0x41ed);
u16(&mut inodes, 0);
u16(&mut inodes, 0);
u32(&mut inodes, 0);
u32(&mut inodes, 2);
u32(&mut inodes, 0); // start_block (dir table block location)
u32(&mut inodes, 2); // nlink
u16(&mut inodes, 27); // file_size (listing + 3)
u16(&mut inodes, 37); // offset into dir block
u32(&mut inodes, 1); // parent inode
// root: basic directory
u16(&mut inodes, 1);
u16(&mut inodes, 0x41ed);
u16(&mut inodes, 0);
u16(&mut inodes, 0);
u32(&mut inodes, 0);
u32(&mut inodes, 1);
u32(&mut inodes, 0);
u32(&mut inodes, 3);
u16(&mut inodes, 40);
u16(&mut inodes, 0);
u32(&mut inodes, 1);
fn directory(out: &mut Vec<u8>, extended: bool, number: u32, links: u32, size: u16, offset: u16) {
u16(out, if extended { 8 } else { 1 });
u16(out, 0x41ed);
u16(out, 0);
u16(out, 0);
u32(out, 0); // mtime
u32(out, number);
if extended {
u32(out, links);
u32(out, u32::from(size));
u32(out, 0); // start_block
u32(out, 1); // parent inode
u16(out, 0); // no directory indexes
u16(out, offset);
u32(out, u32::MAX); // no xattrs
} else {
u32(out, 0); // start_block
u32(out, links);
u16(out, size);
u16(out, offset);
u32(out, 1); // parent inode
}
}
directory(&mut inodes, extended_nested, 2, 2, 27, 37);
let root_ref = inodes.len() as u64;
directory(&mut inodes, extended_root, 1, 3, 40, 0);
// Directory metadata block (decompressed): root listing @0, etc listing @37.
let mut dirs = Vec::new();
@@ -543,7 +551,7 @@ pub fn squashfs_image() -> Vec<u8> {
u16(&mut v, 1); // id count
u16(&mut v, 4); // version major
u16(&mut v, 0); // version minor
v.extend_from_slice(&104u64.to_le_bytes()); // root inode ref (block 0, offset 104)
v.extend_from_slice(&root_ref.to_le_bytes()); // root inode ref (block 0)
v.extend_from_slice(&bytes_used.to_le_bytes());
v.extend_from_slice(&id_table.to_le_bytes());
v.extend_from_slice(&u64::MAX.to_le_bytes()); // xattr
+1
View File
@@ -171,6 +171,7 @@ Owner: open · Status: done (branch chore/r0.5-cleanups)
- [x] rustfmt drift on main (11 files)
- [x] Checked arithmetic on file offsets in `fsread/extfs.rs`: an inode offset near `usize::MAX`
overflows `o + 128` (two sites), found by a lint scan of `crates/formats` (#42)
- [x] Read extended SquashFS directory offsets correctly (#338). — claimed: Afloat16, branch bugfix/r0.5-squashfs-extended-directory-20261102
- [x] `macho.rs` `method_list`: `list + 8 + i * entsize` adds to a pointer read from the file, so a
segment that ends at `u64::MAX` overflowed it (found by a lint scan; now reproduced by a
regression test, and the entry addresses are checked)
+7
View File
@@ -60,6 +60,13 @@ checked with Rizin 0.9.1 on Windows) and radio decoding with rtl_433 (`radio_rec
shares with `=h& 9090` (`native_cutter_sessions_list`, `native_cutter_command_run`; tested
against a stand-in server, not yet a real Cutter). Every tool is listed in `docs/tools.md`.
### SquashFS extended directories
The filesystem reader recovers the same files and bytes from basic and extended directory inodes.
Synthetic images cover an extended root, a nested extended directory with a nonzero listing offset,
and both together; the results are compared with the existing basic-directory fixture. Compressed
metadata, directory indexes and a real firmware extraction were not newly checked for this fix.
## Known limits
- Locks: every mutex but the browser viewer's goes through `platform::sync`. A deadlock on one thread or across