btrfs-forensic
A from-scratch btrfs reader and a graded anomaly auditor — walk the chunk tree, B-trees, and FS-tree of a btrfs image over any byte source, then turn its copy-on-write history into evidence: stale backup roots, csum-mismatched tree blocks, orphaned inodes, and deleted files still carvable from an older generation's FS-tree.
Two crates, one workspace:
btrfs-core— the reader: superblock +sys_chunk_arraybootstrap, chunk-tree logical→physical mapping, B-tree node navigation, FS-tree inode / directory / path resolution, andEXTENT_DATA→ file content with always-compiled zlib / LZO / zstd decompression, over any byte slice. Nounsafe, no C bindings.btrfs-forensic— the auditor: turns parsed btrfs structures into severity-gradedforensicnomicon::report::Findings, and recovers CoW-deleted files, so a btrfs volume's anomalies aggregate uniformly with the partition and container layers.
Audit a btrfs image in 30 seconds
[]
= "0.1" # pulls in btrfs-core
use audit_findings;
// Feed it the raw image bytes; get back graded findings.
for finding in audit_findings
audit_findings parses the superblock, chunk tree, and FS-tree in place and grades what it finds. A structurally invalid image yields no findings (corruption is surfaced as its own finding or by the carver, never a panic). For the typed form, audit_image(&image) returns Vec<Anomaly> — each anomaly.to_finding(source) converts to a report::Finding.
The anomaly codes
Each finding is an observation ("consistent with …"); the examiner draws the conclusions. Codes are a stable, published contract.
| Code | Severity | What it observes |
|---|---|---|
BTRFS-SUPERBLOCK-CRC-MISMATCH |
High | The superblock's own crc32c does not verify — corruption or post-write tampering |
BTRFS-CRC-MISMATCH |
High | A metadata node/leaf whose stored crc32c does not verify |
BTRFS-BACKUP-ROOT-DIVERGENCE |
High | A btrfs_root_backup entry inconsistent with the committed generation — consistent with rollback / tampered backup roots |
BTRFS-IMPOSSIBLE-GEOMETRY |
High | A root/geometry field beyond what the image can hold — an allocation-bomb / corruption guard |
BTRFS-ORPHANED-INODE |
Medium | An ORPHAN_ITEM in the FS_TREE — an inode unlinked while open / pending cleanup, a recovery lead |
Deleted-file recovery is separate: recover_deleted(&image) walks an older generation's FS_TREE (reached through a btrfs_root_backup entry), diffs it against the current FS_TREE, and returns each carved RecoveredFile — name, inode, generation, size, content, and the content's sha256 recovery gate.
The reader: navigate an image
btrfs-core (imported as btrfs_core) reads a btrfs image over any byte slice:
use ;
// The superblock lives at physical offset 65536; the chunk map is walked from
// the sys_chunk_array bootstrap it carries.
let sb = parse?;
let map = walk?;
// Resolve a slash-separated path from the FS_TREE root to its file bytes,
// decompressing zlib / LZO / zstd extents transparently.
let bytes = read_by_path_content?;
# Ok::
The bare crate name btrfs on crates.io is an unrelated live-FS ioctl wrapper, so this on-disk reader publishes as btrfs-core and imports as btrfs_core.
What makes this different from a general-purpose btrfs crate
Most btrfs crates answer one question: "what files are on this volume?" This workspace answers the questions a digital forensics examiner actually needs:
| Capability | General-purpose btrfs crate | this workspace |
|---|---|---|
Superblock + sys_chunk_array bootstrap |
✅ | ✅ |
| Chunk-tree logical→physical mapping | ✅ | ✅ |
| B-tree node / leaf navigation | ✅ | ✅ |
| FS-tree inode / directory / path resolution | ✅ | ✅ |
EXTENT_DATA → file content (inline / regular / prealloc / hole) |
✅ | ✅ |
| zlib / LZO / zstd extent decompression | partial | ✅ |
| crc32c metadata-node verification (per block) | — | ✅ |
| Superblock csum tamper check | — | ✅ |
btrfs_root_backup divergence detection (rollback tell) |
— | ✅ |
Orphaned-inode (ORPHAN_ITEM) enumeration |
— | ✅ |
| CoW deleted-file recovery from an older generation's FS-tree | — | ✅ |
| Impossible-geometry / allocation-bomb guards | — | ✅ |
Severity-graded report::Finding output |
— | ✅ |
#![forbid(unsafe_code)] |
— | ✅ |
Trust but verify
#![forbid(unsafe_code)]in both crates — nounsafe, no C bindings.- Panic-free — every integer / length / offset field is read through bounds-checked helpers; a malformed image degrades to an empty or typed result, never a panic.
- Fuzzed — one
cargo-fuzztarget per parsed structure (superblock,node,chunk,fstree,extent,crc) plus afuzz_forensictarget driving the fullaudit_image/recover_deletedpipeline.fuzz.ymlbuilds every target on each push and deep-fuzzes each for 10 minutes weekly. - Tier-1 validated — the reader is checked against a real Fedora Cloud btrfs filesystem whose ground truth comes from btrfs-progs' own
btrfs inspect-internal dump-super -f/dump-tree, a wholly separate implementation. Seedocs/validation.md.
Reader API (btrfs-core)
| Item | Purpose |
|---|---|
Superblock::parse |
Superblock geometry, tree roots, csum descriptor, sys_chunk_array |
ChunkMap::walk / logical_to_physical |
Full chunk-tree logical→physical map |
read_node / Node::parse |
Read / decode any nodesize B-tree block (header + items or key-ptrs) |
fs_tree_root / read_inode / list_dir / read_by_path |
FS-tree ROOT_ITEM, inode metadata, directory entries, path resolution |
read_file / read_by_path_content |
EXTENT_DATA → file bytes (inline / regular / prealloc / hole), truncated to size |
decompress_extent |
zlib / LZO / zstd extent decompression (always compiled in) |
superblock_crc_status / verify_superblock_crc32c |
crc32c superblock verification |
Privacy Policy · Terms of Service · © 2026 Security Ronin Ltd