diff --git a/build_merged_v2.py b/build_merged_v2.py new file mode 100644 index 0000000..eefc8f6 --- /dev/null +++ b/build_merged_v2.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +""" +Build merged GDT using corrected 5-chunk translation formula. + +With the old formula (group*4*CHUNK), chunk_idx=4 blocks were skipped and +every address past group 0 was wrong. The correct formula is group*5*CHUNK +for all chunk indices 0-4. + +Since all chunks are now readable, we use the primary GDT for everything and +fall back to the backup (group 1) only when a primary entry reads as all-zeros +(indicating it genuinely landed in a damaged sector). +""" +import struct + +CHUNK = 128 * 512 # 64 KB +LV_START = 5120000 * 512 +BSIZE = 4096 +BPG = 32768 +GDT_ENTRY = 64 +NUM_GROUPS = 35728 + + +def raw_read(virt_offset, length): + """Correct PERC H710 translation — 5-chunk stride, all chunks readable.""" + result = bytearray(length) + pos = virt_offset + 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_offset + phys = LV_START + group * 5 * 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) + + +print('Building merged GDT (v2 — corrected 5-chunk formula)...') +primary_start = BSIZE # block 1 +backup_start = (1 * BPG + 1) * BSIZE # group 1 backup SB location + +primary_gdt = raw_read(primary_start, NUM_GROUPS * GDT_ENTRY) +backup_gdt = raw_read(backup_start, NUM_GROUPS * GDT_ENTRY) + +merged = bytearray(NUM_GROUPS * GDT_ENTRY) +primary_used = 0 +backup_used = 0 +zero_both = 0 + +for g in range(NUM_GROUPS): + src_off = g * GDT_ENTRY + prim_e = primary_gdt[src_off:src_off + GDT_ENTRY] + back_e = backup_gdt[src_off:src_off + GDT_ENTRY] + + if any(prim_e): + merged[src_off:src_off + GDT_ENTRY] = prim_e + primary_used += 1 + elif any(back_e): + merged[src_off:src_off + GDT_ENTRY] = back_e + backup_used += 1 + else: + zero_both += 1 + +print(f' From primary GDT : {primary_used}') +print(f' From backup GDT : {backup_used}') +print(f' Both zero (lost) : {zero_both}') + +if zero_both > 0: + print(f' WARNING: {zero_both} group descriptors could not be recovered') + +print() +print('Sample entries:') +for g in [0, 1, 13, 100, 1000, 35000, 35727]: + e = merged[g * GDT_ENTRY:(g + 1) * GDT_ENTRY] + bb = struct.unpack_from(' 0: + group = pos // (5 * CHUNK_BYTES) + in_group = pos % (5 * CHUNK_BYTES) + chunk_idx = in_group // CHUNK_BYTES + intra = in_group % CHUNK_BYTES + seg_len = min(CHUNK_BYTES - intra, remaining) + dst_off = pos - virt_offset + phys = (LV_PHYS_START + + group * 5 * CHUNK_BYTES + + chunk_idx * CHUNK_BYTES + + intra) + f.seek(phys) + data = f.read(seg_len) + result[dst_off:dst_off + len(data)] = data + pos += seg_len + remaining -= seg_len + return result + + +def read_virtual(virt_offset, length): + """Read with merged GDT overlay over the primary GDT region only.""" + data = raw_read(virt_offset, length) + req_end = virt_offset + length + + if virt_offset < GDT_VIRT_END and req_end > GDT_VIRT_START: + ol_start = max(virt_offset, GDT_VIRT_START) + ol_end = min(req_end, GDT_VIRT_END) + src_off = ol_start - GDT_VIRT_START + dst_off = ol_start - virt_offset + n = ol_end - ol_start + data[dst_off:dst_off + n] = MERGED_GDT_DATA[src_off:src_off + n] + + return bytes(data) + + +def recv_all(conn, n): + buf = b'' + while len(buf) < n: + d = conn.recv(n - len(buf)) + if not d: + raise ConnectionError('disconnected') + buf += d + return buf + + +def send_reply(conn, opt, rtype, data=b''): + conn.sendall(struct.pack('>Q', REPLYMAGIC)) + conn.sendall(struct.pack('>I', opt)) + conn.sendall(struct.pack('>I', rtype)) + conn.sendall(struct.pack('>I', len(data))) + if data: + conn.sendall(data) + + +def handle_client(conn, addr): + print(f'[nbd] {addr} connected') + try: + conn.sendall(struct.pack('>Q', NBDMAGIC)) + conn.sendall(struct.pack('>Q', IHAVEOPT)) + conn.sendall(struct.pack('>H', 0x0003)) + recv_all(conn, 4) + + while True: + hdr = recv_all(conn, 16) + _, opt, opt_len = struct.unpack('>QII', hdr) + opt_data = recv_all(conn, opt_len) if opt_len else b'' + + if opt == NBD_OPT_EXPORT_NAME: + conn.sendall(struct.pack('>Q', VIRT_SIZE)) + conn.sendall(struct.pack('>H', TX_FLAGS)) + break + elif opt == NBD_OPT_GO: + info = struct.pack('>HQH', NBD_INFO_EXPORT, VIRT_SIZE, TX_FLAGS) + send_reply(conn, opt, NBD_REP_INFO, info) + send_reply(conn, opt, NBD_REP_ACK) + break + elif opt == NBD_OPT_LIST: + send_reply(conn, opt, NBD_REP_SERVER, struct.pack('>I', 0)) + send_reply(conn, opt, NBD_REP_ACK) + elif opt == NBD_OPT_ABORT: + send_reply(conn, opt, NBD_REP_ACK) + return + else: + send_reply(conn, opt, NBD_REP_ERR_UNSUP) + + print(f'[nbd] {addr} transmission') + while True: + hdr = recv_all(conn, 28) + magic, flags, cmd, handle, offset, length = \ + struct.unpack('>IHHQQI', hdr) + if magic != NBD_REQUEST_MAGIC: + return + + if cmd == NBD_CMD_READ: + try: + payload = read_virtual(offset, length) + except Exception as e: + print(f'[nbd] read error {offset}+{length}: {e}') + payload = b'\x00' * length + conn.sendall(struct.pack('>IIQ', NBD_REPLY_MAGIC, 0, handle)) + conn.sendall(payload) + elif cmd == NBD_CMD_FLUSH: + conn.sendall(struct.pack('>IIQ', NBD_REPLY_MAGIC, 0, handle)) + elif cmd == NBD_CMD_DISC: + print(f'[nbd] {addr} disconnected') + return + else: + conn.sendall(struct.pack('>IIQ', NBD_REPLY_MAGIC, 1, handle)) + + except (ConnectionError, BrokenPipeError, ConnectionResetError): + print(f'[nbd] {addr} dropped') + except Exception as e: + print(f'[nbd] {addr} error: {e}') + import traceback; traceback.print_exc() + finally: + conn.close() + + +def main(): + print('PERC H710 recovery NBD server v10') + print(f' device : {DEV}') + print(f' lv_start : byte {LV_PHYS_START}') + print(f' virt_size : {VIRT_SIZE // 1024**3} GB') + print(f' GDT region : bytes {GDT_VIRT_START}-{GDT_VIRT_END}') + print(f' translation: group*5*CHUNK (all 5 chunks are filesystem data)') + print() + + srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + srv.bind(('127.0.0.1', 10809)) + srv.listen(5) + print('Listening on 127.0.0.1:10809') + print(' nbd-client 127.0.0.1 10809 /dev/nbd0 -N ""') + print() + + while True: + conn, addr = srv.accept() + threading.Thread(target=handle_client, args=(conn, addr), + daemon=True).start() + + +if __name__ == '__main__': + main() diff --git a/test/nbd_server_v9.py b/nbd_server_v9.py similarity index 100% rename from test/nbd_server_v9.py rename to nbd_server_v9.py