Initial remote commit
This commit is contained in:
154
test/scan.py
Normal file
154
test/scan.py
Normal file
@@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Strict ext4 directory entry scanner for pterodactyl paths.
|
||||
"""
|
||||
import struct
|
||||
|
||||
CHUNK = 128 * 512
|
||||
LV_START = 5120000 * 512
|
||||
BSIZE = 4096
|
||||
DISKS = ['/dev/sda', '/dev/sdd', '/dev/sdc', '/dev/sdb']
|
||||
|
||||
# Only exact target names we expect as directory entries
|
||||
EXACT_TARGETS = [
|
||||
b'pterodactyl',
|
||||
b'volumes',
|
||||
b'wings',
|
||||
]
|
||||
|
||||
def is_valid_dirent(block, off, name):
|
||||
"""Strict validation of an ext4 directory entry."""
|
||||
if off + 8 + len(name) > BSIZE:
|
||||
return False
|
||||
|
||||
inode = struct.unpack_from('<I', block, off)[0]
|
||||
rec_len = struct.unpack_from('<H', block, off+4)[0]
|
||||
name_len = block[off+6]
|
||||
ftype = block[off+7]
|
||||
|
||||
# inode must be plausible (> 10, not absurdly large)
|
||||
if not (10 < inode < 500_000_000):
|
||||
return False
|
||||
|
||||
# name_len must exactly match our target
|
||||
if name_len != len(name):
|
||||
return False
|
||||
|
||||
# rec_len must be >= 8 + name_len and <= 4096
|
||||
# and aligned to 4 bytes
|
||||
min_rec = 8 + name_len
|
||||
if rec_len < min_rec or rec_len > BSIZE or rec_len % 4 != 0:
|
||||
return False
|
||||
|
||||
# file type must be a known ext4 type
|
||||
if ftype not in (1, 2, 7): # file, dir, symlink only
|
||||
return False
|
||||
|
||||
# the name bytes must match exactly and be clean ASCII
|
||||
actual_name = block[off+8:off+8+name_len]
|
||||
if actual_name != name:
|
||||
return False
|
||||
|
||||
# byte immediately after name (padding) should be 0
|
||||
pad_off = off + 8 + name_len
|
||||
if pad_off < BSIZE and block[pad_off] != 0:
|
||||
return False
|
||||
|
||||
# Previous entry should also look valid if we're not at start of block
|
||||
# (skip this check for now - too complex)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def scan_block(block, phys_base):
|
||||
hits = []
|
||||
for off in range(0, BSIZE - 8):
|
||||
for target in EXACT_TARGETS:
|
||||
# Quick check: does target appear at this offset+8?
|
||||
if block[off+8:off+8+len(target)] != target:
|
||||
continue
|
||||
if is_valid_dirent(block, off, target):
|
||||
inode = struct.unpack_from('<I', block, off)[0]
|
||||
rec_len = struct.unpack_from('<H', block, off+4)[0]
|
||||
ftype = block[off+7]
|
||||
grp = (inode - 1) // 8192
|
||||
hits.append({
|
||||
'phys': phys_base + off,
|
||||
'inode': inode,
|
||||
'name': target.decode(),
|
||||
'ftype': {1:'file',2:'dir',7:'symlink'}.get(ftype,'?'),
|
||||
'group': grp,
|
||||
'intact': grp >= 13,
|
||||
'rec_len': rec_len,
|
||||
})
|
||||
return hits
|
||||
|
||||
|
||||
def iter_data_chunks(disk_path):
|
||||
with open(disk_path, 'rb') as f:
|
||||
f.seek(0, 2)
|
||||
disk_size = f.tell()
|
||||
|
||||
chunk_num = 0
|
||||
with open(disk_path, 'rb') as f:
|
||||
phys = LV_START
|
||||
while phys + CHUNK <= disk_size:
|
||||
if chunk_num % 5 != 4:
|
||||
f.seek(phys)
|
||||
yield phys, f.read(CHUNK)
|
||||
phys += CHUNK
|
||||
chunk_num += 1
|
||||
|
||||
|
||||
def main():
|
||||
all_hits = []
|
||||
|
||||
for disk_idx, disk in enumerate(DISKS):
|
||||
print(f'\nScanning {disk}...', flush=True)
|
||||
chunks = 0
|
||||
hits = 0
|
||||
|
||||
for phys, chunk_data in iter_data_chunks(disk):
|
||||
# Pre-filter: any target in chunk?
|
||||
if not any(t in chunk_data for t in EXACT_TARGETS):
|
||||
chunks += 1
|
||||
continue
|
||||
|
||||
# Scan each 4KB block in chunk
|
||||
for blk in range(0, len(chunk_data), BSIZE):
|
||||
block = chunk_data[blk:blk+BSIZE]
|
||||
for hit in scan_block(block, phys + blk):
|
||||
status = 'INTACT' if hit['intact'] else 'LOST'
|
||||
print(f" [{status}] '{hit['name']}' "
|
||||
f"inode={hit['inode']} "
|
||||
f"group={hit['group']} "
|
||||
f"type={hit['ftype']} "
|
||||
f"phys={hit['phys']}")
|
||||
all_hits.append((disk_idx, hit))
|
||||
hits += 1
|
||||
|
||||
chunks += 1
|
||||
if chunks % 5000 == 0:
|
||||
gb = (phys - LV_START) / 1024**3
|
||||
print(f' {disk}: {gb:.1f}GB, {hits} hits', flush=True)
|
||||
|
||||
print(f' Finished: {hits} hits')
|
||||
|
||||
print('\n=== RESULTS ===')
|
||||
# Group by name and inode
|
||||
from collections import defaultdict
|
||||
by_inode = defaultdict(list)
|
||||
for disk_idx, hit in all_hits:
|
||||
key = (hit['inode'], hit['name'])
|
||||
by_inode[key].append((DISKS[disk_idx], hit['phys']))
|
||||
|
||||
print(f'\nUnique (inode, name) pairs: {len(by_inode)}')
|
||||
for (inode, name), locations in sorted(by_inode.items()):
|
||||
grp = (inode-1)//8192
|
||||
status = 'INTACT' if grp >= 13 else 'LOST'
|
||||
print(f" '{name}' inode={inode} group={grp} [{status}]")
|
||||
for disk, phys in locations[:3]:
|
||||
print(f" {disk} phys={phys}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user