xfs-forensic 0.1.0

Forensic anomaly auditor for XFS — deleted-inode recovery, directory-slack residue, v5 CRC/self-describing integrity as graded report::Finding, built on xfs-core
Documentation

xfs-forensic

xfs-core xfs-forensic Docs.rs Rust 1.83+ License: Apache-2.0 Sponsor

CI Coverage unsafe forbidden Security audit Docs

A from-scratch XFS reader and a graded anomaly auditor — walk the superblock, allocation-group headers, inodes, extents (inline and bmap-B+tree), and the five directory formats of an XFS image over any byte source, then turn its residue into evidence: v5 CRC-mismatched metadata, secondary-superblock divergence, AGI-unlinked orphaned inodes, and deleted inodes still carvable from their surviving residual extent records.

Two crates, one workspace:

  • xfs-core — the reader (imported as xfs): superblock + geometry, AGF / AGI / AGFL headers, inode cores (v2 100-byte / v3 176-byte, bigtime timestamps), inline and bmap-B+tree extents, short-form / block / data / leaf directories, and v5 CRC32c verification, over any byte slice. No unsafe, no C bindings.
  • xfs-forensic — the auditor: turns parsed XFS structures into severity-graded forensicnomicon::report::Findings, and recovers deleted inodes, so an XFS volume's anomalies aggregate uniformly with the partition and container layers.

Audit an XFS image in 30 seconds

[dependencies]
xfs-forensic = "0.1"   # pulls in xfs-core
use xfs_forensic::audit_findings;

// Feed it the raw image bytes; get back graded findings.
for finding in audit_findings(&image_bytes, "xfs") {
    println!("[{:?}] {}{}", finding.severity, finding.code, finding.note);
    // e.g. [Some(High)] XFS-SB-MIRROR-DIVERGENCE — AG 2 secondary superblock: agcount …
}

audit_findings parses the superblock, AG headers, and inode residue in place and grades what it finds. A structurally invalid image yields no findings (corruption is surfaced as its own finding, 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
XFS-CRC-MISMATCH High A v5 self-describing metadata block whose stored crc32c does not verify — consistent with corruption or post-write tampering
XFS-SB-MIRROR-DIVERGENCE High A secondary (per-AG) superblock field that differs from the AG-0 primary — consistent with a spliced or edited image
XFS-IMPOSSIBLE-GEOMETRY High A geometry field beyond what the image can hold — an allocation-bomb / corruption guard
XFS-ORPHANED-INODE Medium An AGI unlinked[64] bucket pointing at a live inode — unlinked while still open (orphaned-but-live), a recovery lead

Deleted-inode recovery is separate: recover_deleted(&image, &sb) scans for freed (di_mode == 0) inodes whose data fork still holds non-zero residual extent records, decodes them, and returns each carved DeletedInode — inode number, size, the recovered content, and the content's sha256 recovery gate.

The reader: navigate an image

xfs-core (imported as xfs) reads an XFS image over any byte slice:

use xfs::{Superblock, read_by_path};

// The primary superblock lives at physical offset 0; parse its geometry, then
// resolve a slash-separated path from the root inode to its file bytes,
// walking short-form / block / leaf directories and inline / btree extents.
let sb = Superblock::parse(&image[0..512])?;
let bytes = read_by_path(&image, &sb, "etc/hostname")?;
# Ok::<(), xfs::XfsError>(())

The bare crate name xfs on crates.io is an abandoned 2016 perf-data parser unrelated to the filesystem, so this on-disk reader publishes as xfs-core and takes the import path xfs.

What makes this different from a general-purpose XFS crate

Most XFS crates answer one question: "what files are on this volume?" This workspace answers the questions a digital forensics examiner actually needs:

Capability General-purpose XFS crate this workspace
Superblock + geometry (v4 and v5)
AGF / AGI / AGFL allocation-group headers
Inode cores — v2 (100-byte) / v3 (176-byte)
bigtime (64-bit) timestamp decoding partial
Inline extent list → file content
bmap-B+tree (di_format = btree) extent walk partial
Short-form / block / data / leaf directory formats
v5 CRC32c metadata verification (per block)
Secondary-superblock divergence detection (splice tell)
Orphaned-inode (AGI unlinked[64]) enumeration
Deleted-inode recovery from surviving residual extents
Impossible-geometry / allocation-bomb guards
Severity-graded report::Finding output
#![forbid(unsafe_code)]

Trust but verify

  • #![forbid(unsafe_code)] in both crates — no unsafe, no C bindings.
  • Panic-free — every integer / length / offset field is read through bounds-checked big-endian helpers; a malformed image degrades to an empty or typed result, never a panic.
  • Fuzzed — one cargo-fuzz target per parsed structure (superblock, agheaders, inode, extent, btree, dir, crc) plus a fuzz_forensic target driving the full audit_image / recover_deleted pipeline. fuzz.yml builds every target on each push and deep-fuzzes each for 10 minutes weekly.
  • Tier-1 validated — the reader is checked against a real third-party XFS filesystem, log2timeline/dfvfs's xfs.raw (Apache-2.0), whose ground truth comes from three implementations wholly separate from ours: xfs_db (xfsprogs), the Linux kernel's own read-only mount, and libfsxfs (which uses this same image as its reference corpus). See docs/validation.md.

Reader API (xfs-core)

Item Purpose
Superblock::parse / inode_to_location / read_inode Superblock geometry, inode-number → (AG, block, offset), inode read
Agf::parse / Agi::parse / Agfl::parse_v5 Allocation-group free-space / inode / free-list headers (v4 + v5)
Inode::parse On-disk inode core, format, timestamps (incl. bigtime), fork offset
read_extents / read_file_from_fork / assemble_extents Inline extent list → file bytes (holes zero-filled, size-truncated)
read_btree_extents bmap-B+tree (di_format = btree) extent walk
read_shortform_dir / read_block_dir / read_data_dir_block / read_by_path Directory decode across all shapes + path resolution
crc_status / verify_crc / verify_dir_block_crc / verify_bmbt_block_crc v5 CRC32c verification (v4 → None, no false positives)

Privacy Policy · Terms of Service · © 2026 Security Ronin Ltd