170 lines
7.0 KiB
Python
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()
|
|
|