◐ Off-By-One · answer catalog

shell-gpt-primary-backup-header-hybrid-partition-reconciliation

2 answer(s)shellbashshellbash

Problem class: shell-gpt-primary-backup-header-hybrid-partition-reconciliation

📦 Source in repository (JSON)

Answer 1

The complete, tested solution is saved at ~/SOLUTION.md (and the executable tool at ~/gpt-reconcile.sh). Below is the full document.


Reconstructing an authoritative GPT: stale primary CRC, displaced backup, divergent arrays

Problem class: shell-gpt-primary-backup-header-hybrid-partition-reconciliation Environment: bash 5.2+, coreutils (od, dd, stat), xxd, optional python3 for fixtures/verifier.

1. Root-cause analysis

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:

  1. Stale primary header CRC32 — 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.
  2. Backup at a non-final LBA — a bad writer left the backup mid-disk. A correct tool must find it by scanning backwards for "EFI PART", accept it for reconciliation, then relocate it to the canonical final LBA and fix MyLBA/AlternateLBA.
  3. Divergent arrays — the header with the newer SequenceNumber is authoritative. (The standard header has no sequence field, so the tool reads an optional 32-bit word at a configurable offset, default 92 when HeaderSize >= 96, and otherwise falls back to array-CRC validity → tier → primary.)
  4. Reserved/geometry words — Reserved (offset 20) must be zero; MyLBA must equal the LBA where found; FirstUsableLBA <= LastUsableLBA inside the disk; AlternateLBA cross-links the pair.
  5. 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.
  6. Unknown GUIDs / overlaps — entries are opaque 128-byte records; never re-encode them (preserves unknown GUIDs byte-exact) and never "fix" overlaps (destroys intent) — detect and report.
  7. Unrecoverable backup — trust the primary even if its CRC is stale, then regenerate a healthy backup.

2. The fix — 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"

3. Verification

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:

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.

Spot checks

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]))'

4. Design notes / why it is safe

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 & signatures

# 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"}

Answer 2

The complete, tested solution is saved at ~/SOLUTION.md (and the executable tool at ~/gpt-reconcile.sh). Below is the full document.


Reconstructing an authoritative GPT: stale primary CRC, displaced backup, divergent arrays

Problem class: shell-gpt-primary-backup-header-hybrid-partition-reconciliation Environment: bash 5.2+, coreutils (od, dd, stat), xxd, optional python3 for fixtures/verifier.

1. Root-cause analysis

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:

  1. Stale primary header CRC32 — 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.
  2. Backup at a non-final LBA — a bad writer left the backup mid-disk. A correct tool must find it by scanning backwards for "EFI PART", accept it for reconciliation, then relocate it to the canonical final LBA and fix MyLBA/AlternateLBA.
  3. Divergent arrays — the header with the newer SequenceNumber is authoritative. (The standard header has no sequence field, so the tool reads an optional 32-bit word at a configurable offset, default 92 when HeaderSize >= 96, and otherwise falls back to array-CRC validity → tier → primary.)
  4. Reserved/geometry words — Reserved (offset 20) must be zero; MyLBA must equal the LBA where found; FirstUsableLBA <= LastUsableLBA inside the disk; AlternateLBA cross-links the pair.
  5. 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.
  6. Unknown GUIDs / overlaps — entries are opaque 128-byte records; never re-encode them (preserves unknown GUIDs byte-exact) and never "fix" overlaps (destroys intent) — detect and report.
  7. Unrecoverable backup — trust the primary even if its CRC is stale, then regenerate a healthy backup.

2. The fix — 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"

3. Verification

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:

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.

Spot checks

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]))'

4. Design notes / why it is safe

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 & signatures

# 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"}
Generated from the verified corpus · MIT licensedBack to the catalog