PhoinixDR

ext2/3/4 reader and undelete (M10)

phoinix-fs-ext reads ext2, ext3 and ext4 volumes natively over a BlockReader, enumerates deleted files and recovers their layouts through the jbd2 journal where one exists. Nothing is ever written to the source (ADR-0002).

Structures read

structure module notes
superblock (+1024, magic EF53) superblock 64-bit block counts, feature flags, metadata_csum seed, checksum verification, meta_bg descriptor placement
group descriptors (32 or 64 bytes) group block/inode bitmaps, inode table, INODE_UNINIT, itable_unused
inodes (128 or larger) inode mode, size, links, dtime, crtime, generation, flags, crc32c checksum with the volume seed
extent trees (F30A) extent depth-bounded, cycle-safe walk into logical runs, uninitialized extents flagged
block maps (ext2/3) blockmap direct, indirect, double and triple indirection, holes as explicit runs
directories dir live entries, deleted entries hidden in record slack, htree roots
block bitmaps bitmap lazily cached per group, BLOCK_UNINIT groups are free
jbd2 journal journal descriptor, commit and revoke blocks; 8/12/16-byte tags; per-tag crc32c (v2/v3); escaped blocks restored

Inline data (INLINE_DATA) is read from the inode's i_block only; the part of an inline file that lives in the in-inode extended attribute is not followed yet. Encryption and compression features are not supported.

What deletion leaves behind

driver inode directory entry
ext4 (also mounting ext3) dtime and ctime set, mtime rewritten by the truncate, size, block count and extent tree cleared, generation kept folded into the previous record's rec_len; name and inode number survive in slack
ext2 as above, block map cleared name and inode number zeroed

So the inode table alone yields deleted inodes with their deletion time and nothing to locate. The journal changes that: jbd2 logs whole metadata blocks, and the copy of the inode-table block written by the last transaction that touched the file while it was alive still carries its size and extent tree. Directory blocks are journaled too, so names that were removed from a directory or whose slack was overwritten can be read back from older copies.

The undelete join

ExtUndelete joins three sources per inode number:

inode table ──► deleted inodes (dtime ≠ 0 or links = 0)
journal     ──► older copies of inode-table blocks: size, extents, timestamps
                older copies of directory blocks: names, plus the range of
                transactions in which each entry was live
directories ──► slack entries: name → inode number

Rules:

Candidates are identified by FileSystemObjectId::Ext { inode, generation } and addressed on the command line by inode number (phoinix recover img 25).

Streams

ExtVolume::open_layout serves a file as an ExtentStream. Holes (logical gaps between extents, tails beyond the last extent, zero pointers in a block map) and uninitialized extents read as zeros through a zero-tail reader, so sparse files come back byte-exact.

Allocation view

ExtUndelete implements AllocationView over the block bitmaps (first data block onwards, block-sized units), so deep scan carves free blocks only and folds hits into journal candidates with the same start.

Limits