Files
ext4recovery/diagnose_inode.py
2026-04-30 11:04:05 +00:00

170 lines
7.0 KiB
Python

#!/usr/bin/env python3
"""
Diagnose why a file inode extracted as empty (or wrong size).
For each inode given, reports:
- size recorded in the inode
- flags (inline-data, extents, etc.)
- extent tree validity
- whether data blocks fall in the zeroed region
- actual bytes readable from each block
Usage:
python3 diagnose_inode.py <inode> [<inode> ...]
python3 diagnose_inode.py --dir <dir_inode> # diagnose all files in a dir
Options:
--device DEV [/dev/dm-0]
--backup-sb BLOCK [32768]
--db PATH [inodes.db]
--dir INUM Diagnose every file entry in this directory inode
"""
import argparse, struct, sys
import ext4lib
import ext4db
DEFAULT_DEV = '/dev/dm-0'
DEFAULT_BACKUP_SB = 32768
FLAG_INLINE_DATA = 0x10000000
FLAG_EXTENTS = 0x00080000
def diagnose_inode(f, sb, gdt_data, db, inum, zeroed_max_inum):
print(f"\ninode {inum}")
print(f" {''*50}")
# ── DB metadata ───────────────────────────────────────────────────────────
row = ext4db.get_inode(db, inum) if db else None
if row:
print(f" DB status : {row['status']}")
print(f" DB size : {row['size']:,} bytes")
print(f" DB type : {row['itype']:#06x}")
# ── raw inode from disk ───────────────────────────────────────────────────
try:
idata, slot = ext4lib.read_inode(f, sb, gdt_data, inum)
except Exception as e:
print(f" ERROR reading inode from disk: {e}")
return
inode = ext4lib.parse_inode_full(idata, slot, sb)
if inode is None:
print(" ERROR inode data too short")
return
size = inode['size']
flags = inode['flags']
status = ext4lib.classify_inode(idata, slot)
print(f" disk size : {size:,} bytes")
print(f" flags : {flags:#010x}", end='')
flag_names = []
if flags & FLAG_INLINE_DATA: flag_names.append('INLINE_DATA')
if flags & FLAG_EXTENTS: flag_names.append('EXTENTS')
print(f" ({', '.join(flag_names)})" if flag_names else '')
print(f" status : {status}")
if size == 0:
print(" DIAGNOSIS : inode size is 0 — file was empty or size field is corrupted")
return
# ── inline data ───────────────────────────────────────────────────────────
if flags & FLAG_INLINE_DATA:
inline = idata[slot + 40:slot + 40 + size]
nonzero = sum(1 for b in inline if b != 0)
print(f" DIAGNOSIS : inline data ({size} bytes, {nonzero} non-zero bytes in inode body)")
return
# ── extent tree ───────────────────────────────────────────────────────────
base = slot + 40
magic, entries, _, depth = struct.unpack_from('<HHHH', idata, base)
print(f" ext magic : {magic:#06x} {'OK' if magic == 0xF30A else 'BAD — not a valid extent header'}")
print(f" ext depth : {depth} entries: {entries}")
if magic != 0xF30A:
print(" DIAGNOSIS : invalid extent tree magic — inode body is corrupt/zeroed")
return
blocks = ext4lib.read_extent_tree_blocks(f, idata, slot)
print(f" extents : {len(blocks)} logical→physical block pairs")
if not blocks:
print(" DIAGNOSIS : extent tree parsed but returned no blocks")
print(" (entries=0, or all extents have phys=0, or index nodes unreadable)")
return
zeroed = [(l, p) for l, p in blocks if p * ext4lib.BLOCK // ext4lib.BLOCK <= zeroed_max_inum * 8]
# More direct check: block in zeroed region means block < zeroed_groups * blocks_per_group
zeroed_block_max = ext4db.get_fs_meta_int(db, 'zeroed_groups', 13) * \
ext4db.get_fs_meta_int(db, 'blocks_per_group', 32768) if db else 0
zeroed_blocks = [(l, p) for l, p in blocks if p < zeroed_block_max]
good_blocks = [(l, p) for l, p in blocks if p >= zeroed_block_max]
print(f" blocks in zeroed region : {len(zeroed_blocks)}/{len(blocks)}")
print(f" blocks in good region : {len(good_blocks)}/{len(blocks)}")
# Sample first good and first zeroed block
if zeroed_blocks and not good_blocks:
print(" DIAGNOSIS : ALL data blocks are in the zeroed region — file content is lost")
print(f" first block: phys={zeroed_blocks[0][1]} (zeroed region ends at block {zeroed_block_max})")
return
if zeroed_blocks and good_blocks:
print(" DIAGNOSIS : PARTIAL — some blocks recoverable, some in zeroed region")
print(f" {len(good_blocks)} of {len(blocks)} blocks are readable")
# Check readability of first block
l0, p0 = blocks[0]
try:
bdata = ext4lib.read_at(f, p0 * ext4lib.BLOCK, ext4lib.BLOCK)
nonzero = sum(1 for b in bdata if b != 0)
print(f" first block phys={p0}: {nonzero}/4096 non-zero bytes")
if nonzero == 0:
print(" DIAGNOSIS : first data block reads as all zeros — block content wiped or wrong address")
else:
print(" DIAGNOSIS : data blocks appear readable — extraction should work")
except OSError as e:
print(f" first block read error: {e}")
print(" DIAGNOSIS : I/O error reading data block")
def main():
parser = argparse.ArgumentParser(description='Diagnose why an inode extracted as empty')
parser.add_argument('inodes', nargs='*', type=int)
parser.add_argument('--device', default=DEFAULT_DEV)
parser.add_argument('--backup-sb', type=int, default=DEFAULT_BACKUP_SB)
parser.add_argument('--db', default='inodes.db')
parser.add_argument('--dir', type=int, metavar='INUM',
help='Diagnose all file entries under this directory inode')
args = parser.parse_args()
db = ext4db.open_db(args.db)
zeroed_max_inum = (ext4db.get_fs_meta_int(db, 'zeroed_groups', 13) *
ext4db.get_fs_meta_int(db, 'inodes_per_group', 8192))
inodes = list(args.inodes)
if args.dir:
entries = ext4db.get_dir_entries(db, args.dir)
for name, (child_inum, ftype) in sorted(entries.items()):
if name in ('.', '..'):
continue
if ftype == ext4lib.FTYPE_REG or ftype == 0:
print(f"\n{'='*60}")
print(f" {name!r} (inode {child_inum})")
inodes.append(child_inum)
if not inodes:
parser.error('Provide inode numbers or use --dir <inode>')
with open(args.device, 'rb') as f:
sb, gdt_data, _ = ext4lib.load_fs(f, args.backup_sb)
for inum in inodes:
diagnose_inode(f, sb, gdt_data, db, inum, zeroed_max_inum)
if __name__ == '__main__':
main()