Initial remote commit
This commit is contained in:
80
misc_tools/check_translation.py
Normal file
80
misc_tools/check_translation.py
Normal 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')
|
||||
33
misc_tools/check_volumes.py
Normal file
33
misc_tools/check_volumes.py
Normal 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
100
misc_tools/checkfifth.py
Normal 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
107
misc_tools/digging.py
Normal 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
157
misc_tools/extant_debug.py
Normal 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
|
||||
37
misc_tools/find_chunk_location.py
Normal file
37
misc_tools/find_chunk_location.py
Normal 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}')
|
||||
48
misc_tools/inode2_check.py
Normal file
48
misc_tools/inode2_check.py
Normal 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}]')
|
||||
66
misc_tools/inspect_chunk.py
Normal file
66
misc_tools/inspect_chunk.py
Normal 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
|
||||
91
misc_tools/inspect_disk.py
Normal file
91
misc_tools/inspect_disk.py
Normal 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}')
|
||||
80
misc_tools/interleave_check.py
Normal file
80
misc_tools/interleave_check.py
Normal 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()}')
|
||||
39
misc_tools/investigate_translation.py
Normal file
39
misc_tools/investigate_translation.py
Normal 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}')
|
||||
91
misc_tools/offset_check.py
Normal file
91
misc_tools/offset_check.py
Normal 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
93
misc_tools/raw_test.py
Normal 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
95
misc_tools/read_root.py
Normal 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
70
misc_tools/search_var.py
Normal 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
54
misc_tools/stump.py
Normal 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
73
misc_tools/test1.py
Normal 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
55
misc_tools/test2.py
Normal 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
75
misc_tools/test3.py
Normal 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}')
|
||||
50
misc_tools/test_corruption.py
Normal file
50
misc_tools/test_corruption.py
Normal 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')
|
||||
59
misc_tools/verify_mapping.py
Normal file
59
misc_tools/verify_mapping.py
Normal 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 '')
|
||||
|
||||
Reference in New Issue
Block a user