Problem class: shell-gpt-primary-backup-header-hybrid-partition-reconciliation
The complete, tested solution is saved at ~/SOLUTION.md (and the executable tool at ~/gpt-reconcile.sh). Below is the full document.
Problem class: shell-gpt-primary-backup-header-hybrid-partition-reconciliation
Environment: bash 5.2+, coreutils (od, dd, stat), xxd, optional python3 for fixtures/verifier.
A 4Kn (logical block = 4096 B) image carries two GPT descriptions of the same disk:
| structure | canonical LBA | what it protects |
|---|---|---|
| Protective MBR | 0 | legacy tools: one 0xEE entry covering the usable range |
| Primary GPT header | 1 | primary entry array at LBA 2 |
| Backup GPT header | last LBA | backup entry array immediately before it |
Every consistency check is over a redundant pair, so one corruption must not be fatal:
HeaderCRC32 (offset 16) no longer matches CRC32 over HeaderSize bytes with the CRC field zeroed. The header remains structurally usable (signature, geometry, MyLBA), so it is a tier-1 candidate, not garbage."EFI PART", accept it for reconciliation, then relocate it to the canonical final LBA and fix MyLBA/AlternateLBA.HeaderSize >= 96, and otherwise falls back to array-CRC validity → tier → primary.)Reserved (offset 20) must be zero; MyLBA must equal the LBA where found; FirstUsableLBA <= LastUsableLBA inside the disk; AlternateLBA cross-links the pair.EntryCount * EntrySize exceeds the describable region — the product can run past FirstUsableLBA, past the backup array, or past EOF. Clamp NumberOfPartitionEntries to what fits and recompute the array CRC.gpt-reconcile.sh#!/usr/bin/env bash
#
# gpt-reconcile.sh -- reconstruct the authoritative GPT of a (4Kn) disk image.
#
# Handles:
# * stale primary header CRC32
# * backup header at a non-final LBA
# * divergent primary/backup entry arrays -> newer SequenceNumber wins
# * reserved-zero word / MyLBA / AlternateLBA / FirstUsable / LastUsable checks
# * NumberOfPartitionEntries * SizeOfPartitionEntry larger than the region
# * unknown type GUIDs preserved byte-exact (entries are copied raw)
# * overlapping partitions (detected + reported)
# * unrecoverable backup header (primary only)
#
# Emits: a repaired image + a machine readable JSON report naming every field
# written.
#
# Requires only bash >= 4, coreutils (od/dd/stat), xxd, and bc-free arithmetic.
#
set -euo pipefail
PROG=${0##*/}
BLOCK=4096
PMBR_SPAN=usable # usable|disk
IN_IMG=""
OUT_IMG=""
OUT_REPORT=""
SCAN_BACK=256 # how many LBAs back from EOF to look for the backup
SEQ_OFFSET=92 # byte offset of the optional 32-bit SequenceNumber
VERBOSE=0
die() { printf '%s: error: %s\n' "$PROG" "$*" >&2; exit 1; }
warn() { printf '%s: warning: %s\n' "$PROG" "$*" >&2; }
note() { (( VERBOSE )) && printf '%s: %s\n' "$PROG" "$*" >&2 || true; }
usage() {
cat <<EOF
usage: $PROG [options] INPUT_IMAGE OUTPUT_IMAGE REPORT_JSON
options:
-b, --block-size N logical block size (default 4096; 4Kn)
--pmbr-span M protective MBR length: usable (default) or disk
--scan-back N LBAs below EOF to search for a stray backup (default $SCAN_BACK)
--seq-offset N byte offset of the optional SequenceNumber word in the header (default $SEQ_OFFSET)
-v, --verbose
-h, --help
EOF
exit "${1:-0}"
}
# accept --opt=value as well as --opt value
_args=()
for _a in "$@"; do
case $_a in
--*=*) _args+=("${_a%%=*}" "${_a#*=}");;
*) _args+=("$_a");;
esac
done
set -- "${_args[@]}"
while (( $# )); do
case $1 in
-b|--block-size) BLOCK=$2; shift 2;;
--pmbr-span) PMBR_SPAN=$2; shift 2;;
--scan-back) SCAN_BACK=$2; shift 2;;
--seq-offset) SEQ_OFFSET=$2; shift 2;;
-v|--verbose) VERBOSE=1; shift;;
-h|--help) usage 0;;
--) shift; break;;
-*) die "unknown option: $1";;
*) break;;
esac
done
(( $# == 3 )) || usage 1
IN_IMG=$1; OUT_IMG=$2; OUT_REPORT=$3
[[ -f $IN_IMG ]] || die "no such image: $IN_IMG"
[[ $BLOCK =~ ^[0-9]+$ ]] || die "bad block size"
case $PMBR_SPAN in usable|disk) ;; *) die "--pmbr-span must be usable or disk";; esac
# ---------------------------------------------------------------------------
# low level binary helpers
# ---------------------------------------------------------------------------
# print space separated unsigned byte values
get_bytes() { od -An -v -tu1 -j "$2" -N "$3" -- "$1" | tr -s ' \n' ' '; }
u32le() { local -a b=(); read -r -a b <<<"$(get_bytes "$1" "$2" 4)"; echo $(( (b[0]) | (b[1]<<8) | (b[2]<<16) | (b[3]<<24) )); }
u64le() {
local -a b=(); read -r -a b <<<"$(get_bytes "$1" "$2" 8)"
echo $(( (b[0]) | (b[1]<<8) | (b[2]<<16) | (b[3]<<24) \
| (b[4]<<32) | (b[5]<<40) | (b[6]<<48) | (b[7]<<56) ))
}
hexbytes() { od -An -v -tx1 -j "$2" -N "$3" -- "$1" | tr -d ' \n'; }
le32hex() { local v=$1; printf '%02x%02x%02x%02x' $((v&255)) $(((v>>8)&255)) $(((v>>16)&255)) $(((v>>24)&255)); }
le64hex() { local v=$1; printf '%s%s' "$(le32hex $((v & 0xffffffff)))" "$(le32hex $(((v>>32) & 0xffffffff)))"; }
# CRC32 (IEEE 802.3, same as gzip/zlib)
CRC_TABLE=()
build_crc_table() {
local i j c
for ((i=0;i<256;i++)); do
c=$i
for ((j=0;j<8;j++)); do
if (( c & 1 )); then c=$(( (c>>1) ^ 0xEDB88320 )); else c=$(( c>>1 )); fi
done
CRC_TABLE[$i]=$c
done
}
crc32_file() {
local crc=$((0xffffffff)) line b
while read -r -a line; do
for b in "${line[@]}"; do
crc=$(( (crc>>8) ^ CRC_TABLE[ (crc ^ b) & 0xff ] ))
done
done < <(od -An -v -tu1 -- "$1")
echo $(( crc ^ 0xffffffff ))
}
# CRC of file region, with [zrel,zrel+zlen) zeroed first
crc32_region_zero() {
local file=$1 off=$2 len=$3 zrel=$4 zlen=$5 tmp
tmp=$(mktemp)
dd if="$file" of="$tmp" bs=1 skip="$off" count="$len" status=none
if (( zlen > 0 )); then
dd if=/dev/zero of="$tmp" bs=1 seek="$zrel" count="$zlen" conv=notrunc status=none
fi
crc32_file "$tmp"
rm -f "$tmp"
}
# create a 512 byte protective MBR, preserving boot code
write_pmbr() {
local dest=$1 off=$2 disk_lba=$3 last_usable=$4 src=$5
local boot end_lba size
boot=$(hexbytes "$src" 0 446)
if [[ $PMBR_SPAN == usable ]]; then end_lba=$last_usable; else end_lba=$disk_lba; fi
(( end_lba > 0xFFFFFFFF )) && size=0xFFFFFFFF || size=$end_lba
local hex="${boot}00000200eeffffff01000000$(le32hex "$size")"
# three empty legacy entries + 0x55AA
hex+="$(printf '00%.0s' {1..48})55aa"
printf '%s' "$hex" | xxd -r -p | dd of="$dest" bs=1 seek="$off" conv=notrunc status=none
}
# Write a GPT header at byte offset `off` of `dest`.
# args: dest off my alt first last guid_hex entry_lba count size array_crc
write_gpt_header() {
local dest=$1 off=$2 my=$3 alt=$4 first=$5 last=$6 guid=$7 elba=$8 cnt=$9 esz=${10} acrc=${11}
local hdr
hdr=$(mktemp)
local base="4546492050415254$(le32hex 0x00010000)$(le32hex 92)"
local tail="$(le32hex 0)$(le64hex "$my")$(le64hex "$alt")$(le64hex "$first")$(le64hex "$last")${guid}$(le64hex "$elba")$(le32hex "$cnt")$(le32hex "$esz")$(le32hex "$acrc")"
# first pass with header CRC = 0
printf '%s' "${base}$(le32hex 0)${tail}" | xxd -r -p > "$hdr"
local hcrc; hcrc=$(crc32_file "$hdr")
printf '%s' "${base}$(le32hex "$hcrc")${tail}" | xxd -r -p > "$hdr"
dd if="$hdr" of="$dest" bs=1 seek="$off" conv=notrunc status=none
rm -f "$hdr"
echo "$hcrc"
}
# ---------------------------------------------------------------------------
# GPT interpretation
# ---------------------------------------------------------------------------
SIG_HEX=4546492050415254 # "EFI PART"
# Parse a candidate header found at LBA `lba` into H_* globals.
# Returns 0 on success, 1 if the signature is absent.
parse_header() {
local img=$1 lba=$2 off=$(( lba * BLOCK ))
[[ $(hexbytes "$img" "$off" 8) == "$SIG_HEX" ]] || return 1
H_FOUND_LBA=$lba
H_REV=$(u32le "$img" $((off+8)))
H_SIZE=$(u32le "$img" $((off+12)))
H_CRC=$(u32le "$img" $((off+16)))
H_RESERVED=$(u32le "$img" $((off+20)))
H_MYLBA=$(u64le "$img" $((off+24)))
H_ALT=$(u64le "$img" $((off+32)))
H_FIRST=$(u64le "$img" $((off+40)))
H_LAST=$(u64le "$img" $((off+48)))
H_GUID=$(hexbytes "$img" $((off+56)) 16)
H_ENTLBA=$(u64le "$img" $((off+72)))
H_ENTCNT=$(u32le "$img" $((off+80)))
H_ENTSZ=$(u32le "$img" $((off+84)))
H_ACRC=$(u32le "$img" $((off+88)))
return 0
}
# compute computed header CRC and array CRC for parsed header
compute_crcs() {
local img=$1
local off=$(( H_FOUND_LBA * BLOCK ))
H_CRC_CALC=$(crc32_region_zero "$img" "$off" "$H_SIZE" 16 4)
local abytes=$(( H_ENTCNT * H_ENTSZ ))
local aoff=$(( H_ENTLBA * BLOCK ))
local imgsz; imgsz=$(stat -c %s -- "$img")
if (( aoff >= 0 && abytes > 0 && aoff + abytes <= imgsz )); then
H_ACRC_CALC=$(crc32_region_zero "$img" "$aoff" "$abytes" 0 0)
else
H_ACRC_CALC=-1
fi
}
# ---------------------------------------------------------------------------
# main
# ---------------------------------------------------------------------------
build_crc_table
SIZE=$(stat -c %s -- "$IN_IMG")
(( SIZE % BLOCK == 0 )) || die "image size $SIZE is not a multiple of block size $BLOCK"
DISK_LBA=$(( SIZE / BLOCK )) # number of LBAs
LAST_LBA=$(( DISK_LBA - 1 ))
# --- collect candidate headers -------------------------------------------------
declare -A CAND_LBA=() # lba -> 1
for lba in 1 "$LAST_LBA"; do (( lba >= 0 && lba < DISK_LBA )) && CAND_LBA[$lba]=1; done
start=$(( LAST_LBA - SCAN_BACK )); (( start < 1 )) && start=1
for ((lba=start; lba<=LAST_LBA; lba++)); do CAND_LBA[$lba]=1; done
note "scanning $((${#CAND_LBA[@]})) candidate LBAs"
# parse each, keep primary (lba 1) and any other (prefer highest SequenceNumber)
PRIMARY_LEVEL=-1; BACKUP_LEVEL=-1
# sequence number: NOT in the standard 92 byte header. We support an optional
# sequence word parked after the standard header (offset 92 when header_size>=96).
read_seq() {
local img=$1 lba=$2 off=$(( lba * BLOCK )) hsize=$3
if (( hsize >= SEQ_OFFSET + 4 )); then
echo "$(u32le "$img" $((off+SEQ_OFFSET)))"
else
echo 0
fi
}
BEST_OTHER_LBA=-1
for lba in "${!CAND_LBA[@]}"; do
parse_header "$IN_IMG" "$lba" || continue
compute_crcs "$IN_IMG"
level=0
reason=""
# geometry sanity
geo_ok=1
(( H_FIRST >= 2 && H_FIRST <= H_LAST && H_LAST < DISK_LBA )) || geo_ok=0
(( H_ENTSZ >= 128 && (H_ENTSZ & (H_ENTSZ-1)) == 0 )) || geo_ok=0
(( H_ENTCNT > 0 )) || geo_ok=0
crc_ok=0; (( H_CRC == H_CRC_CALC )) && crc_ok=1
arr_ok=0
if (( H_ACRC_CALC != -1 && H_ACRC == H_ACRC_CALC )); then arr_ok=1; fi
res_ok=0; (( H_RESERVED == 0 )) && res_ok=1
my_ok=0; (( H_MYLBA == lba )) && my_ok=1
hsz_ok=0; (( H_SIZE >= 92 && H_SIZE <= BLOCK )) && hsz_ok=1
if (( geo_ok && crc_ok && res_ok && my_ok && hsz_ok )); then
level=2
elif (( geo_ok )); then
level=1
fi
note "LBA $lba: level=$level crc_ok=$crc_ok res_ok=$res_ok my_ok=$my_ok geo_ok=$geo_ok array_ok=$arr_ok seq=$(read_seq "$IN_IMG" "$lba" "$H_SIZE")"
if (( lba == 1 )); then
PRIMARY_LEVEL=$level
P_FOUND_LBA=$lba; P_REV=$H_REV; P_SIZE=$H_SIZE; P_CRC=$H_CRC; P_CRC_CALC=$H_CRC_CALC
P_RESERVED=$H_RESERVED; P_MYLBA=$H_MYLBA; P_ALT=$H_ALT; P_FIRST=$H_FIRST; P_LAST=$H_LAST
P_GUID=$H_GUID; P_ENTLBA=$H_ENTLBA; P_ENTCNT=$H_ENTCNT; P_ENTSZ=$H_ENTSZ
P_ACRC=$H_ACRC; P_ACRC_CALC=$H_ACRC_CALC; P_SEQ=$(read_seq "$IN_IMG" "$lba" "$H_SIZE")
P_ARR_OK=$arr_ok; P_CRC_OK=$crc_ok; P_RES_OK=$res_ok; P_MY_OK=$my_ok
else
# keep the most authoritative "other" candidate
cur_seq=$(read_seq "$IN_IMG" "$lba" "$H_SIZE")
if (( BEST_OTHER_LBA < 0 )); then
keep=1
else
keep=0
if (( level > BACKUP_LEVEL )); then keep=1
elif (( level == BACKUP_LEVEL )); then
if (( cur_seq > B_SEQ )); then keep=1
elif (( cur_seq == B_SEQ && arr_ok > B_ARR_OK )); then keep=1
fi
fi
fi
if (( keep )); then
BEST_OTHER_LBA=$lba; BACKUP_LEVEL=$level
B_FOUND_LBA=$lba; B_REV=$H_REV; B_SIZE=$H_SIZE; B_CRC=$H_CRC; B_CRC_CALC=$H_CRC_CALC
B_RESERVED=$H_RESERVED; B_MYLBA=$H_MYLBA; B_ALT=$H_ALT; B_FIRST=$H_FIRST; B_LAST=$H_LAST
B_GUID=$H_GUID; B_ENTLBA=$H_ENTLBA; B_ENTCNT=$H_ENTCNT; B_ENTSZ=$H_ENTSZ
B_ACRC=$H_ACRC; B_ACRC_CALC=$H_ACRC_CALC; B_SEQ=$cur_seq
B_ARR_OK=$arr_ok; B_CRC_OK=$crc_ok; B_RES_OK=$res_ok; B_MY_OK=$my_ok
fi
fi
done
# --- choose authority ---------------------------------------------------------
AUTH=""
if (( PRIMARY_LEVEL >= 0 && BACKUP_LEVEL >= 0 )); then
if (( PRIMARY_LEVEL > BACKUP_LEVEL )); then AUTH=P
elif (( BACKUP_LEVEL > PRIMARY_LEVEL )); then AUTH=B
elif (( P_ARR_OK > B_ARR_OK )); then AUTH=P
elif (( B_ARR_OK > P_ARR_OK )); then AUTH=B
elif (( P_SEQ >= B_SEQ )); then AUTH=P
else AUTH=B
fi
elif (( PRIMARY_LEVEL >= 0 )); then AUTH=P
elif (( BACKUP_LEVEL >= 0 )); then AUTH=B
else
die "no usable GPT header found (neither primary nor backup)"
fi
if [[ $AUTH == P ]]; then
A_LBA=$P_FOUND_LBA; A_SIZE=$P_SIZE; A_FIRST=$P_FIRST; A_LAST=$P_LAST; A_GUID=$P_GUID
A_ENTLBA=$P_ENTLBA; A_ENTCNT=$P_ENTCNT; A_ENTSZ=$P_ENTSZ; A_ACRC=$P_ACRC
A_ARR_OK=$P_ARR_OK; A_SEQ=$P_SEQ; OTHER_ARR_OK=${B_ARR_OK:-0}; OTHER_ENTLBA=${B_ENTLBA:-0}
OTHER_ENTCNT=${B_ENTCNT:-0}; OTHER_ENTSZ=${B_ENTSZ:-0}
else
A_LBA=$B_FOUND_LBA; A_SIZE=$B_SIZE; A_FIRST=$B_FIRST; A_LAST=$B_LAST; A_GUID=$B_GUID
A_ENTLBA=$B_ENTLBA; A_ENTCNT=$B_ENTCNT; A_ENTSZ=$B_ENTSZ; A_ACRC=$B_ACRC
A_ARR_OK=$B_ARR_OK; A_SEQ=$B_SEQ; OTHER_ARR_OK=${P_ARR_OK:-0}; OTHER_ENTLBA=${P_ENTLBA:-0}
OTHER_ENTCNT=${P_ENTCNT:-0}; OTHER_ENTSZ=${P_ENTSZ:-0}
fi
note "authoritative header: $AUTH (LBA $A_LBA, seq $A_SEQ)"
# which raw array to actually copy (prefer authoritative, else other valid one)
ARRAY_SRC_LBA=$A_ENTLBA
ARRAY_SRC_CNT=$A_ENTCNT
ARRAY_SRC_SZ=$A_ENTSZ
ARRAY_DEGRADED=0
if (( A_ARR_OK == 0 )); then
if (( OTHER_ARR_OK == 1 )) && (( OTHER_ENTCNT > 0 && OTHER_ENTSZ >= 128 )); then
warn "authoritative entry-array CRC is bad; using the other header's CRC-valid array"
ARRAY_SRC_LBA=$OTHER_ENTLBA; ARRAY_SRC_CNT=$OTHER_ENTCNT; ARRAY_SRC_SZ=$OTHER_ENTSZ
ARRAY_DEGRADED=1
else
warn "no CRC-valid entry array; trusting authoritative array as last resort"
ARRAY_DEGRADED=2
fi
fi
# --- clamp the array so it fits and cannot collide with the GPT structures ----
ENTCNT=$ARRAY_SRC_CNT
ENTSZ=$ARRAY_SRC_SZ
ARRAY_BYTES=$(( ENTCNT * ENTSZ ))
ARRAY_LBAS=$(( (ARRAY_BYTES + BLOCK - 1) / BLOCK ))
CLAMPED=0
PRIMARY_ENT_LBA=2
PRIMARY_ENT_END=$(( PRIMARY_ENT_LBA + ARRAY_LBAS - 1 ))
NEW_FIRST=$A_FIRST
(( NEW_FIRST < PRIMARY_ENT_END + 1 )) && NEW_FIRST=$(( PRIMARY_ENT_END + 1 ))
BACKUP_HDR_LBA=$LAST_LBA
BACKUP_ENT_LBA=$(( BACKUP_HDR_LBA - ARRAY_LBAS ))
NEW_LAST=$A_LAST
(( NEW_LAST > BACKUP_ENT_LBA - 1 )) && NEW_LAST=$(( BACKUP_ENT_LBA - 1 ))
# If the requested array cannot fit between the two GPT structures, shrink
# NumberOfPartitionEntries (the classic "EntryCount*EntrySize exceeds the
# region the header can describe" case).
while (( NEW_FIRST > NEW_LAST && ENTCNT > 0 )); do
ENTCNT=$(( ENTCNT - 1 ))
ARRAY_BYTES=$(( ENTCNT * ENTSZ ))
ARRAY_LBAS=$(( (ARRAY_BYTES + BLOCK - 1) / BLOCK ))
PRIMARY_ENT_END=$(( PRIMARY_ENT_LBA + ARRAY_LBAS - 1 ))
NEW_FIRST=$A_FIRST; (( NEW_FIRST < PRIMARY_ENT_END + 1 )) && NEW_FIRST=$(( PRIMARY_ENT_END + 1 ))
BACKUP_ENT_LBA=$(( BACKUP_HDR_LBA - ARRAY_LBAS ))
NEW_LAST=$A_LAST; (( NEW_LAST > BACKUP_ENT_LBA - 1 )) && NEW_LAST=$(( BACKUP_ENT_LBA - 1 ))
CLAMPED=1
done
(( ENTCNT > 0 )) || die "cannot fit any partition entry in the usable range"
# --- read the raw authoritative entries --------------------------------------
ENT_TMP=$(mktemp)
SRC_BYTES=$(( ARRAY_BYTES ))
SRC_OFF=$(( ARRAY_SRC_LBA * BLOCK ))
if (( SRC_OFF + SRC_BYTES > SIZE )); then
die "authoritative entry array lies outside the image"
fi
dd if="$IN_IMG" of="$ENT_TMP" bs=1 skip="$SRC_OFF" count="$SRC_BYTES" status=none
# zero pad to a block multiple
PAD=$(( ARRAY_LBAS * BLOCK - ARRAY_BYTES ))
if (( PAD > 0 )); then dd if=/dev/zero bs=1 count="$PAD" status=none >> "$ENT_TMP"; fi
ARRAY_CRC=$(crc32_file "$ENT_TMP")
# --- overlap / unknown-GUID scan ---------------------------------------------
OVERLAPS=""
declare -A USED_START=() USED_END=() USED_IDX=()
n=0
for ((i=0;i<ENTCNT;i++)); do
eoff=$(( i * ENTSZ ))
# type GUID all zero -> unused
tg=$(hexbytes "$ENT_TMP" "$eoff" 16)
[[ $tg == "00000000000000000000000000000000" ]] && continue
first=$(u64le "$ENT_TMP" $((eoff+32)))
last=$(u64le "$ENT_TMP" $((eoff+40)))
(( first == 0 && last == 0 )) && continue
name=$(hexbytes "$ENT_TMP" $((eoff+56)) 72 | tr -d '\0') # utf16le crude, informational
USED_IDX[$n]=$i; USED_START[$n]=$first; USED_END[$n]=$last
n=$((n+1))
done
for ((a=0;a<n;a++)); do
for ((b=a+1;b<n;b++)); do
if (( USED_START[$a] <= USED_END[$b] && USED_START[$b] <= USED_END[$a] )); then
OVERLAPS+="{\"a\":${USED_IDX[$a]},\"b\":${USED_IDX[$b]},\"first\":$(( USED_START[$a] > USED_START[$b] ? USED_START[$a] : USED_START[$b] )),\"last\":$(( USED_END[$a] < USED_END[$b] ? USED_END[$a] : USED_END[$b] ))},"
fi
done
done
OVERLAPS="[${OVERLAPS%,}]"
# --- craft the repaired image -------------------------------------------------
if [[ $IN_IMG != "$OUT_IMG" ]]; then cp -- "$IN_IMG" "$OUT_IMG"; fi
# zero the primary GPT structures then write entries
dd if=/dev/zero of="$OUT_IMG" bs="$BLOCK" seek=1 count=$(( 1 + ARRAY_LBAS )) conv=notrunc status=none
dd if="$ENT_TMP" of="$OUT_IMG" bs=1 seek=$(( PRIMARY_ENT_LBA * BLOCK )) conv=notrunc status=none
# backup entries + header
dd if=/dev/zero of="$OUT_IMG" bs="$BLOCK" seek="$BACKUP_ENT_LBA" count=$(( 1 + ARRAY_LBAS )) conv=notrunc status=none
dd if="$ENT_TMP" of="$OUT_IMG" bs=1 seek=$(( BACKUP_ENT_LBA * BLOCK )) conv=notrunc status=none
P_HCRC=$(write_gpt_header "$OUT_IMG" $(( 1 * BLOCK )) 1 "$BACKUP_HDR_LBA" "$NEW_FIRST" "$NEW_LAST" \
"$A_GUID" "$PRIMARY_ENT_LBA" "$ENTCNT" "$ENTSZ" "$ARRAY_CRC")
B_HCRC=$(write_gpt_header "$OUT_IMG" $(( BACKUP_HDR_LBA * BLOCK )) "$BACKUP_HDR_LBA" 1 "$NEW_FIRST" "$NEW_LAST" \
"$A_GUID" "$BACKUP_ENT_LBA" "$ENTCNT" "$ENTSZ" "$ARRAY_CRC")
# protective MBR
write_pmbr "$OUT_IMG" 0 "$LAST_LBA" "$NEW_LAST" "$IN_IMG"
if [[ $PMBR_SPAN == usable ]]; then PMBR_SIZE=$NEW_LAST; else PMBR_SIZE=$LAST_LBA; fi
(( PMBR_SIZE > 0xFFFFFFFF )) && PMBR_SIZE=0xFFFFFFFF
rm -f "$ENT_TMP"
# --- report -------------------------------------------------------------------
rjson() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'; }
AUTH_NAME="primary"; [[ $AUTH == B ]] && AUTH_NAME="backup"
P_VALID=false; (( PRIMARY_LEVEL == 2 )) && P_VALID=true
B_VALID=false; (( BACKUP_LEVEL == 2 )) && B_VALID=true
cat > "$OUT_REPORT" <<EOF
{
"tool": "gpt-reconcile",
"input": "$(rjson "$IN_IMG")",
"output": "$(rjson "$OUT_IMG")",
"block_size": $BLOCK,
"disk_lbas": $DISK_LBA,
"last_lba": $LAST_LBA,
"headers_found": {
"primary": {"lba": $P_FOUND_LBA, "valid": $P_VALID, "level": $PRIMARY_LEVEL,
"header_crc_stored": $P_CRC, "header_crc_calculated": $P_CRC_CALC,
"reserved_zero": $([[ ${P_RES_OK:-0} == 1 ]] && echo true || echo false),
"mylba_ok": $([[ ${P_MY_OK:-0} == 1 ]] && echo true || echo false),
"sequence": ${P_SEQ:-0}, "array_crc_ok": $([[ ${P_ARR_OK:-0} == 1 ]] && echo true || echo false)},
"backup": {"lba": ${B_FOUND_LBA:--1}, "valid": $B_VALID, "level": $BACKUP_LEVEL,
"header_crc_stored": ${B_CRC:--1}, "header_crc_calculated": ${B_CRC_CALC:--1},
"reserved_zero": $([[ ${B_RES_OK:-0} == 1 ]] && echo true || echo false),
"mylba_ok": $([[ ${B_MY_OK:-0} == 1 ]] && echo true || echo false),
"sequence": ${B_SEQ:-0}, "array_crc_ok": $([[ ${B_ARR_OK:-0} == 1 ]] && echo true || echo false)}
},
"authoritative": "$AUTH_NAME",
"sequence_used": $A_SEQ,
"array_source_lba": $ARRAY_SRC_LBA,
"array_degraded": $ARRAY_DEGRADED,
"entry_array_clamped": $([[ $CLAMPED == 1 ]] && echo true || echo false),
"overlaps": $OVERLAPS,
"fields_written": {
"protective_mbr": {
"lba": 0,
"boot_code_preserved_bytes": 446,
"entry0": {"status": 0, "type": "0xEE", "start_lba": 1, "size_lba": $PMBR_SIZE,
"span_mode": "$PMBR_SPAN"}
},
"primary_header": {
"lba": 1,
"signature": "EFI PART",
"revision": 65536,
"header_size": 92,
"header_crc32": $P_HCRC,
"reserved": 0,
"my_lba": 1,
"alternate_lba": $BACKUP_HDR_LBA,
"first_usable_lba": $NEW_FIRST,
"last_usable_lba": $NEW_LAST,
"disk_guid": "$A_GUID",
"partition_entry_lba": $PRIMARY_ENT_LBA,
"number_of_partition_entries": $ENTCNT,
"size_of_partition_entry": $ENTSZ,
"partition_entry_array_crc32": $ARRAY_CRC
},
"backup_header": {
"lba": $BACKUP_HDR_LBA,
"signature": "EFI PART",
"revision": 65536,
"header_size": 92,
"header_crc32": $B_HCRC,
"reserved": 0,
"my_lba": $BACKUP_HDR_LBA,
"alternate_lba": 1,
"first_usable_lba": $NEW_FIRST,
"last_usable_lba": $NEW_LAST,
"disk_guid": "$A_GUID",
"partition_entry_lba": $BACKUP_ENT_LBA,
"number_of_partition_entries": $ENTCNT,
"size_of_partition_entry": $ENTSZ,
"partition_entry_array_crc32": $ARRAY_CRC
},
"partition_entries": {
"primary_lba": $PRIMARY_ENT_LBA,
"backup_lba": $BACKUP_ENT_LBA,
"count": $ENTCNT,
"entry_size": $ENTSZ,
"bytes": $ARRAY_BYTES,
"crc32": $ARRAY_CRC,
"type_guids_preserved_byte_exact": true
}
},
"warnings": [
$(
sep=""
(( PRIMARY_LEVEL == 1 )) && { printf '%s "primary header was structurally usable but failed integrity checks; CRC/fields were recomputed"' "$sep"; sep=$',\n'; }
(( BACKUP_LEVEL < 2 )) && { printf '%s "backup header was not fully valid; a fresh backup was written"' "$sep"; sep=$',\n'; }
(( BACKUP_LEVEL < 0 )) && { printf '%s "backup header was unrecoverable; only the primary header was trusted"' "$sep"; sep=$',\n'; }
(( ARRAY_DEGRADED == 1 )) && { printf '%s "authoritative entry array failed CRC; used CRC-valid array from the other header"' "$sep"; sep=$',\n'; }
(( ARRAY_DEGRADED == 2 )) && { printf '%s "no CRC-valid entry array; entries copied as-is and re-checksummed"' "$sep"; sep=$',\n'; }
(( CLAMPED == 1 )) && { printf '%s "entry array size exceeded the describable region; NumberOfPartitionEntries was reduced"' "$sep"; sep=$',\n'; }
[[ $n -gt 0 && $OVERLAPS != "[]" ]] && { printf '%s "overlapping partitions detected (left intact; see overlaps[])"' "$sep"; sep=$',\n'; }
true
)
]
}
EOF
printf '%s: wrote %s and %s\n' "$PROG" "$OUT_IMG" "$OUT_REPORT"
verify.py re-parses the repaired image with zlib.crc32 and never uses the tool's own logic. Fixture generator and verifier are in ~/SOLUTION.md.
python3 mkfixtures.py out
for c in stale:case_stale_primary seq:case_seq bad:case_backup_bad \
oversize:case_oversize clean:case_clean; do
name=${c%%:*}; src=${c##*:}
./gpt-reconcile.sh out/$src.img out/$name.fixed.img out/$name.json
done
python3 verify.py
Observed (real run on this host, bash 5.3, no sgdisk):
[PASS] out/stale.fixed.img auth=backup prim_cnt=128 first=6 last=122 pmbr_size=122 array_lba=2
[PASS] out/seq.fixed.img auth=primary prim_cnt=128 first=6 last=122 pmbr_size=122 array_lba=2
[PASS] out/bad.fixed.img auth=primary prim_cnt=128 first=6 last=122 pmbr_size=122 array_lba=2
[PASS] out/oversize.fixed.img auth=primary prim_cnt=1984 first=64 last=64 pmbr_size=64 array_lba=2
[PASS] out/clean.fixed.img auth=primary prim_cnt=128 first=34 last=122 pmbr_size=122 array_lba=2
What each pass proves:
level=1 (stale CRC), backup level=2; authority = backup; backup relocated from LBA 100 to final LBA 127; primary array rebuilt from the backup array (unknown GUIDs preserved because entries are copied raw).level=-1); primary trusted despite stale CRC; healthy backup regenerated at LBA 127.EntryCount=2000 cannot fit; NumberOfPartitionEntries clamped, array CRC recomputed, report sets "entry_array_clamped": true.cmp returns 0).For every output the verifier enforces: both header CRC32s valid, Reserved == 0, MyLBA correct; backup header at the final LBA with cross-linked AlternateLBA; FirstUsableLBA <= LastUsableLBA with no usable range overlapping either array; primary/backup arrays byte-identical with matching CRCs; exactly one 0xEE MBR entry with StartLBA = 1 spanning the usable range, legacy slots zeroed, 55 AA present; unknown GUIDs match the authoritative source.
python3 - <<'PY'
import struct, zlib
img = open("out/stale.fixed.img","rb").read()
for name, lba in (("primary",1),("backup",len(img)//4096-1)):
off = lba*4096
size = struct.unpack_from("<I", img, off+12)[0]
stored = struct.unpack_from("<I", img, off+16)[0]
hdr = bytearray(img[off:off+size]); struct.pack_into("<I", hdr, 16, 0)
print(name, "crc_ok =", (zlib.crc32(bytes(hdr)) & 0xffffffff) == stored)
PY
python3 -c 'import struct;m=open("out/stale.fixed.img","rb").read()[:512];print("type=%#x start=%d size=%d"%(m[450],struct.unpack_from("<I",m,454)[0],struct.unpack_from("<I",m,458)[0]))'
0xEE entry. --pmbr-span usable (default) ends at LastUsableLBA; --pmbr-span disk ends at the last LBA. Boot code (bytes 0–445) is preserved; legacy slots 1–3 are zeroed.| edge case | handling |
|---|---|
| backup at non-final LBA | backward signature scan (--scan-back, default 256 LBAs); relocated to final LBA |
| both valid, arrays differ | newer SequenceNumber wins (--seq-offset, default 92) |
| newer header's array CRC bad | fall back to the other header's CRC-valid array (array_degraded) |
| no CRC-valid array | copy authority entries as-is, re-CRC (array_degraded: 2) |
EntryCount*EntrySize too large / past EOF |
clamp count until arrays fit between usable bounds |
| backup unrecoverable | trust primary (even with stale CRC), regenerate backup |
| unknown type GUID / overlap | bytes preserved, overlap reported |
# Evidence - Problem class: shell-gpt-primary-backup-header-hybrid-partition-reconciliation - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-30T04:18:28.857Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Given a 4Kn-sector disk image whose primary GPT header carries a stale header CRC32 and whose backup header sits at a non-final LBA, reconstruct the authoritative partition table: validate both headers (CRC32s, reserved-zero words, MyLBA/AlternateLBA/FirstUsable/LastUsable), reconcile divergent entry arrays by newer SequenceNumber, and re-derive the protective MBR so its single 0xEE entry spans the true usable range. Emit a repaired image plus a machine-readable repair report naming every field written. Must handle overlapping partitions, entry arrays whose EntryCount*EntrySize exceeds the header's stated size, unknown type GUIDs that must be preserved byte-exact, and a case where the backup header is unrecoverable so only the primary header may be trusted.", "environment": "bash", "language": "shell", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "shell-gpt-primary-backup-header-hybrid-partition-reconciliation", "provider": "openrouter", "solved_at": "2026-09-30T04:18:28.858Z", "version": "5.2"}The complete, tested solution is saved at ~/SOLUTION.md (and the executable tool at ~/gpt-reconcile.sh). Below is the full document.
Problem class: shell-gpt-primary-backup-header-hybrid-partition-reconciliation
Environment: bash 5.2+, coreutils (od, dd, stat), xxd, optional python3 for fixtures/verifier.
A 4Kn (logical block = 4096 B) image carries two GPT descriptions of the same disk:
| structure | canonical LBA | what it protects |
|---|---|---|
| Protective MBR | 0 | legacy tools: one 0xEE entry covering the usable range |
| Primary GPT header | 1 | primary entry array at LBA 2 |
| Backup GPT header | last LBA | backup entry array immediately before it |
Every consistency check is over a redundant pair, so one corruption must not be fatal:
HeaderCRC32 (offset 16) no longer matches CRC32 over HeaderSize bytes with the CRC field zeroed. The header remains structurally usable (signature, geometry, MyLBA), so it is a tier-1 candidate, not garbage."EFI PART", accept it for reconciliation, then relocate it to the canonical final LBA and fix MyLBA/AlternateLBA.HeaderSize >= 96, and otherwise falls back to array-CRC validity → tier → primary.)Reserved (offset 20) must be zero; MyLBA must equal the LBA where found; FirstUsableLBA <= LastUsableLBA inside the disk; AlternateLBA cross-links the pair.EntryCount * EntrySize exceeds the describable region — the product can run past FirstUsableLBA, past the backup array, or past EOF. Clamp NumberOfPartitionEntries to what fits and recompute the array CRC.gpt-reconcile.sh#!/usr/bin/env bash
#
# gpt-reconcile.sh -- reconstruct the authoritative GPT of a (4Kn) disk image.
#
# Handles:
# * stale primary header CRC32
# * backup header at a non-final LBA
# * divergent primary/backup entry arrays -> newer SequenceNumber wins
# * reserved-zero word / MyLBA / AlternateLBA / FirstUsable / LastUsable checks
# * NumberOfPartitionEntries * SizeOfPartitionEntry larger than the region
# * unknown type GUIDs preserved byte-exact (entries are copied raw)
# * overlapping partitions (detected + reported)
# * unrecoverable backup header (primary only)
#
# Emits: a repaired image + a machine readable JSON report naming every field
# written.
#
# Requires only bash >= 4, coreutils (od/dd/stat), xxd, and bc-free arithmetic.
#
set -euo pipefail
PROG=${0##*/}
BLOCK=4096
PMBR_SPAN=usable # usable|disk
IN_IMG=""
OUT_IMG=""
OUT_REPORT=""
SCAN_BACK=256 # how many LBAs back from EOF to look for the backup
SEQ_OFFSET=92 # byte offset of the optional 32-bit SequenceNumber
VERBOSE=0
die() { printf '%s: error: %s\n' "$PROG" "$*" >&2; exit 1; }
warn() { printf '%s: warning: %s\n' "$PROG" "$*" >&2; }
note() { (( VERBOSE )) && printf '%s: %s\n' "$PROG" "$*" >&2 || true; }
usage() {
cat <<EOF
usage: $PROG [options] INPUT_IMAGE OUTPUT_IMAGE REPORT_JSON
options:
-b, --block-size N logical block size (default 4096; 4Kn)
--pmbr-span M protective MBR length: usable (default) or disk
--scan-back N LBAs below EOF to search for a stray backup (default $SCAN_BACK)
--seq-offset N byte offset of the optional SequenceNumber word in the header (default $SEQ_OFFSET)
-v, --verbose
-h, --help
EOF
exit "${1:-0}"
}
# accept --opt=value as well as --opt value
_args=()
for _a in "$@"; do
case $_a in
--*=*) _args+=("${_a%%=*}" "${_a#*=}");;
*) _args+=("$_a");;
esac
done
set -- "${_args[@]}"
while (( $# )); do
case $1 in
-b|--block-size) BLOCK=$2; shift 2;;
--pmbr-span) PMBR_SPAN=$2; shift 2;;
--scan-back) SCAN_BACK=$2; shift 2;;
--seq-offset) SEQ_OFFSET=$2; shift 2;;
-v|--verbose) VERBOSE=1; shift;;
-h|--help) usage 0;;
--) shift; break;;
-*) die "unknown option: $1";;
*) break;;
esac
done
(( $# == 3 )) || usage 1
IN_IMG=$1; OUT_IMG=$2; OUT_REPORT=$3
[[ -f $IN_IMG ]] || die "no such image: $IN_IMG"
[[ $BLOCK =~ ^[0-9]+$ ]] || die "bad block size"
case $PMBR_SPAN in usable|disk) ;; *) die "--pmbr-span must be usable or disk";; esac
# ---------------------------------------------------------------------------
# low level binary helpers
# ---------------------------------------------------------------------------
# print space separated unsigned byte values
get_bytes() { od -An -v -tu1 -j "$2" -N "$3" -- "$1" | tr -s ' \n' ' '; }
u32le() { local -a b=(); read -r -a b <<<"$(get_bytes "$1" "$2" 4)"; echo $(( (b[0]) | (b[1]<<8) | (b[2]<<16) | (b[3]<<24) )); }
u64le() {
local -a b=(); read -r -a b <<<"$(get_bytes "$1" "$2" 8)"
echo $(( (b[0]) | (b[1]<<8) | (b[2]<<16) | (b[3]<<24) \
| (b[4]<<32) | (b[5]<<40) | (b[6]<<48) | (b[7]<<56) ))
}
hexbytes() { od -An -v -tx1 -j "$2" -N "$3" -- "$1" | tr -d ' \n'; }
le32hex() { local v=$1; printf '%02x%02x%02x%02x' $((v&255)) $(((v>>8)&255)) $(((v>>16)&255)) $(((v>>24)&255)); }
le64hex() { local v=$1; printf '%s%s' "$(le32hex $((v & 0xffffffff)))" "$(le32hex $(((v>>32) & 0xffffffff)))"; }
# CRC32 (IEEE 802.3, same as gzip/zlib)
CRC_TABLE=()
build_crc_table() {
local i j c
for ((i=0;i<256;i++)); do
c=$i
for ((j=0;j<8;j++)); do
if (( c & 1 )); then c=$(( (c>>1) ^ 0xEDB88320 )); else c=$(( c>>1 )); fi
done
CRC_TABLE[$i]=$c
done
}
crc32_file() {
local crc=$((0xffffffff)) line b
while read -r -a line; do
for b in "${line[@]}"; do
crc=$(( (crc>>8) ^ CRC_TABLE[ (crc ^ b) & 0xff ] ))
done
done < <(od -An -v -tu1 -- "$1")
echo $(( crc ^ 0xffffffff ))
}
# CRC of file region, with [zrel,zrel+zlen) zeroed first
crc32_region_zero() {
local file=$1 off=$2 len=$3 zrel=$4 zlen=$5 tmp
tmp=$(mktemp)
dd if="$file" of="$tmp" bs=1 skip="$off" count="$len" status=none
if (( zlen > 0 )); then
dd if=/dev/zero of="$tmp" bs=1 seek="$zrel" count="$zlen" conv=notrunc status=none
fi
crc32_file "$tmp"
rm -f "$tmp"
}
# create a 512 byte protective MBR, preserving boot code
write_pmbr() {
local dest=$1 off=$2 disk_lba=$3 last_usable=$4 src=$5
local boot end_lba size
boot=$(hexbytes "$src" 0 446)
if [[ $PMBR_SPAN == usable ]]; then end_lba=$last_usable; else end_lba=$disk_lba; fi
(( end_lba > 0xFFFFFFFF )) && size=0xFFFFFFFF || size=$end_lba
local hex="${boot}00000200eeffffff01000000$(le32hex "$size")"
# three empty legacy entries + 0x55AA
hex+="$(printf '00%.0s' {1..48})55aa"
printf '%s' "$hex" | xxd -r -p | dd of="$dest" bs=1 seek="$off" conv=notrunc status=none
}
# Write a GPT header at byte offset `off` of `dest`.
# args: dest off my alt first last guid_hex entry_lba count size array_crc
write_gpt_header() {
local dest=$1 off=$2 my=$3 alt=$4 first=$5 last=$6 guid=$7 elba=$8 cnt=$9 esz=${10} acrc=${11}
local hdr
hdr=$(mktemp)
local base="4546492050415254$(le32hex 0x00010000)$(le32hex 92)"
local tail="$(le32hex 0)$(le64hex "$my")$(le64hex "$alt")$(le64hex "$first")$(le64hex "$last")${guid}$(le64hex "$elba")$(le32hex "$cnt")$(le32hex "$esz")$(le32hex "$acrc")"
# first pass with header CRC = 0
printf '%s' "${base}$(le32hex 0)${tail}" | xxd -r -p > "$hdr"
local hcrc; hcrc=$(crc32_file "$hdr")
printf '%s' "${base}$(le32hex "$hcrc")${tail}" | xxd -r -p > "$hdr"
dd if="$hdr" of="$dest" bs=1 seek="$off" conv=notrunc status=none
rm -f "$hdr"
echo "$hcrc"
}
# ---------------------------------------------------------------------------
# GPT interpretation
# ---------------------------------------------------------------------------
SIG_HEX=4546492050415254 # "EFI PART"
# Parse a candidate header found at LBA `lba` into H_* globals.
# Returns 0 on success, 1 if the signature is absent.
parse_header() {
local img=$1 lba=$2 off=$(( lba * BLOCK ))
[[ $(hexbytes "$img" "$off" 8) == "$SIG_HEX" ]] || return 1
H_FOUND_LBA=$lba
H_REV=$(u32le "$img" $((off+8)))
H_SIZE=$(u32le "$img" $((off+12)))
H_CRC=$(u32le "$img" $((off+16)))
H_RESERVED=$(u32le "$img" $((off+20)))
H_MYLBA=$(u64le "$img" $((off+24)))
H_ALT=$(u64le "$img" $((off+32)))
H_FIRST=$(u64le "$img" $((off+40)))
H_LAST=$(u64le "$img" $((off+48)))
H_GUID=$(hexbytes "$img" $((off+56)) 16)
H_ENTLBA=$(u64le "$img" $((off+72)))
H_ENTCNT=$(u32le "$img" $((off+80)))
H_ENTSZ=$(u32le "$img" $((off+84)))
H_ACRC=$(u32le "$img" $((off+88)))
return 0
}
# compute computed header CRC and array CRC for parsed header
compute_crcs() {
local img=$1
local off=$(( H_FOUND_LBA * BLOCK ))
H_CRC_CALC=$(crc32_region_zero "$img" "$off" "$H_SIZE" 16 4)
local abytes=$(( H_ENTCNT * H_ENTSZ ))
local aoff=$(( H_ENTLBA * BLOCK ))
local imgsz; imgsz=$(stat -c %s -- "$img")
if (( aoff >= 0 && abytes > 0 && aoff + abytes <= imgsz )); then
H_ACRC_CALC=$(crc32_region_zero "$img" "$aoff" "$abytes" 0 0)
else
H_ACRC_CALC=-1
fi
}
# ---------------------------------------------------------------------------
# main
# ---------------------------------------------------------------------------
build_crc_table
SIZE=$(stat -c %s -- "$IN_IMG")
(( SIZE % BLOCK == 0 )) || die "image size $SIZE is not a multiple of block size $BLOCK"
DISK_LBA=$(( SIZE / BLOCK )) # number of LBAs
LAST_LBA=$(( DISK_LBA - 1 ))
# --- collect candidate headers -------------------------------------------------
declare -A CAND_LBA=() # lba -> 1
for lba in 1 "$LAST_LBA"; do (( lba >= 0 && lba < DISK_LBA )) && CAND_LBA[$lba]=1; done
start=$(( LAST_LBA - SCAN_BACK )); (( start < 1 )) && start=1
for ((lba=start; lba<=LAST_LBA; lba++)); do CAND_LBA[$lba]=1; done
note "scanning $((${#CAND_LBA[@]})) candidate LBAs"
# parse each, keep primary (lba 1) and any other (prefer highest SequenceNumber)
PRIMARY_LEVEL=-1; BACKUP_LEVEL=-1
# sequence number: NOT in the standard 92 byte header. We support an optional
# sequence word parked after the standard header (offset 92 when header_size>=96).
read_seq() {
local img=$1 lba=$2 off=$(( lba * BLOCK )) hsize=$3
if (( hsize >= SEQ_OFFSET + 4 )); then
echo "$(u32le "$img" $((off+SEQ_OFFSET)))"
else
echo 0
fi
}
BEST_OTHER_LBA=-1
for lba in "${!CAND_LBA[@]}"; do
parse_header "$IN_IMG" "$lba" || continue
compute_crcs "$IN_IMG"
level=0
reason=""
# geometry sanity
geo_ok=1
(( H_FIRST >= 2 && H_FIRST <= H_LAST && H_LAST < DISK_LBA )) || geo_ok=0
(( H_ENTSZ >= 128 && (H_ENTSZ & (H_ENTSZ-1)) == 0 )) || geo_ok=0
(( H_ENTCNT > 0 )) || geo_ok=0
crc_ok=0; (( H_CRC == H_CRC_CALC )) && crc_ok=1
arr_ok=0
if (( H_ACRC_CALC != -1 && H_ACRC == H_ACRC_CALC )); then arr_ok=1; fi
res_ok=0; (( H_RESERVED == 0 )) && res_ok=1
my_ok=0; (( H_MYLBA == lba )) && my_ok=1
hsz_ok=0; (( H_SIZE >= 92 && H_SIZE <= BLOCK )) && hsz_ok=1
if (( geo_ok && crc_ok && res_ok && my_ok && hsz_ok )); then
level=2
elif (( geo_ok )); then
level=1
fi
note "LBA $lba: level=$level crc_ok=$crc_ok res_ok=$res_ok my_ok=$my_ok geo_ok=$geo_ok array_ok=$arr_ok seq=$(read_seq "$IN_IMG" "$lba" "$H_SIZE")"
if (( lba == 1 )); then
PRIMARY_LEVEL=$level
P_FOUND_LBA=$lba; P_REV=$H_REV; P_SIZE=$H_SIZE; P_CRC=$H_CRC; P_CRC_CALC=$H_CRC_CALC
P_RESERVED=$H_RESERVED; P_MYLBA=$H_MYLBA; P_ALT=$H_ALT; P_FIRST=$H_FIRST; P_LAST=$H_LAST
P_GUID=$H_GUID; P_ENTLBA=$H_ENTLBA; P_ENTCNT=$H_ENTCNT; P_ENTSZ=$H_ENTSZ
P_ACRC=$H_ACRC; P_ACRC_CALC=$H_ACRC_CALC; P_SEQ=$(read_seq "$IN_IMG" "$lba" "$H_SIZE")
P_ARR_OK=$arr_ok; P_CRC_OK=$crc_ok; P_RES_OK=$res_ok; P_MY_OK=$my_ok
else
# keep the most authoritative "other" candidate
cur_seq=$(read_seq "$IN_IMG" "$lba" "$H_SIZE")
if (( BEST_OTHER_LBA < 0 )); then
keep=1
else
keep=0
if (( level > BACKUP_LEVEL )); then keep=1
elif (( level == BACKUP_LEVEL )); then
if (( cur_seq > B_SEQ )); then keep=1
elif (( cur_seq == B_SEQ && arr_ok > B_ARR_OK )); then keep=1
fi
fi
fi
if (( keep )); then
BEST_OTHER_LBA=$lba; BACKUP_LEVEL=$level
B_FOUND_LBA=$lba; B_REV=$H_REV; B_SIZE=$H_SIZE; B_CRC=$H_CRC; B_CRC_CALC=$H_CRC_CALC
B_RESERVED=$H_RESERVED; B_MYLBA=$H_MYLBA; B_ALT=$H_ALT; B_FIRST=$H_FIRST; B_LAST=$H_LAST
B_GUID=$H_GUID; B_ENTLBA=$H_ENTLBA; B_ENTCNT=$H_ENTCNT; B_ENTSZ=$H_ENTSZ
B_ACRC=$H_ACRC; B_ACRC_CALC=$H_ACRC_CALC; B_SEQ=$cur_seq
B_ARR_OK=$arr_ok; B_CRC_OK=$crc_ok; B_RES_OK=$res_ok; B_MY_OK=$my_ok
fi
fi
done
# --- choose authority ---------------------------------------------------------
AUTH=""
if (( PRIMARY_LEVEL >= 0 && BACKUP_LEVEL >= 0 )); then
if (( PRIMARY_LEVEL > BACKUP_LEVEL )); then AUTH=P
elif (( BACKUP_LEVEL > PRIMARY_LEVEL )); then AUTH=B
elif (( P_ARR_OK > B_ARR_OK )); then AUTH=P
elif (( B_ARR_OK > P_ARR_OK )); then AUTH=B
elif (( P_SEQ >= B_SEQ )); then AUTH=P
else AUTH=B
fi
elif (( PRIMARY_LEVEL >= 0 )); then AUTH=P
elif (( BACKUP_LEVEL >= 0 )); then AUTH=B
else
die "no usable GPT header found (neither primary nor backup)"
fi
if [[ $AUTH == P ]]; then
A_LBA=$P_FOUND_LBA; A_SIZE=$P_SIZE; A_FIRST=$P_FIRST; A_LAST=$P_LAST; A_GUID=$P_GUID
A_ENTLBA=$P_ENTLBA; A_ENTCNT=$P_ENTCNT; A_ENTSZ=$P_ENTSZ; A_ACRC=$P_ACRC
A_ARR_OK=$P_ARR_OK; A_SEQ=$P_SEQ; OTHER_ARR_OK=${B_ARR_OK:-0}; OTHER_ENTLBA=${B_ENTLBA:-0}
OTHER_ENTCNT=${B_ENTCNT:-0}; OTHER_ENTSZ=${B_ENTSZ:-0}
else
A_LBA=$B_FOUND_LBA; A_SIZE=$B_SIZE; A_FIRST=$B_FIRST; A_LAST=$B_LAST; A_GUID=$B_GUID
A_ENTLBA=$B_ENTLBA; A_ENTCNT=$B_ENTCNT; A_ENTSZ=$B_ENTSZ; A_ACRC=$B_ACRC
A_ARR_OK=$B_ARR_OK; A_SEQ=$B_SEQ; OTHER_ARR_OK=${P_ARR_OK:-0}; OTHER_ENTLBA=${P_ENTLBA:-0}
OTHER_ENTCNT=${P_ENTCNT:-0}; OTHER_ENTSZ=${P_ENTSZ:-0}
fi
note "authoritative header: $AUTH (LBA $A_LBA, seq $A_SEQ)"
# which raw array to actually copy (prefer authoritative, else other valid one)
ARRAY_SRC_LBA=$A_ENTLBA
ARRAY_SRC_CNT=$A_ENTCNT
ARRAY_SRC_SZ=$A_ENTSZ
ARRAY_DEGRADED=0
if (( A_ARR_OK == 0 )); then
if (( OTHER_ARR_OK == 1 )) && (( OTHER_ENTCNT > 0 && OTHER_ENTSZ >= 128 )); then
warn "authoritative entry-array CRC is bad; using the other header's CRC-valid array"
ARRAY_SRC_LBA=$OTHER_ENTLBA; ARRAY_SRC_CNT=$OTHER_ENTCNT; ARRAY_SRC_SZ=$OTHER_ENTSZ
ARRAY_DEGRADED=1
else
warn "no CRC-valid entry array; trusting authoritative array as last resort"
ARRAY_DEGRADED=2
fi
fi
# --- clamp the array so it fits and cannot collide with the GPT structures ----
ENTCNT=$ARRAY_SRC_CNT
ENTSZ=$ARRAY_SRC_SZ
ARRAY_BYTES=$(( ENTCNT * ENTSZ ))
ARRAY_LBAS=$(( (ARRAY_BYTES + BLOCK - 1) / BLOCK ))
CLAMPED=0
PRIMARY_ENT_LBA=2
PRIMARY_ENT_END=$(( PRIMARY_ENT_LBA + ARRAY_LBAS - 1 ))
NEW_FIRST=$A_FIRST
(( NEW_FIRST < PRIMARY_ENT_END + 1 )) && NEW_FIRST=$(( PRIMARY_ENT_END + 1 ))
BACKUP_HDR_LBA=$LAST_LBA
BACKUP_ENT_LBA=$(( BACKUP_HDR_LBA - ARRAY_LBAS ))
NEW_LAST=$A_LAST
(( NEW_LAST > BACKUP_ENT_LBA - 1 )) && NEW_LAST=$(( BACKUP_ENT_LBA - 1 ))
# If the requested array cannot fit between the two GPT structures, shrink
# NumberOfPartitionEntries (the classic "EntryCount*EntrySize exceeds the
# region the header can describe" case).
while (( NEW_FIRST > NEW_LAST && ENTCNT > 0 )); do
ENTCNT=$(( ENTCNT - 1 ))
ARRAY_BYTES=$(( ENTCNT * ENTSZ ))
ARRAY_LBAS=$(( (ARRAY_BYTES + BLOCK - 1) / BLOCK ))
PRIMARY_ENT_END=$(( PRIMARY_ENT_LBA + ARRAY_LBAS - 1 ))
NEW_FIRST=$A_FIRST; (( NEW_FIRST < PRIMARY_ENT_END + 1 )) && NEW_FIRST=$(( PRIMARY_ENT_END + 1 ))
BACKUP_ENT_LBA=$(( BACKUP_HDR_LBA - ARRAY_LBAS ))
NEW_LAST=$A_LAST; (( NEW_LAST > BACKUP_ENT_LBA - 1 )) && NEW_LAST=$(( BACKUP_ENT_LBA - 1 ))
CLAMPED=1
done
(( ENTCNT > 0 )) || die "cannot fit any partition entry in the usable range"
# --- read the raw authoritative entries --------------------------------------
ENT_TMP=$(mktemp)
SRC_BYTES=$(( ARRAY_BYTES ))
SRC_OFF=$(( ARRAY_SRC_LBA * BLOCK ))
if (( SRC_OFF + SRC_BYTES > SIZE )); then
die "authoritative entry array lies outside the image"
fi
dd if="$IN_IMG" of="$ENT_TMP" bs=1 skip="$SRC_OFF" count="$SRC_BYTES" status=none
# zero pad to a block multiple
PAD=$(( ARRAY_LBAS * BLOCK - ARRAY_BYTES ))
if (( PAD > 0 )); then dd if=/dev/zero bs=1 count="$PAD" status=none >> "$ENT_TMP"; fi
ARRAY_CRC=$(crc32_file "$ENT_TMP")
# --- overlap / unknown-GUID scan ---------------------------------------------
OVERLAPS=""
declare -A USED_START=() USED_END=() USED_IDX=()
n=0
for ((i=0;i<ENTCNT;i++)); do
eoff=$(( i * ENTSZ ))
# type GUID all zero -> unused
tg=$(hexbytes "$ENT_TMP" "$eoff" 16)
[[ $tg == "00000000000000000000000000000000" ]] && continue
first=$(u64le "$ENT_TMP" $((eoff+32)))
last=$(u64le "$ENT_TMP" $((eoff+40)))
(( first == 0 && last == 0 )) && continue
name=$(hexbytes "$ENT_TMP" $((eoff+56)) 72 | tr -d '\0') # utf16le crude, informational
USED_IDX[$n]=$i; USED_START[$n]=$first; USED_END[$n]=$last
n=$((n+1))
done
for ((a=0;a<n;a++)); do
for ((b=a+1;b<n;b++)); do
if (( USED_START[$a] <= USED_END[$b] && USED_START[$b] <= USED_END[$a] )); then
OVERLAPS+="{\"a\":${USED_IDX[$a]},\"b\":${USED_IDX[$b]},\"first\":$(( USED_START[$a] > USED_START[$b] ? USED_START[$a] : USED_START[$b] )),\"last\":$(( USED_END[$a] < USED_END[$b] ? USED_END[$a] : USED_END[$b] ))},"
fi
done
done
OVERLAPS="[${OVERLAPS%,}]"
# --- craft the repaired image -------------------------------------------------
if [[ $IN_IMG != "$OUT_IMG" ]]; then cp -- "$IN_IMG" "$OUT_IMG"; fi
# zero the primary GPT structures then write entries
dd if=/dev/zero of="$OUT_IMG" bs="$BLOCK" seek=1 count=$(( 1 + ARRAY_LBAS )) conv=notrunc status=none
dd if="$ENT_TMP" of="$OUT_IMG" bs=1 seek=$(( PRIMARY_ENT_LBA * BLOCK )) conv=notrunc status=none
# backup entries + header
dd if=/dev/zero of="$OUT_IMG" bs="$BLOCK" seek="$BACKUP_ENT_LBA" count=$(( 1 + ARRAY_LBAS )) conv=notrunc status=none
dd if="$ENT_TMP" of="$OUT_IMG" bs=1 seek=$(( BACKUP_ENT_LBA * BLOCK )) conv=notrunc status=none
P_HCRC=$(write_gpt_header "$OUT_IMG" $(( 1 * BLOCK )) 1 "$BACKUP_HDR_LBA" "$NEW_FIRST" "$NEW_LAST" \
"$A_GUID" "$PRIMARY_ENT_LBA" "$ENTCNT" "$ENTSZ" "$ARRAY_CRC")
B_HCRC=$(write_gpt_header "$OUT_IMG" $(( BACKUP_HDR_LBA * BLOCK )) "$BACKUP_HDR_LBA" 1 "$NEW_FIRST" "$NEW_LAST" \
"$A_GUID" "$BACKUP_ENT_LBA" "$ENTCNT" "$ENTSZ" "$ARRAY_CRC")
# protective MBR
write_pmbr "$OUT_IMG" 0 "$LAST_LBA" "$NEW_LAST" "$IN_IMG"
if [[ $PMBR_SPAN == usable ]]; then PMBR_SIZE=$NEW_LAST; else PMBR_SIZE=$LAST_LBA; fi
(( PMBR_SIZE > 0xFFFFFFFF )) && PMBR_SIZE=0xFFFFFFFF
rm -f "$ENT_TMP"
# --- report -------------------------------------------------------------------
rjson() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'; }
AUTH_NAME="primary"; [[ $AUTH == B ]] && AUTH_NAME="backup"
P_VALID=false; (( PRIMARY_LEVEL == 2 )) && P_VALID=true
B_VALID=false; (( BACKUP_LEVEL == 2 )) && B_VALID=true
cat > "$OUT_REPORT" <<EOF
{
"tool": "gpt-reconcile",
"input": "$(rjson "$IN_IMG")",
"output": "$(rjson "$OUT_IMG")",
"block_size": $BLOCK,
"disk_lbas": $DISK_LBA,
"last_lba": $LAST_LBA,
"headers_found": {
"primary": {"lba": $P_FOUND_LBA, "valid": $P_VALID, "level": $PRIMARY_LEVEL,
"header_crc_stored": $P_CRC, "header_crc_calculated": $P_CRC_CALC,
"reserved_zero": $([[ ${P_RES_OK:-0} == 1 ]] && echo true || echo false),
"mylba_ok": $([[ ${P_MY_OK:-0} == 1 ]] && echo true || echo false),
"sequence": ${P_SEQ:-0}, "array_crc_ok": $([[ ${P_ARR_OK:-0} == 1 ]] && echo true || echo false)},
"backup": {"lba": ${B_FOUND_LBA:--1}, "valid": $B_VALID, "level": $BACKUP_LEVEL,
"header_crc_stored": ${B_CRC:--1}, "header_crc_calculated": ${B_CRC_CALC:--1},
"reserved_zero": $([[ ${B_RES_OK:-0} == 1 ]] && echo true || echo false),
"mylba_ok": $([[ ${B_MY_OK:-0} == 1 ]] && echo true || echo false),
"sequence": ${B_SEQ:-0}, "array_crc_ok": $([[ ${B_ARR_OK:-0} == 1 ]] && echo true || echo false)}
},
"authoritative": "$AUTH_NAME",
"sequence_used": $A_SEQ,
"array_source_lba": $ARRAY_SRC_LBA,
"array_degraded": $ARRAY_DEGRADED,
"entry_array_clamped": $([[ $CLAMPED == 1 ]] && echo true || echo false),
"overlaps": $OVERLAPS,
"fields_written": {
"protective_mbr": {
"lba": 0,
"boot_code_preserved_bytes": 446,
"entry0": {"status": 0, "type": "0xEE", "start_lba": 1, "size_lba": $PMBR_SIZE,
"span_mode": "$PMBR_SPAN"}
},
"primary_header": {
"lba": 1,
"signature": "EFI PART",
"revision": 65536,
"header_size": 92,
"header_crc32": $P_HCRC,
"reserved": 0,
"my_lba": 1,
"alternate_lba": $BACKUP_HDR_LBA,
"first_usable_lba": $NEW_FIRST,
"last_usable_lba": $NEW_LAST,
"disk_guid": "$A_GUID",
"partition_entry_lba": $PRIMARY_ENT_LBA,
"number_of_partition_entries": $ENTCNT,
"size_of_partition_entry": $ENTSZ,
"partition_entry_array_crc32": $ARRAY_CRC
},
"backup_header": {
"lba": $BACKUP_HDR_LBA,
"signature": "EFI PART",
"revision": 65536,
"header_size": 92,
"header_crc32": $B_HCRC,
"reserved": 0,
"my_lba": $BACKUP_HDR_LBA,
"alternate_lba": 1,
"first_usable_lba": $NEW_FIRST,
"last_usable_lba": $NEW_LAST,
"disk_guid": "$A_GUID",
"partition_entry_lba": $BACKUP_ENT_LBA,
"number_of_partition_entries": $ENTCNT,
"size_of_partition_entry": $ENTSZ,
"partition_entry_array_crc32": $ARRAY_CRC
},
"partition_entries": {
"primary_lba": $PRIMARY_ENT_LBA,
"backup_lba": $BACKUP_ENT_LBA,
"count": $ENTCNT,
"entry_size": $ENTSZ,
"bytes": $ARRAY_BYTES,
"crc32": $ARRAY_CRC,
"type_guids_preserved_byte_exact": true
}
},
"warnings": [
$(
sep=""
(( PRIMARY_LEVEL == 1 )) && { printf '%s "primary header was structurally usable but failed integrity checks; CRC/fields were recomputed"' "$sep"; sep=$',\n'; }
(( BACKUP_LEVEL < 2 )) && { printf '%s "backup header was not fully valid; a fresh backup was written"' "$sep"; sep=$',\n'; }
(( BACKUP_LEVEL < 0 )) && { printf '%s "backup header was unrecoverable; only the primary header was trusted"' "$sep"; sep=$',\n'; }
(( ARRAY_DEGRADED == 1 )) && { printf '%s "authoritative entry array failed CRC; used CRC-valid array from the other header"' "$sep"; sep=$',\n'; }
(( ARRAY_DEGRADED == 2 )) && { printf '%s "no CRC-valid entry array; entries copied as-is and re-checksummed"' "$sep"; sep=$',\n'; }
(( CLAMPED == 1 )) && { printf '%s "entry array size exceeded the describable region; NumberOfPartitionEntries was reduced"' "$sep"; sep=$',\n'; }
[[ $n -gt 0 && $OVERLAPS != "[]" ]] && { printf '%s "overlapping partitions detected (left intact; see overlaps[])"' "$sep"; sep=$',\n'; }
true
)
]
}
EOF
printf '%s: wrote %s and %s\n' "$PROG" "$OUT_IMG" "$OUT_REPORT"
verify.py re-parses the repaired image with zlib.crc32 and never uses the tool's own logic. Fixture generator and verifier are in ~/SOLUTION.md.
python3 mkfixtures.py out
for c in stale:case_stale_primary seq:case_seq bad:case_backup_bad \
oversize:case_oversize clean:case_clean; do
name=${c%%:*}; src=${c##*:}
./gpt-reconcile.sh out/$src.img out/$name.fixed.img out/$name.json
done
python3 verify.py
Observed (real run on this host, bash 5.3, no sgdisk):
[PASS] out/stale.fixed.img auth=backup prim_cnt=128 first=6 last=122 pmbr_size=122 array_lba=2
[PASS] out/seq.fixed.img auth=primary prim_cnt=128 first=6 last=122 pmbr_size=122 array_lba=2
[PASS] out/bad.fixed.img auth=primary prim_cnt=128 first=6 last=122 pmbr_size=122 array_lba=2
[PASS] out/oversize.fixed.img auth=primary prim_cnt=1984 first=64 last=64 pmbr_size=64 array_lba=2
[PASS] out/clean.fixed.img auth=primary prim_cnt=128 first=34 last=122 pmbr_size=122 array_lba=2
What each pass proves:
level=1 (stale CRC), backup level=2; authority = backup; backup relocated from LBA 100 to final LBA 127; primary array rebuilt from the backup array (unknown GUIDs preserved because entries are copied raw).level=-1); primary trusted despite stale CRC; healthy backup regenerated at LBA 127.EntryCount=2000 cannot fit; NumberOfPartitionEntries clamped, array CRC recomputed, report sets "entry_array_clamped": true.cmp returns 0).For every output the verifier enforces: both header CRC32s valid, Reserved == 0, MyLBA correct; backup header at the final LBA with cross-linked AlternateLBA; FirstUsableLBA <= LastUsableLBA with no usable range overlapping either array; primary/backup arrays byte-identical with matching CRCs; exactly one 0xEE MBR entry with StartLBA = 1 spanning the usable range, legacy slots zeroed, 55 AA present; unknown GUIDs match the authoritative source.
python3 - <<'PY'
import struct, zlib
img = open("out/stale.fixed.img","rb").read()
for name, lba in (("primary",1),("backup",len(img)//4096-1)):
off = lba*4096
size = struct.unpack_from("<I", img, off+12)[0]
stored = struct.unpack_from("<I", img, off+16)[0]
hdr = bytearray(img[off:off+size]); struct.pack_into("<I", hdr, 16, 0)
print(name, "crc_ok =", (zlib.crc32(bytes(hdr)) & 0xffffffff) == stored)
PY
python3 -c 'import struct;m=open("out/stale.fixed.img","rb").read()[:512];print("type=%#x start=%d size=%d"%(m[450],struct.unpack_from("<I",m,454)[0],struct.unpack_from("<I",m,458)[0]))'
0xEE entry. --pmbr-span usable (default) ends at LastUsableLBA; --pmbr-span disk ends at the last LBA. Boot code (bytes 0–445) is preserved; legacy slots 1–3 are zeroed.| edge case | handling |
|---|---|
| backup at non-final LBA | backward signature scan (--scan-back, default 256 LBAs); relocated to final LBA |
| both valid, arrays differ | newer SequenceNumber wins (--seq-offset, default 92) |
| newer header's array CRC bad | fall back to the other header's CRC-valid array (array_degraded) |
| no CRC-valid array | copy authority entries as-is, re-CRC (array_degraded: 2) |
EntryCount*EntrySize too large / past EOF |
clamp count until arrays fit between usable bounds |
| backup unrecoverable | trust primary (even with stale CRC), regenerate backup |
| unknown type GUID / overlap | bytes preserved, overlap reported |
# Evidence - Problem class: shell-gpt-primary-backup-header-hybrid-partition-reconciliation - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-30T04:18:28.857Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Given a 4Kn-sector disk image whose primary GPT header carries a stale header CRC32 and whose backup header sits at a non-final LBA, reconstruct the authoritative partition table: validate both headers (CRC32s, reserved-zero words, MyLBA/AlternateLBA/FirstUsable/LastUsable), reconcile divergent entry arrays by newer SequenceNumber, and re-derive the protective MBR so its single 0xEE entry spans the true usable range. Emit a repaired image plus a machine-readable repair report naming every field written. Must handle overlapping partitions, entry arrays whose EntryCount*EntrySize exceeds the header's stated size, unknown type GUIDs that must be preserved byte-exact, and a case where the backup header is unrecoverable so only the primary header may be trusted.", "environment": "bash", "language": "shell", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "shell-gpt-primary-backup-header-hybrid-partition-reconciliation", "provider": "openrouter", "solved_at": "2026-09-30T04:18:28.858Z", "version": "5.2"}