Initial remote commit

This commit is contained in:
2026-04-30 11:04:05 +00:00
commit b86e4f9a98
103 changed files with 262770 additions and 0 deletions

View File

@@ -0,0 +1,80 @@
CHUNK = 128*512
LV_START = 5120000*512
BSIZE = 4096
# What if block 5251104 is an ABSOLUTE block number on md0
# (not relative to LV start)?
# Then: md0_byte = 5251104 * 4096 = 21,508,521,984
# And we DON'T add LV_START
abs_byte = 5251104 * BSIZE
print(f'Block 5251104 as absolute md0 byte: {abs_byte}')
print(f'md0 sector: {abs_byte//512}')
# Read it directly from md0 (no translation)
with open('/dev/md0','rb') as f:
f.seek(abs_byte)
data = f.read(BSIZE)
import struct
nonzero = sum(1 for b in data if b != 0)
print(f'Nonzero bytes: {nonzero}/4096')
print(f'First 32: {data[:32].hex()}')
# Try as directory
off = 0
entries = []
while off < BSIZE-8:
ino = struct.unpack_from('<I', data, off)[0]
rec_len = struct.unpack_from('<H', data, off+4)[0]
name_len= data[off+6]
ftype = data[off+7]
if rec_len < 8: break
if ino > 0 and name_len > 0:
name = data[off+8:off+8+name_len].decode('utf-8',errors='replace')
tname = {1:'file',2:'dir',7:'link'}.get(ftype,'?')
entries.append((ino,name,ftype))
off += rec_len
if entries:
print('Directory entries:')
for ino,name,ftype in entries:
tname = {1:'file',2:'dir',7:'link'}.get(ftype,'?')
print(f' {tname} inode={ino} {name!r}')
else:
print('No valid directory entries')
# Also try: what if the block number is relative to LV start
# but WITHOUT the 4/5 chunk translation?
# i.e. just: md0_byte = LV_START + block * BSIZE
direct_byte = LV_START + 5251104 * BSIZE
print()
print(f'Block 5251104 relative to LV, no translation: {direct_byte}')
with open('/dev/md0','rb') as f:
f.seek(direct_byte)
data2 = f.read(BSIZE)
nonzero2 = sum(1 for b in data2 if b != 0)
print(f'Nonzero bytes: {nonzero2}/4096')
print(f'First 32: {data2[:32].hex()}')
off = 0
entries2 = []
while off < BSIZE-8:
ino = struct.unpack_from('<I', data2, off)[0]
rec_len = struct.unpack_from('<H', data2, off+4)[0]
name_len= data2[off+6]
ftype = data2[off+7]
if rec_len < 8: break
if ino > 0 and name_len > 0:
name = data2[off+8:off+8+name_len].decode('utf-8',errors='replace')
tname = {1:'file',2:'dir',7:'link'}.get(ftype,'?')
entries2.append((ino,name,ftype))
off += rec_len
if entries2:
print('Directory entries (no translation):')
for ino,name,ftype in entries2:
tname = {1:'file',2:'dir',7:'link'}.get(ftype,'?')
print(f' {tname} inode={ino} {name!r}')
else:
print('No valid directory entries')

View File

@@ -0,0 +1,33 @@
import subprocess, struct
CHUNK = 128*512
LV_START = 5120000*512
BSIZE = 4096
# Check istat for volumes dir (inode 1585918) - we know fls works on this
r = subprocess.run(['istat', '/dev/nbd0', '1585918'],
capture_output=True, text=True)
print('istat for volumes (inode 1585918):')
print(r.stdout)
# Check istat for pterodactyl dir (inode 1574102)
r2 = subprocess.run(['istat', '/dev/nbd0', '1574102'],
capture_output=True, text=True)
print('istat for pterodactyl (inode 1574102):')
print(r2.stdout)
# Check which chunk each data block falls in
def check_block(block_num):
virt = block_num * BSIZE
group = virt // (5*CHUNK)
in_group = virt % (5*CHUNK)
chunk_idx = in_group // CHUNK
return chunk_idx
# Parse direct blocks from istat output
for line in r.stdout.splitlines():
if 'Direct Blocks' in line or (line.strip() and line.strip()[0].isdigit()):
blocks = [int(x) for x in line.split() if x.isdigit()]
for b in blocks:
ci = check_block(b)
print(f' block {b}: chunk_idx={ci} {"METADATA" if ci==4 else "DATA"}')

100
misc_tools/checkfifth.py Normal file
View File

@@ -0,0 +1,100 @@
CHUNK = 128*512
LV_START = 5120000*512
BSIZE = 4096
# We know block 9262 is the root directory data block
# and it reads correctly. Let's find its chunk position.
known_virt = 9262 * BSIZE
group = known_virt // (5*CHUNK)
in_group = known_virt % (5*CHUNK)
chunk_idx = in_group // CHUNK
intra = in_group % CHUNK
print(f'Root dir block 9262: chunk_group={group} chunk_idx={chunk_idx}')
# Now check the 5th chunk in the SAME group
meta_virt = group * 5 * CHUNK + 4 * CHUNK
print(f'5th chunk in same group at phys byte: {LV_START + meta_virt}')
# Read 512 bytes from the 5th chunk
with open('/dev/md0','rb') as f:
f.seek(LV_START + meta_virt)
data = f.read(512)
print(f'First 64 bytes: {data[:64].hex()}')
print()
# Compare with what we read from the PERC metadata chunk earlier
# istat showed volumes dir data at block 5251104 (chunk_idx=4)
# Let's check if that block's data might actually be at a different
# physical location - maybe the PERC remapped it
# What if for chunk_idx=4, the data is stored in a DIFFERENT disk's
# corresponding chunk? i.e. the PERC uses the 5th physical chunk
# on a different disk?
# Our disk order: sda(0) sde(1) sdd(2) sdc(3)
# In RAID0: chunk N goes to disk N%4
# For a group of 5 virtual chunks:
# virtual chunk 0 -> disk 0
# virtual chunk 1 -> disk 1
# virtual chunk 2 -> disk 2
# virtual chunk 3 -> disk 3
# virtual chunk 4 -> ??? (metadata chunk - no disk?)
# BUT: if the physical layout has 5 chunks per group across 4 disks
# maybe it's: disk0 gets chunks 0,4 / disk1 gets chunk1 / etc?
# Or maybe all 5 chunks exist on disk but chunk 4 is PERC metadata
# that happens to occupy the same space as filesystem data?
# The PERC sector-level metadata we found earlier:
# Each 64KB metadata chunk stores 512 bytes per sector of the adjacent data
# That's 128 sectors * 512 bytes = 64KB of per-sector checksums/metadata
# So the 5th chunk IS pure PERC internal data, not filesystem data
# This means: filesystem block 5251104 maps to virtual chunk_idx=4
# BUT the actual filesystem data for that block must be stored differently
# OR the block number in the inode is a VIRTUAL block number
# that the PERC translates differently than we think
# Let's check: what if block numbers in inodes are PERC virtual block numbers?
# PERC virtual block 5251104:
# In PERC virtual space (5 chunks per group):
# group = 5251104*4096 // (5*CHUNK) = ?
perc_virt_byte = 5251104 * BSIZE
perc_group = perc_virt_byte // (5*CHUNK)
perc_in_group = perc_virt_byte % (5*CHUNK)
perc_chunk_idx = perc_in_group // CHUNK
perc_intra = perc_in_group % CHUNK
print(f'If block 5251104 is PERC virtual:')
print(f' PERC group={perc_group} chunk_idx={perc_chunk_idx} intra={perc_intra}')
# And the physical location would be:
# physical = LV_START + perc_group*4*CHUNK + perc_chunk_idx*CHUNK + perc_intra
if perc_chunk_idx != 4:
phys = LV_START + perc_group*4*CHUNK + perc_chunk_idx*CHUNK + perc_intra
print(f' Physical byte: {phys}')
with open('/dev/md0','rb') as f:
f.seek(phys)
data = f.read(BSIZE)
nonzero = sum(1 for b in data if b != 0)
print(f' Nonzero: {nonzero}/4096')
print(f' First 32: {data[:32].hex()}')
# Try to parse as directory
import struct
off = 0
print(' Directory entries:')
while off < BSIZE-8:
ino = struct.unpack_from('<I', data, off)[0]
rec_len = struct.unpack_from('<H', data, off+4)[0]
name_len= data[off+6]
ftype = data[off+7]
if rec_len < 8: break
if ino > 0 and name_len > 0:
name = data[off+8:off+8+name_len].decode('utf-8',errors='replace')
tname = {1:'file',2:'dir',7:'link'}.get(ftype,'?')
print(f' {tname} inode={ino} {name!r}')
off += rec_len
else:
print(f' Still in metadata chunk!')

107
misc_tools/digging.py Normal file
View File

@@ -0,0 +1,107 @@
import struct
CHUNK = 128*512
LV_START = 5120000*512
BSIZE = 4096
IPG = 8192
def read_virt(virt_byte, length):
result = bytearray(length)
pos = virt_byte
remaining = length
with open('/dev/md0','rb') as f:
while remaining > 0:
group = pos // (5*CHUNK)
in_group = pos % (5*CHUNK)
chunk_idx = in_group // CHUNK
intra = in_group % CHUNK
seg_len = min(CHUNK-intra, remaining)
dst_off = pos - virt_byte
if chunk_idx != 4:
phys = LV_START + group*4*CHUNK + chunk_idx*CHUNK + intra
f.seek(phys)
data = f.read(seg_len)
result[dst_off:dst_off+len(data)] = data
pos += seg_len
remaining -= seg_len
return bytes(result)
# Cache GDT
print('Loading full GDT...')
NUM_GROUPS = 35728
gdt_data = read_virt(BSIZE, NUM_GROUPS*64)
print(f'GDT loaded: {len(gdt_data)} bytes')
def get_inode_table_block(group):
entry = gdt_data[group*64:(group+1)*64]
it_lo = struct.unpack_from('<I', entry, 8)[0]
it_hi = struct.unpack_from('<I', entry, 40)[0]
return (it_hi<<32)|it_lo
def read_inode(inode_num):
group = (inode_num-1)//IPG
idx = (inode_num-1)%IPG
it_block = get_inode_table_block(group)
return read_virt(it_block*BSIZE + idx*256, 256)
def get_extents(inode_data):
blocks = []
if struct.unpack_from('<H', inode_data, 40)[0] != 0xf30a:
return blocks
entries = struct.unpack_from('<H', inode_data, 42)[0]
depth = struct.unpack_from('<H', inode_data, 46)[0]
if depth == 0:
for i in range(min(entries,4)):
off = 52+i*12
ee_len = struct.unpack_from('<H', inode_data, off+4)[0]
ee_hi = struct.unpack_from('<H', inode_data, off+6)[0]
ee_lo = struct.unpack_from('<I', inode_data, off+8)[0]
start = (ee_hi<<32)|ee_lo
if ee_len > 1024: continue
for b in range(min(ee_len,8)):
blocks.append(start+b)
return blocks
def list_dir(inode_num):
inode_data = read_inode(inode_num)
mode = struct.unpack_from('<H', inode_data, 0)[0]
if (mode&0xf000) != 0x4000:
return []
entries = []
for blk in get_extents(inode_data):
blk_data = read_virt(blk*BSIZE, BSIZE)
off = 0
while off < BSIZE-8:
ino = struct.unpack_from('<I', blk_data, off)[0]
rec_len = struct.unpack_from('<H', blk_data, off+4)[0]
name_len= blk_data[off+6]
ftype = blk_data[off+7]
if rec_len < 8: break
if ino > 0 and name_len > 0:
name = blk_data[off+8:off+8+name_len].decode('utf-8',errors='replace')
if name not in ('.','..'):
entries.append((ino, name, ftype))
off += rec_len
return entries
# Walk tree from /var
print()
print('=== /var (inode 1310721) ===')
for ino, name, ftype in list_dir(1310721):
tname = {1:'file',2:'dir',7:'link'}.get(ftype,'?')
print(f' {tname:4s} inode={ino:10d} {name!r}')
print()
for ino, name, ftype in list_dir(1310721):
if name == 'lib':
print(f'=== /var/lib (inode {ino}) ===')
for i2,n2,f2 in list_dir(ino):
tname = {1:'file',2:'dir',7:'link'}.get(f2,'?')
print(f' {tname:4s} inode={i2:10d} {n2!r}')
for i2,n2,f2 in list_dir(ino):
if n2 == 'pterodactyl':
print(f'\n=== /var/lib/pterodactyl (inode {i2}) ===')
for i3,n3,f3 in list_dir(i2):
tname = {1:'file',2:'dir',7:'link'}.get(f3,'?')
print(f' {tname:4s} inode={i3:10d} {n3!r}')

157
misc_tools/extant_debug.py Normal file
View File

@@ -0,0 +1,157 @@
import struct
CHUNK = 128*512
LV_START = 5120000*512
BSIZE = 4096
IPG = 8192
def read_virt(virt_byte, length):
result = bytearray(length)
pos = virt_byte
remaining = length
with open('/dev/md0','rb') as f:
while remaining > 0:
group = pos // (5*CHUNK)
in_group = pos % (5*CHUNK)
chunk_idx = in_group // CHUNK
intra = in_group % CHUNK
seg_len = min(CHUNK-intra, remaining)
dst_off = pos - virt_byte
if chunk_idx != 4:
phys = LV_START + group*4*CHUNK + chunk_idx*CHUNK + intra
f.seek(phys)
data = f.read(seg_len)
result[dst_off:dst_off+len(data)] = data
pos += seg_len
remaining -= seg_len
return bytes(result)
NUM_GROUPS = 35728
gdt_data = read_virt(BSIZE, NUM_GROUPS*64)
def get_inode_table_block(group):
entry = gdt_data[group*64:(group+1)*64]
it_lo = struct.unpack_from('<I', entry, 8)[0]
it_hi = struct.unpack_from('<I', entry, 40)[0]
return (it_hi<<32)|it_lo
# Read /var inode raw and dump everything
var_inode = 1310721
group = (var_inode-1)//IPG
idx = (var_inode-1)%IPG
it_block = get_inode_table_block(group)
it_virt = it_block*BSIZE + idx*256
print(f'/var inode {var_inode}:')
print(f' group={group} idx={idx}')
print(f' it_block={it_block}')
print(f' inode virt byte={it_virt}')
# Check if this virtual byte is in a metadata chunk
in_group = it_virt % (5*CHUNK)
chunk_idx = in_group // CHUNK
print(f' chunk_idx={chunk_idx} (4=metadata, returns zeros)')
inode_data = read_virt(it_virt, 256)
print(f' raw inode: {inode_data[:64].hex()}')
mode = struct.unpack_from('<H', inode_data, 0)[0]
size = struct.unpack_from('<I', inode_data, 4)[0]
links = struct.unpack_from('<H', inode_data, 26)[0]
mtime = struct.unpack_from('<I', inode_data, 16)[0]
ext_magic = struct.unpack_from('<H', inode_data, 40)[0]
eh_entries= struct.unpack_from('<H', inode_data, 42)[0]
eh_depth = struct.unpack_from('<H', inode_data, 46)[0]
print(f' mode=0x{mode:04x} size={size} links={links}')
print(f' mtime={mtime} ext_magic=0x{ext_magic:04x}')
print(f' eh_entries={eh_entries} eh_depth={eh_depth}')
print()
# Dump extent tree
if ext_magic == 0xf30a:
print('Extent tree:')
if eh_depth == 0:
for i in range(min(eh_entries,4)):
off = 52+i*12
ee_block = struct.unpack_from('<I', inode_data, off)[0]
ee_len = struct.unpack_from('<H', inode_data, off+4)[0]
ee_hi = struct.unpack_from('<H', inode_data, off+6)[0]
ee_lo = struct.unpack_from('<I', inode_data, off+8)[0]
ee_start = (ee_hi<<32)|ee_lo
print(f' Extent {i}: logical={ee_block} len={ee_len} '
f'phys_start={ee_start}')
# Check if this block is in a metadata chunk
blk_virt = ee_start*BSIZE
in_group = blk_virt % (5*CHUNK)
chunk_idx = in_group // CHUNK
print(f' virt_byte={blk_virt} chunk_idx={chunk_idx}')
# Read the block
blk_data = read_virt(blk_virt, BSIZE)
nonzero = sum(1 for b in blk_data if b != 0)
print(f' nonzero bytes: {nonzero}/4096')
print(f' first 32 bytes: {blk_data[:32].hex()}')
# Try to parse as directory
off2 = 0
count = 0
while off2 < BSIZE-8:
ino = struct.unpack_from('<I', blk_data, off2)[0]
rec_len = struct.unpack_from('<H', blk_data, off2+4)[0]
name_len= blk_data[off2+6]
ftype = blk_data[off2+7]
if rec_len < 8: break
if ino > 0 and name_len > 0:
name = blk_data[off2+8:off2+8+name_len].decode('utf-8',errors='replace')
tname = {1:'file',2:'dir',7:'link'}.get(ftype,'?')
print(f' entry: {tname} inode={ino} {name!r}')
count += 1
off2 += rec_len
if count == 0:
print(f' No valid directory entries found')
else:
print(f' depth={eh_depth} - need to follow interior nodes')
# Read interior node
for i in range(min(eh_entries,4)):
off = 52+i*12
ei_block = struct.unpack_from('<I', inode_data, off)[0]
ei_hi = struct.unpack_from('<H', inode_data, off+4)[0]
ei_lo = struct.unpack_from('<I', inode_data, off+8)[0] # wrong offsets for idx
# Extent index: ee_block(4) + ei_leaf_lo(4) + ei_leaf_hi(2) + unused(2)
ei_leaf_lo = struct.unpack_from('<I', inode_data, off+4)[0]
ei_leaf_hi = struct.unpack_from('<H', inode_data, off+8)[0]
ei_leaf = (ei_leaf_hi<<32)|ei_leaf_lo
print(f' Index {i}: logical={ei_block} leaf={ei_leaf}')
# Read the leaf block
leaf_virt = ei_leaf*BSIZE
leaf_data = read_virt(leaf_virt, BSIZE)
leaf_magic = struct.unpack_from('<H', leaf_data, 0)[0]
leaf_entries = struct.unpack_from('<H', leaf_data, 2)[0]
leaf_depth = struct.unpack_from('<H', leaf_data, 6)[0]
print(f' leaf magic=0x{leaf_magic:04x} entries={leaf_entries} depth={leaf_depth}')
if leaf_magic == 0xf30a and leaf_depth == 0:
for j in range(min(leaf_entries,4)):
off2 = 12+j*12
ee_len = struct.unpack_from('<H', leaf_data, off2+4)[0]
ee_hi = struct.unpack_from('<H', leaf_data, off2+6)[0]
ee_lo = struct.unpack_from('<I', leaf_data, off2+8)[0]
ee_start = (ee_hi<<32)|ee_lo
print(f' Extent {j}: len={ee_len} start={ee_start}')
blk_data = read_virt(ee_start*BSIZE, BSIZE)
off3 = 0
while off3 < BSIZE-8:
ino = struct.unpack_from('<I', blk_data, off3)[0]
rec_len = struct.unpack_from('<H', blk_data, off3+4)[0]
name_len= blk_data[off3+6]
ftype = blk_data[off3+7]
if rec_len < 8: break
if ino > 0 and name_len > 0:
name = blk_data[off3+8:off3+8+name_len].decode('utf-8',errors='replace')
tname = {1:'file',2:'dir',7:'link'}.get(ftype,'?')
print(f' entry: {tname} inode={ino} {name!r}')
off3 += rec_len

View File

@@ -0,0 +1,37 @@
CHUNK = 128*512
LV_START = 5120000*512
BSIZE = 4096
found_byte = 47323856896
block_num = (found_byte - LV_START) // BSIZE
print(f'Found at md0 byte: {found_byte}')
print(f'LV_START: {LV_START}')
print(f'Offset from LV: {found_byte - LV_START}')
print(f'Block number: {block_num}')
virt = block_num * BSIZE
group = virt // (5*CHUNK)
in_group = virt % (5*CHUNK)
chunk_idx = in_group // CHUNK
intra = in_group % CHUNK
print(f'chunk_group={group} chunk_idx={chunk_idx} intra={intra}')
print()
# The block was found at chunk_idx=4 but IS readable
# This means our assumption that chunk_idx=4 = unreadable is WRONG
# The data IS there, we just need to read it
# Our current translation SKIPS chunk_idx=4
# But the data exists at: LV_START + group*5*CHUNK + 4*CHUNK + intra
# We've been computing: LV_START + group*4*CHUNK + chunk_idx*CHUNK + intra
# For chunk_idx < 4 these are equivalent
# For chunk_idx = 4 we skip it entirely
# So the fix is: for chunk_idx=4, read from the 5th physical chunk
# phys = LV_START + group*5*CHUNK + 4*CHUNK + intra
phys_5chunk = LV_START + group*5*CHUNK + 4*CHUNK + intra
print(f'Physical via 5-chunk formula: {phys_5chunk}')
print(f'Found at: {found_byte}')
print(f'Match: {phys_5chunk == found_byte}')

View File

@@ -0,0 +1,48 @@
import struct
CHUNK = 128*512
LV_START = 5120000*512
BSIZE = 4096
IPG = 8192
INODE_SZ = 256
def read_inode(inode_num):
group = (inode_num-1)//IPG
idx = (inode_num-1)%IPG
it_block = 1070 + group*512
it_virt = it_block*BSIZE + idx*INODE_SZ
grp = it_virt//(5*CHUNK)
in_group = it_virt%(5*CHUNK)
chunk_idx= in_group//CHUNK
intra = in_group%CHUNK
if chunk_idx == 4:
return None, 'METADATA CHUNK'
phys = LV_START + grp*4*CHUNK + chunk_idx*CHUNK + intra
with open('/dev/sda','rb') as f:
f.seek(phys)
return f.read(INODE_SZ), 'OK'
def parse_inode(data):
mode = struct.unpack_from('<H', data, 0)[0]
size = struct.unpack_from('<I', data, 4)[0]
mtime = struct.unpack_from('<I', data, 16)[0]
links = struct.unpack_from('<H', data, 26)[0]
flags = struct.unpack_from('<I', data, 32)[0]
ext_magic = struct.unpack_from('<H', data, 40)[0]
return mode, size, mtime, links, flags, ext_magic
# Check root inode and nearby system inodes
for ino in [2, 3, 4, 5, 6, 7, 8, 11, 12]:
data, status = read_inode(ino)
if data is None:
print(f'Inode {ino:4d}: {status}')
continue
mode, size, mtime, links, flags, ext_magic = parse_inode(data)
ftype = {0x4000:'dir', 0x8000:'file', 0xa000:'link'}.get(mode&0xf000,'?')
print(f'Inode {ino:4d}: type={ftype} mode=0x{mode:04x} '
f'size={size} links={links} '
f'extent_magic=0x{ext_magic:04x} '
f'mtime={mtime} [{status}]')

View File

@@ -0,0 +1,66 @@
CHUNK = 128*512
LV_START = 5120000*512
BSIZE = 4096
# Block 5251104 falls in:
# virt_byte = 5251104 * 4096 = 21508521984
# group=65638, chunk_idx=4, intra=0
# The physical location of this metadata chunk:
group = 65638
phys_meta = LV_START + group*5*CHUNK + 4*CHUNK
print(f'Metadata chunk physical byte: {phys_meta}')
# Read the full 64KB metadata chunk
with open('/dev/md0','rb') as f:
f.seek(phys_meta)
meta = f.read(CHUNK)
nonzero = sum(1 for b in meta if b != 0)
print(f'Nonzero bytes: {nonzero}/{CHUNK}')
print()
# Earlier we found that metadata chunks store 512 bytes per sector
# of the 4 adjacent data chunks = 128 sectors * 4 chunks = 512 entries
# But what if SOME of those 512-byte slots contain filesystem data
# instead of PERC metadata?
# The /var dir block should be 4096 bytes
# Could it be stored in 8 consecutive 512-byte slots?
# Let's look for directory signatures in the metadata chunk
import struct
# Search for directory entry signature: valid inode + rec_len + name_len
print('Searching for directory entries in metadata chunk...')
for off in range(0, CHUNK-8, 4):
ino = struct.unpack_from('<I', meta, off)[0]
rec_len = struct.unpack_from('<H', meta, off+4)[0]
name_len= meta[off+6]
ftype = meta[off+7]
if (10 < ino < 500_000_000 and
8 <= rec_len <= 256 and
0 < name_len <= rec_len-8 and
ftype in (1,2,7) and
rec_len % 4 == 0):
name = meta[off+8:off+8+name_len].decode('utf-8',errors='replace')
if name.isprintable() and len(name) == name_len:
print(f' offset {off}: inode={ino} rec_len={rec_len} '
f'name={name!r} ftype={ftype}')
# Also check: does the metadata chunk contain the filesystem data
# at a fixed offset? e.g. first 4096 bytes = filesystem data block?
print()
print('First 4096 bytes as directory:')
off = 0
while off < 4096-8:
ino = struct.unpack_from('<I', meta, off)[0]
rec_len = struct.unpack_from('<H', meta, off+4)[0]
name_len= meta[off+6]
ftype = meta[off+7]
if rec_len < 8: break
if ino > 0 and name_len > 0:
name = meta[off+8:off+8+name_len].decode('utf-8',errors='replace')
tname = {1:'file',2:'dir',7:'link'}.get(ftype,'?')
print(f' {tname} inode={ino} {name!r}')
off += rec_len

View File

@@ -0,0 +1,91 @@
CHUNK = 128*512 # 64KB
LV_START = 5120000*512
BSIZE = 4096
# Block 5251104, virtual byte = 5251104 * 4096 = 21508521984
# group=65638, chunk_idx=4, intra=0
# Physical layout: each group of 5 virtual chunks occupies
# 5*CHUNK bytes on the physical disk (md0)
# group 65638 starts at:
group = 65638
group_phys_start = LV_START + group * 5 * CHUNK
print(f'Group {group} physical start: {group_phys_start}')
print()
import struct
with open('/dev/md0','rb') as f:
# Read all 5 chunks of this group
f.seek(group_phys_start)
all_chunks = f.read(5*CHUNK)
for ci in range(5):
chunk = all_chunks[ci*CHUNK:(ci+1)*CHUNK]
nonzero = sum(1 for b in chunk if b != 0)
# Try first 32 bytes as directory
ino = struct.unpack_from('<I', chunk, 0)[0]
rec = struct.unpack_from('<H', chunk, 4)[0]
nl = chunk[6]
ft = chunk[7]
print(f'Chunk {ci}: nonzero={nonzero}/{CHUNK}', end='')
if 10 < ino < 500_000_000 and 8 <= rec <= 256 and 0 < nl < 32 and ft in (1,2,7):
name = chunk[8:8+nl].decode('utf-8',errors='replace')
print(f' POSSIBLE DIR: inode={ino} name={name!r}', end='')
print()
# The data chunks (0-3) map to virtual chunks as follows:
# Our translation: virtual chunk N -> physical chunk N (for N<4)
# So physical chunks 0,1,2,3 = virtual chunks 0,1,2,3
# And physical chunk 4 = the metadata chunk we skip
# But what if our anchor points were right (FAT32 and LV)
# yet the internal mapping within the LV is different?
# What if the PERC uses a different chunk as metadata inside the LV?
# Our two anchors:
# FAT32 VBR at PERC virtual 2048 -> md0 sector 1664
# LV start at PERC virtual 6400000 -> md0 sector 5120000
# These are BEFORE the LV. Inside the LV, could the chunk order differ?
# What if inside the LV the metadata chunk is chunk 0, not chunk 4?
# Test: read chunk 0 of group 65638 as directory
chunk0 = all_chunks[0:CHUNK]
print()
print('Chunk 0 as directory:')
off = 0
while off < BSIZE-8:
ino = struct.unpack_from('<I', chunk0, off)[0]
rec_len = struct.unpack_from('<H', chunk0, off+4)[0]
name_len= chunk0[off+6]
ftype = chunk0[off+7]
if rec_len < 8: break
if ino > 0 and name_len > 0:
name = chunk0[off+8:off+8+name_len].decode('utf-8',errors='replace')
tname = {1:'file',2:'dir',7:'link'}.get(ftype,'?')
print(f' {tname} inode={ino} {name!r}')
off += rec_len
# Test each chunk as potential /var directory
print()
for ci in range(5):
chunk = all_chunks[ci*CHUNK:(ci+1)*CHUNK]
off = 0
entries = []
while off < BSIZE-8:
ino = struct.unpack_from('<I', chunk, off)[0]
rec_len = struct.unpack_from('<H', chunk, off+4)[0]
name_len= chunk[off+6]
ftype = chunk[off+7]
if rec_len < 8: break
if 10 < ino < 500_000_000 and name_len > 0 and ftype in (1,2,7):
name = chunk[off+8:off+8+name_len].decode('utf-8',errors='replace')
if name.isprintable():
entries.append((ino, name, ftype))
off += rec_len
if entries:
print(f'Chunk {ci} has {len(entries)} directory entries:')
for ino, name, ftype in entries[:5]:
tname = {1:'file',2:'dir',7:'link'}.get(ftype,'?')
print(f' {tname} inode={ino} {name!r}')

View File

@@ -0,0 +1,80 @@
CHUNK = 128*512
LV_START = 5120000*512
BSIZE = 4096
# The directory data block is at virtual block 5251104
# virt_byte = 5251104 * 4096 = 21508521984
virt_byte = 5251104 * BSIZE
group = virt_byte // (5*CHUNK)
in_group = virt_byte % (5*CHUNK)
chunk_idx = in_group // CHUNK
intra = in_group % CHUNK
print(f'Virtual byte: {virt_byte}')
print(f'Chunk group: {group}')
print(f'Chunk index: {chunk_idx}')
print(f'Intra: {intra}')
print()
# This is chunk_idx=4 - a PERC metadata chunk
# The PERC stored filesystem data here
# Physical location:
# In our translation, we skip chunk_idx=4
# But if the PERC stored data there, it's at:
# phys = LV_START + group*5*CHUNK + chunk_idx*CHUNK + intra
# (5 chunks per group including metadata chunk)
phys_with_meta = LV_START + group*5*CHUNK + chunk_idx*CHUNK + intra
print(f'Physical byte if 5-chunk groups: {phys_with_meta}')
print(f'Physical sector: {phys_with_meta//512}')
# Also try: physical = LV_START + group*4*CHUNK + ... but for chunk 4
# In our current scheme chunk 4 has no mapping
# But the data must be somewhere on disk
# The PERC presents 5 virtual chunks = 5*64KB = 320KB per group
# Of these, 4 are data and 1 is PERC metadata
# But what if it's the OPPOSITE?
# What if 4 are PERC metadata and only 1 is data? No - we verified 4/5 ratio
# What if the metadata chunk IS stored on disk but at a different offset?
# The PERC stores its own metadata in those chunk positions
# For reading filesystem data that falls there, it must use the metadata chunk
# storage differently
# Let's read what's physically at that location
# The virtual block 5251104 is in chunk group 'group' at chunk position 4
# In the physical layout (5 chunks per group):
# group X physical layout: [data0][data1][data2][data3][meta4]
# Our translation: physical chunk = group*4 + chunk_idx (skipping meta)
# So physical chunk for data chunks 0-3 = group*4 + 0,1,2,3
# And physical chunk for meta = stored AFTER the 4 data chunks = group*4+4?
# But that would mean physical layout is: data0,data1,data2,data3,meta4
# which IS 5 consecutive chunks on disk
# So the physical byte for chunk_idx=4 would be:
phys_consecutive = LV_START + group*5*CHUNK + 4*CHUNK + intra
print(f'Physical byte (consecutive 5-chunk): {phys_consecutive}')
# Read it
with open('/dev/md0','rb') as f:
f.seek(phys_consecutive)
data = f.read(512)
nonzero = sum(1 for b in data if b != 0)
print(f'Nonzero bytes: {nonzero}/512')
print(f'First 32: {data[:32].hex()}')
# Also check: maybe the metadata chunk is interleaved differently
# What if physical layout per group is: [meta0][data1][data2][data3][data4]?
# i.e. metadata chunk is FIRST, not last?
for meta_pos in range(5):
# If metadata is at position meta_pos within each group of 5 physical chunks
# Then for virtual chunk_idx=4 (the one we skip):
# virtual chunks 0,1,2,3 map to physical chunks skipping meta_pos
# virtual chunk 4 IS the metadata chunk
phys_test = LV_START + group*5*CHUNK + meta_pos*CHUNK + intra
with open('/dev/md0','rb') as f:
f.seek(phys_test)
data = f.read(64)
nonzero = sum(1 for b in data if b != 0)
print(f'meta_pos={meta_pos}: phys={phys_test} nonzero={nonzero} first8={data[:8].hex()}')

View File

@@ -0,0 +1,39 @@
CHUNK = 128*512 # 65536 bytes
BSIZE = 4096
# Working blocks:
# 9262 (root dir) -> chunk_idx=3, works
# 6300141 (volumes) -> chunk_idx=3, works
# 6299897 (ptero) -> chunk_idx=3, works
# Failing block:
# 5251104 (var) -> chunk_idx=4, fails
# All working blocks have chunk_idx=3
# The failing block has chunk_idx=4
# What if the issue isn't our translation formula
# but that block 5251104 was written when the filesystem
# was mounted through the PERC, and the PERC stored it
# in a way we can't access without the controller?
# Let's find MORE blocks that work to see if chunk_idx matters:
# Check the /boot, /home, /usr directories
import subprocess, struct
for path, inode in [('boot',2621441), ('home',2097153), ('usr',5505025),
('etc',4456449), ('tmp',786433)]:
r = subprocess.run(['istat','/dev/nbd0',str(inode)],
capture_output=True, text=True)
for line in r.stdout.splitlines():
if 'Direct Blocks' in line:
pass
elif line.strip() and all(c.isdigit() or c==' ' for c in line.strip()):
blocks = [int(x) for x in line.split() if x.strip()]
for b in blocks:
virt = b * BSIZE
group = virt // (5*CHUNK)
in_group = virt % (5*CHUNK)
chunk_idx = in_group // CHUNK
print(f'/{path} block={b} chunk_idx={chunk_idx}')

View File

@@ -0,0 +1,91 @@
CHUNK = 128*512 # 64KB
LV_START = 5120000*512
BSIZE = 4096
# Block 5251104, virtual byte = 5251104 * 4096 = 21508521984
# group=65638, chunk_idx=4, intra=0
# Physical layout: each group of 5 virtual chunks occupies
# 5*CHUNK bytes on the physical disk (md0)
# group 65638 starts at:
group = 65638
group_phys_start = LV_START + group * 5 * CHUNK
print(f'Group {group} physical start: {group_phys_start}')
print()
import struct
with open('/dev/md0','rb') as f:
# Read all 5 chunks of this group
f.seek(group_phys_start)
all_chunks = f.read(5*CHUNK)
for ci in range(5):
chunk = all_chunks[ci*CHUNK:(ci+1)*CHUNK]
nonzero = sum(1 for b in chunk if b != 0)
# Try first 32 bytes as directory
ino = struct.unpack_from('<I', chunk, 0)[0]
rec = struct.unpack_from('<H', chunk, 4)[0]
nl = chunk[6]
ft = chunk[7]
print(f'Chunk {ci}: nonzero={nonzero}/{CHUNK}', end='')
if 10 < ino < 500_000_000 and 8 <= rec <= 256 and 0 < nl < 32 and ft in (1,2,7):
name = chunk[8:8+nl].decode('utf-8',errors='replace')
print(f' POSSIBLE DIR: inode={ino} name={name!r}', end='')
print()
# The data chunks (0-3) map to virtual chunks as follows:
# Our translation: virtual chunk N -> physical chunk N (for N<4)
# So physical chunks 0,1,2,3 = virtual chunks 0,1,2,3
# And physical chunk 4 = the metadata chunk we skip
# But what if our anchor points were right (FAT32 and LV)
# yet the internal mapping within the LV is different?
# What if the PERC uses a different chunk as metadata inside the LV?
# Our two anchors:
# FAT32 VBR at PERC virtual 2048 -> md0 sector 1664
# LV start at PERC virtual 6400000 -> md0 sector 5120000
# These are BEFORE the LV. Inside the LV, could the chunk order differ?
# What if inside the LV the metadata chunk is chunk 0, not chunk 4?
# Test: read chunk 0 of group 65638 as directory
chunk0 = all_chunks[0:CHUNK]
print()
print('Chunk 0 as directory:')
off = 0
while off < BSIZE-8:
ino = struct.unpack_from('<I', chunk0, off)[0]
rec_len = struct.unpack_from('<H', chunk0, off+4)[0]
name_len= chunk0[off+6]
ftype = chunk0[off+7]
if rec_len < 8: break
if ino > 0 and name_len > 0:
name = chunk0[off+8:off+8+name_len].decode('utf-8',errors='replace')
tname = {1:'file',2:'dir',7:'link'}.get(ftype,'?')
print(f' {tname} inode={ino} {name!r}')
off += rec_len
# Test each chunk as potential /var directory
print()
for ci in range(5):
chunk = all_chunks[ci*CHUNK:(ci+1)*CHUNK]
off = 0
entries = []
while off < BSIZE-8:
ino = struct.unpack_from('<I', chunk, off)[0]
rec_len = struct.unpack_from('<H', chunk, off+4)[0]
name_len= chunk[off+6]
ftype = chunk[off+7]
if rec_len < 8: break
if 10 < ino < 500_000_000 and name_len > 0 and ftype in (1,2,7):
name = chunk[off+8:off+8+name_len].decode('utf-8',errors='replace')
if name.isprintable():
entries.append((ino, name, ftype))
off += rec_len
if entries:
print(f'Chunk {ci} has {len(entries)} directory entries:')
for ino, name, ftype in entries[:5]:
tname = {1:'file',2:'dir',7:'link'}.get(ftype,'?')
print(f' {tname} inode={ino} {name!r}')

93
misc_tools/raw_test.py Normal file
View File

@@ -0,0 +1,93 @@
import struct
CHUNK = 128*512
LV_START = 5120000*512
BSIZE = 4096
IPG = 8192
def read_virt(virt_byte, length):
result = bytearray(length)
pos = virt_byte
remaining = length
with open('/dev/md0','rb') as f:
while remaining > 0:
group = pos // (5*CHUNK)
in_group = pos % (5*CHUNK)
chunk_idx = in_group // CHUNK
intra = in_group % CHUNK
seg_len = min(CHUNK-intra, remaining)
dst_off = pos - virt_byte
if chunk_idx != 4:
phys = LV_START + group*4*CHUNK + chunk_idx*CHUNK + intra
f.seek(phys)
data = f.read(seg_len)
result[dst_off:dst_off+len(data)] = data
pos += seg_len
remaining -= seg_len
return bytes(result)
# Read GDT from md0
print('Reading GDT from /dev/md0...')
gdt_data = read_virt(BSIZE, 20*64)
for g in range(20):
entry = gdt_data[g*64:(g+1)*64]
it_lo = struct.unpack_from('<I', entry, 8)[0]
it_hi = struct.unpack_from('<I', entry, 40)[0]
it_block = (it_hi<<32)|it_lo
flags = struct.unpack_from('<H', entry, 18)[0]
print(f' Group {g:3d}: it_block={it_block:10d} '
f'formula={1070+g*512} '
f'match={it_block==1070+g*512} '
f'flags=0x{flags:04x}')
# Read /var inode
var_inode = 1310721
var_group = (var_inode-1)//IPG
var_idx = (var_inode-1)%IPG
print(f'\n/var inode {var_inode}: group={var_group} idx={var_idx}')
gdt_entry = read_virt(BSIZE + var_group*64, 64)
it_lo = struct.unpack_from('<I', gdt_entry, 8)[0]
it_hi = struct.unpack_from('<I', gdt_entry, 40)[0]
it_block = (it_hi<<32)|it_lo
print(f'GDT it_block: {it_block}')
inode_virt = it_block*BSIZE + var_idx*256
inode_data = read_virt(inode_virt, 256)
mode = struct.unpack_from('<H', inode_data, 0)[0]
size = struct.unpack_from('<I', inode_data, 4)[0]
links = struct.unpack_from('<H', inode_data, 26)[0]
mtime = struct.unpack_from('<I', inode_data, 16)[0]
ext_magic = struct.unpack_from('<H', inode_data, 40)[0]
print(f'mode=0x{mode:04x} size={size} links={links} '
f'mtime={mtime} ext_magic=0x{ext_magic:04x}')
print(f'Valid dir: {(mode&0xf000)==0x4000 and ext_magic==0xf30a}')
# If valid, list directory contents
if (mode&0xf000)==0x4000 and ext_magic==0xf30a:
print()
print('Directory contents:')
entries_raw = struct.unpack_from('<H', inode_data, 42)[0]
depth = struct.unpack_from('<H', inode_data, 46)[0]
if depth == 0:
for i in range(min(entries_raw,4)):
off = 52+i*12
ee_len = struct.unpack_from('<H', inode_data, off+4)[0]
ee_hi = struct.unpack_from('<H', inode_data, off+6)[0]
ee_lo = struct.unpack_from('<I', inode_data, off+8)[0]
blk = (ee_hi<<32)|ee_lo
blk_data = read_virt(blk*BSIZE, BSIZE)
off2 = 0
while off2 < BSIZE-8:
ino = struct.unpack_from('<I', blk_data, off2)[0]
rec_len = struct.unpack_from('<H', blk_data, off2+4)[0]
name_len= blk_data[off2+6]
ftype = blk_data[off2+7]
if rec_len < 8: break
if ino > 0 and name_len > 0:
name = blk_data[off2+8:off2+8+name_len].decode('utf-8',errors='replace')
if name not in ('.','..'):
tname = {1:'file',2:'dir',7:'link'}.get(ftype,'?')
print(f' {tname:4s} inode={ino:10d} {name!r}')
off2 += rec_len

95
misc_tools/read_root.py Normal file
View File

@@ -0,0 +1,95 @@
import struct
CHUNK = 128*512
LV_START = 5120000*512
BSIZE = 4096
IPG = 8192
INODE_SZ = 256
def phys_from_virt(virt):
grp = virt//(5*CHUNK)
in_group = virt%(5*CHUNK)
chunk_idx= in_group//CHUNK
intra = in_group%CHUNK
if chunk_idx == 4: return None
return LV_START + grp*4*CHUNK + chunk_idx*CHUNK + intra
def read_phys(phys, length):
with open('/dev/sda','rb') as f:
f.seek(phys)
return f.read(length)
def read_virt(virt, length):
phys = phys_from_virt(virt)
if phys is None: return b'\x00'*length
return read_phys(phys, length)
def read_inode(inode_num):
group = (inode_num-1)//IPG
idx = (inode_num-1)%IPG
it_block = 1070 + group*512
virt = it_block*BSIZE + idx*INODE_SZ
phys = phys_from_virt(virt)
if phys is None: return None
return read_phys(phys, INODE_SZ)
def get_extents(inode_data):
blocks = []
if struct.unpack_from('<H', inode_data, 40)[0] != 0xf30a:
return blocks
entries = struct.unpack_from('<H', inode_data, 42)[0]
depth = struct.unpack_from('<H', inode_data, 46)[0]
if depth == 0:
for i in range(min(entries,4)):
off = 52+i*12
ee_len = struct.unpack_from('<H', inode_data, off+4)[0]
ee_hi = struct.unpack_from('<H', inode_data, off+6)[0]
ee_lo = struct.unpack_from('<I', inode_data, off+8)[0]
start = (ee_hi<<32)|ee_lo
for b in range(min(ee_len,8)):
blocks.append(start+b)
return blocks
def list_dir(inode_num):
data = read_inode(inode_num)
if not data: return []
entries = []
for blk in get_extents(data):
blk_data = read_virt(blk*BSIZE, BSIZE)
off = 0
while off < BSIZE-8:
ino = struct.unpack_from('<I', blk_data, off)[0]
rec_len = struct.unpack_from('<H', blk_data, off+4)[0]
name_len= blk_data[off+6]
ftype = blk_data[off+7]
if rec_len < 8: break
if ino > 0 and name_len > 0:
name = blk_data[off+8:off+8+name_len].decode('utf-8',errors='replace')
entries.append((ino, name, ftype))
off += rec_len
return entries
# List root directory
print('Root directory (inode 2):')
for ino, name, ftype in list_dir(2):
tname = {1:'file',2:'dir',7:'link'}.get(ftype,'?')
print(f' {tname:4s} {name:20s} inode={ino}')
# Find var
print()
for ino, name, ftype in list_dir(2):
if name == 'var':
print(f'Found /var at inode {ino}')
print('Contents of /var:')
for i2, n2, f2 in list_dir(ino):
tname = {1:'file',2:'dir',7:'link'}.get(f2,'?')
print(f' {tname:4s} {n2:20s} inode={i2}')
# Find var/lib
for i2, n2, f2 in list_dir(ino):
if n2 == 'lib':
print(f'\nFound /var/lib at inode {i2}')
print('Contents of /var/lib:')
for i3, n3, f3 in list_dir(i2):
tname = {1:'file',2:'dir',7:'link'}.get(f3,'?')
print(f' {tname:4s} {n3:20s} inode={i3}')

70
misc_tools/search_var.py Normal file
View File

@@ -0,0 +1,70 @@
import struct
CHUNK = 128*512
LV_START = 5120000*512
BSIZE = 4096
# Search for /var directory block by looking for its known contents
# /var should contain: lib, log, cache, spool, tmp, www, etc.
# inode 1310721 with links=15 means 15 entries
# The directory block must contain entry for 'lib' pointing to a valid inode
# Let's search all of md0 for a 4KB block containing 'lib' as a dir entry
# where the parent context suggests it's /var
targets = [b'log', b'cache', b'spool', b'backups', b'mail']
print('Searching for /var directory block...')
print('Looking for block containing multiple /var subdirectory names')
chunk_size = 32*1024*1024
offset = LV_START # start from filesystem area
found_blocks = {}
with open('/dev/md0','rb') as f:
f.seek(0,2)
disk_size = f.tell()
f.seek(offset)
pos = offset
while pos < disk_size:
data = f.read(min(chunk_size, disk_size-pos))
if not data: break
# Look for blocks containing multiple target strings
for blk_off in range(0, len(data)-BSIZE, BSIZE):
block = data[blk_off:blk_off+BSIZE]
matches = sum(1 for t in targets if t in block)
if matches >= 2:
abs_byte = pos + blk_off
# Verify as directory block
entries = []
off = 0
while off < BSIZE-8:
ino = 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]
if rec_len < 8: break
if (10 < ino < 500_000_000 and
0 < name_len <= rec_len-8 and
ftype in (1,2,7) and
rec_len % 4 == 0):
name = block[off+8:off+8+name_len].decode('utf-8',errors='replace')
if name.isprintable():
entries.append((ino,name,ftype))
off += rec_len
if len(entries) >= 3:
abs_block = (abs_byte - LV_START) // BSIZE
print(f'Candidate at md0 byte {abs_byte} '
f'(block {abs_block}):')
for ino,name,ftype in entries[:10]:
tname={1:\"file\",2:\"dir\",7:\"link\"}.get(ftype,'?')
print(f' {tname} inode={ino} {name!r}')
print()
pos += chunk_size
if pos % (10*1024**3) == 0:
print(f' Scanned {pos//1024**3}GB...', flush=True)

54
misc_tools/stump.py Normal file
View File

@@ -0,0 +1,54 @@
import struct
CHUNK = 128*512
LV_START = 5120000*512
BSIZE = 4096
BPG = 32768
def read_virt(virt_byte, length):
result = bytearray(length)
pos = virt_byte
remaining = length
with open('/dev/md0','rb') as f:
while remaining > 0:
group = pos // (5*CHUNK)
in_group = pos % (5*CHUNK)
chunk_idx = in_group // CHUNK
intra = in_group % CHUNK
seg_len = min(CHUNK-intra, remaining)
dst_off = pos - virt_byte
if chunk_idx != 4:
phys = LV_START + group*4*CHUNK + chunk_idx*CHUNK + intra
f.seek(phys)
data = f.read(seg_len)
result[dst_off:dst_off+len(data)] = data
pos += seg_len
remaining -= seg_len
return bytes(result)
# Load GDT
NUM_GROUPS = 35728
gdt_data = read_virt(BSIZE, NUM_GROUPS*64)
def get_block_bitmap_block(group):
entry = gdt_data[group*64:(group+1)*64]
bb_lo = struct.unpack_from('<I', entry, 0)[0]
bb_hi = struct.unpack_from('<I', entry, 32)[0]
return (bb_hi<<32)|bb_lo
def is_block_allocated(block_num):
group = block_num // BPG
offset = block_num % BPG
bb_block = get_block_bitmap_block(group)
bitmap = read_virt(bb_block*BSIZE, BSIZE)
byte_idx = offset // 8
bit_idx = offset % 8
return bool(bitmap[byte_idx] & (1 << bit_idx))
for block, name in [(6300141,'volumes'), (6299897,'pterodactyl'), (5251104,'var')]:
allocated = is_block_allocated(block)
virt = block * BSIZE
in_group = virt % (5*CHUNK)
chunk_idx = in_group // CHUNK
print(f'{name:15s} block={block:8d} chunk_idx={chunk_idx} '
f'allocated={allocated}')

73
misc_tools/test1.py Normal file
View File

@@ -0,0 +1,73 @@
import struct
CHUNK = 128*512
LV_START = 5120000*512
BSIZE = 4096
IPG = 8192
INODE_SZ = 256
def phys_from_virt(virt):
grp = virt//(5*CHUNK)
in_group = virt%(5*CHUNK)
chunk_idx= in_group//CHUNK
intra = in_group%CHUNK
if chunk_idx == 4: return None
return LV_START + grp*4*CHUNK + chunk_idx*CHUNK + intra
def read_inode(inode_num):
group = (inode_num-1)//IPG
idx = (inode_num-1)%IPG
it_block = 1070 + group*512
virt = it_block*BSIZE + idx*INODE_SZ
print(f' inode {inode_num}: group={group} idx={idx}')
print(f' inode table block: {it_block}')
print(f' virtual byte: {virt}')
phys = phys_from_virt(virt)
print(f' physical byte: {phys}')
if phys is None:
print(f' IN METADATA CHUNK')
return None
with open('/dev/sda','rb') as f:
f.seek(phys)
return f.read(INODE_SZ)
# Check known good inode - volumes directory
print('=== Inode 1585918 (volumes dir) ===')
data = read_inode(1585918)
if data:
mode = struct.unpack_from('<H', data, 0)[0]
size = struct.unpack_from('<I', data, 4)[0]
links = struct.unpack_from('<H', data, 26)[0]
mtime = struct.unpack_from('<I', data, 16)[0]
ext_magic = struct.unpack_from('<H', data, 40)[0]
print(f' mode=0x{mode:04x} size={size} links={links}')
print(f' mtime={mtime} ext_magic=0x{ext_magic:04x}')
print(f' valid dir: {(mode&0xf000)==0x4000 and ext_magic==0xf30a}')
# Now check inode 2 via NBD (which applies translation correctly)
print()
print('=== Inode 2 via NBD device ===')
group = (2-1)//IPG
idx = (2-1)%IPG
it_block = 1070 + group*512
virt = it_block*BSIZE + idx*INODE_SZ
print(f' virtual byte: {virt}')
with open('/dev/nbd0','rb') as f:
f.seek(virt)
data = f.read(INODE_SZ)
mode = struct.unpack_from('<H', data, 0)[0]
links = struct.unpack_from('<H', data, 26)[0]
mtime = struct.unpack_from('<I', data, 16)[0]
ext_magic = struct.unpack_from('<H', data, 40)[0]
print(f' mode=0x{mode:04x} links={links} mtime={mtime}')
print(f' ext_magic=0x{ext_magic:04x}')
print(f' first 32 bytes: {data[:32].hex()}')
# Compare: what does debugfs think inode 2 contains?
print()
print('=== What istat says about inode 2 ===')
import subprocess
r = subprocess.run(['istat', '/dev/nbd0', '2'],
capture_output=True, text=True)
print(r.stdout[:500])
print(r.stderr[:200])

55
misc_tools/test2.py Normal file
View File

@@ -0,0 +1,55 @@
# Inode 2 is at virtual byte 4382976 via NBD
# Group 0 inode table should be at block = 4382976 // 4096 = 1069 or 1070?
virt = 4382976
BSIZE = 4096
block = virt // BSIZE
offset_in_block = virt % BSIZE
inode_idx = offset_in_block // 256
print(f'Inode 2 virtual byte: {virt}')
print(f'Block: {block}')
print(f'Offset in block: {offset_in_block}')
print(f'Inode index in block: {inode_idx}')
# istat said direct block = 9262 for root dir data
# That means the root directory data is at block 9262
# Let's read that directly via NBD
import struct
BSIZE = 4096
with open('/dev/nbd0','rb') as f:
f.seek(9262 * BSIZE)
data = f.read(BSIZE)
print()
print('Root directory data block (block 9262):')
off = 0
while off < BSIZE - 8:
ino = struct.unpack_from('<I', data, off)[0]
rec_len = struct.unpack_from('<H', data, off+4)[0]
name_len= data[off+6]
ftype = data[off+7]
if rec_len < 8: break
if ino > 0 and name_len > 0:
name = data[off+8:off+8+name_len].decode('utf-8',errors='replace')
tname = {1:'file',2:'dir',7:'link'}.get(ftype,'?')
print(f' {tname:4s} inode={ino:10d} {name!r}')
off += rec_len
# Now check: our formula says group 0 inode table is at block 1070
# But inode 2 is at virtual byte 4382976 = block 1069.something?
print()
print(f'Our formula block: 1070')
print(f'Actual block from NBD: {virt//BSIZE} (byte {virt})')
print(f'Difference: {1070 - virt//BSIZE}')
# The key question: is our GDT formula wrong?
# GDT says inode table at block 1070 for group 0
# But inode 2 (second inode) is at:
# block 1070, offset 256 bytes (one inode size)
# = byte 1070*4096 + 256 = 4382976 + 256... wait
print()
print(f'1070 * 4096 = {1070*4096}')
print(f'1070 * 4096 + 256 = {1070*4096+256}')
print(f'Actual inode 2 virt byte from NBD: {virt}')
print(f'Match: {1070*4096+256 == virt}')

75
misc_tools/test3.py Normal file
View File

@@ -0,0 +1,75 @@
import struct
BSIZE = 4096
IPG = 8192
def read_block(f, block_num):
f.seek(block_num * BSIZE)
return f.read(BSIZE)
def get_extents(inode_data):
blocks = []
if struct.unpack_from('<H', inode_data, 40)[0] != 0xf30a:
return blocks
entries = struct.unpack_from('<H', inode_data, 42)[0]
depth = struct.unpack_from('<H', inode_data, 46)[0]
if depth == 0:
for i in range(min(entries, 4)):
off = 52 + i*12
ee_len = struct.unpack_from('<H', inode_data, off+4)[0]
ee_hi = struct.unpack_from('<H', inode_data, off+6)[0]
ee_lo = struct.unpack_from('<I', inode_data, off+8)[0]
start = (ee_hi<<32)|ee_lo
for b in range(min(ee_len, 8)):
blocks.append(start + b)
return blocks
def read_inode(f, inode_num):
group = (inode_num-1)//IPG
idx = (inode_num-1)%IPG
it_block = 1070 + group*512
f.seek(it_block*BSIZE + idx*256)
return f.read(256)
def list_dir(f, inode_num):
data = read_inode(f, inode_num)
entries = []
for blk in get_extents(data):
blk_data = read_block(f, blk)
off = 0
while off < BSIZE - 8:
ino = struct.unpack_from('<I', blk_data, off)[0]
rec_len = struct.unpack_from('<H', blk_data, off+4)[0]
name_len= blk_data[off+6]
ftype = blk_data[off+7]
if rec_len < 8: break
if ino > 0 and name_len > 0:
name = blk_data[off+8:off+8+name_len].decode('utf-8',errors='replace')
if name not in ('.','..'):
entries.append((ino, name, ftype))
off += rec_len
return entries
with open('/dev/nbd0','rb') as f:
# /var = inode 1310721
print('=== /var (inode 1310721) ===')
for ino, name, ftype in list_dir(f, 1310721):
tname = {1:'file',2:'dir',7:'link'}.get(ftype,'?')
print(f' {tname:4s} {name:30s} inode={ino}')
# Find /var/lib
print()
for ino, name, ftype in list_dir(f, 1310721):
if name == 'lib':
print(f'=== /var/lib (inode {ino}) ===')
for i2, n2, f2 in list_dir(f, ino):
tname = {1:'file',2:'dir',7:'link'}.get(f2,'?')
print(f' {tname:4s} {n2:30s} inode={i2}')
# Find pterodactyl
for i2, n2, f2 in list_dir(f, ino):
if n2 == 'pterodactyl':
print(f'\n=== /var/lib/pterodactyl (inode {i2}) ===')
for i3, n3, f3 in list_dir(f, i2):
tname = {1:'file',2:'dir',7:'link'}.get(f3,'?')
print(f' {tname:4s} {n3:30s} inode={i3}')

View File

@@ -0,0 +1,50 @@
import struct
# Group 0 inode table at block 1070
# Virtual byte: 1070 * 4096 = 4,382,720
# Physical byte via chunk translation:
CHUNK = 128*512
LV_START = 5120000*512
BSIZE = 4096
it_virt = 1070 * BSIZE
group = it_virt // (5*CHUNK)
in_group = it_virt % (5*CHUNK)
chunk_idx = in_group // CHUNK
intra = in_group % CHUNK
print(f'Group 0 inode table:')
print(f' Virtual byte: {it_virt}')
print(f' Chunk group: {group}')
print(f' Chunk index: {chunk_idx} (4=metadata)')
print(f' Is metadata: {chunk_idx == 4}')
if chunk_idx != 4:
phys = LV_START + group*4*CHUNK + chunk_idx*CHUNK + intra
print(f' Physical byte: {phys}')
with open('/dev/sda','rb') as f:
f.seek(phys)
data = f.read(256)
nonzero = sum(1 for b in data if b != 0)
print(f' First inode nonzero bytes: {nonzero}/256')
print(f' First 32 bytes: {data[:32].hex()}')
else:
print(f' In PERC metadata chunk - reads as zeros')
# Check groups 0-15
print()
print('Checking which groups have inode tables in metadata chunks:')
for g in range(20):
it_block = 1070 + g*512
it_virt = it_block * BSIZE
in_group = it_virt % (5*CHUNK)
chunk_idx = in_group // CHUNK
if chunk_idx == 4:
print(f' Group {g:2d}: inode table in METADATA CHUNK (reads as zeros)')
else:
phys = LV_START + (it_virt//(5*CHUNK))*4*CHUNK + chunk_idx*CHUNK + (it_virt%CHUNK)
with open('/dev/sda','rb') as f:
f.seek(phys)
data = f.read(256)
nonzero = sum(1 for b in data if b != 0)
print(f' Group {g:2d}: DATA chunk, nonzero={nonzero}/256')

View File

@@ -0,0 +1,59 @@
CHUNK = 128*512
LV_START = 5120000*512
BSIZE = 4096
# Root dir data block = 9262
# We read this successfully, so our translation works for it
# Let's verify: block 9262 as filesystem block (offset from LV start)
# vs block 9262 as PERC virtual block
fs_virt_byte = 9262 * BSIZE # filesystem block offset from LV start
perc_virt_byte = 9262 * BSIZE # same number, different interpretation
# As filesystem block (what our NBD server does):
# The NBD server presents a virtual address space where block N = byte N*4096
# And applies the 4/5 chunk translation to convert to physical
fs_group = fs_virt_byte // (5*CHUNK)
fs_in_group = fs_virt_byte % (5*CHUNK)
fs_chunk_idx = fs_in_group // CHUNK
fs_intra = fs_in_group % CHUNK
print(f'Block 9262 as filesystem virtual (via NBD translation):')
print(f' virt_byte={fs_virt_byte}')
print(f' chunk_group={fs_group} chunk_idx={fs_chunk_idx} intra={fs_intra}')
if fs_chunk_idx != 4:
phys = LV_START + fs_group*4*CHUNK + fs_chunk_idx*CHUNK + fs_intra
print(f' physical={phys}')
with open('/dev/md0','rb') as f:
f.seek(phys)
data = f.read(64)
print(f' first 32: {data[:32].hex()}')
# We know from istat that block 9262 contains the root directory
# and we verified it has valid directory entries
# So the block numbers ARE filesystem-level, and our translation IS correct
# Now: why does block 5251104 (var dir data) fall in a metadata chunk?
# 5251104 * 4096 = 21508521984
# group = 21508521984 // (5*65536) = 65638
# in_group = 21508521984 % (5*65536) = 262144 = 4*65536
# chunk_idx = 4 -> METADATA
# This means the /var directory data genuinely falls in a PERC metadata slot
# The PERC would have stored its OWN metadata there
# and the filesystem data for block 5251104 would need to be
# at a DIFFERENT physical location
# Unless... the ext4 filesystem uses block numbers differently
# In ext4 with flex_bg, block numbers might be relative to something else
# Let's check: what block does istat say for /var?
# We need to run istat on inode 1310721
import subprocess
r = subprocess.run(['istat', '/dev/nbd0', '1310721'],
capture_output=True, text=True)
print()
print('istat output for /var (inode 1310721):')
print(r.stdout)
print(r.stderr[:200] if r.stderr else '')