Initial remote commit
This commit is contained in:
169
diagnose_inode.py
Normal file
169
diagnose_inode.py
Normal file
@@ -0,0 +1,169 @@
|
||||
#!/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()
|
||||
|
||||
Reference in New Issue
Block a user