From d437df921a273ac319add667bc8f7c8ea8ae32af Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Thu, 24 Sep 2026 16:01:27 -0700 Subject: [PATCH] ac-os: verify the GPT after sgdisk and refuse stale pulls flash-mac.sh: macOS DiskArbitration re-probes the stick the moment the zap lands and can hold it while sgdisk writes the primary header, leaving a torn table (backup OK, primary ERROR) that only surfaced as "partition did not appear". sgdisk output is now visible, the table is verified after writing and retried up to three times. The wifi preset log line counts the list instead of saying 6. ac-os pull: one transient HEAD failure on the CDN skipped the universal-files block and flashed the cached copies, a new kernel over the previous release's initramfs with no kpart, silently. HEAD checks now retry five times and a still-unreachable CDN refuses to flash; only a genuine 404 (a release without those files) falls through to the legacy path. Same for the Chromebook kpart, which would otherwise silently drop KERN-A. --- fedac/native/ac-os | 31 +++++++++++++- fedac/native/scripts/flash-mac.sh | 67 ++++++++++++++++++++++--------- 2 files changed, 77 insertions(+), 21 deletions(-) diff --git a/fedac/native/ac-os b/fedac/native/ac-os index a05cab86fa..f93f55cbd1 100755 --- a/fedac/native/ac-os +++ b/fedac/native/ac-os @@ -1728,7 +1728,29 @@ pull_ota() { local INITRAMFS_URL="${CDN_BASE}/native-notepat-latest.initramfs.cpio.gz" local PULLED_SLIM="${PULL_DIR}/vmlinuz-slim" local PULLED_INITRAMFS="${PULL_DIR}/initramfs.cpio.gz" - if curl -sf --head "${SLIM_URL}" >/dev/null 2>&1; then + # One transient HEAD failure here used to skip this whole block and flash + # whatever was cached — a new kernel over a stale initramfs, silently + # (2026-09-24). Retry, and refuse to flash a stale cache if the CDN still + # won't answer; only a release that genuinely has no universal files + # (HTTP error, curl rc 22) falls through to the legacy kernel-only path. + cdn_head() { # $1=url $2=label → 0 published, 22 not published, else unreachable + local rc=1 _try + for _try in 1 2 3 4 5; do + curl -sf --head --max-time 20 "$1" >/dev/null 2>&1 && return 0 + rc=$? + [ "${rc}" -eq 22 ] && return 22 + log "CDN HEAD for $2 failed (rc=${rc}, try ${_try}/5) — retrying…" + sleep 3 + done + return "${rc}" + } + local SLIM_HEAD_RC=0 + cdn_head "${SLIM_URL}" "universal boot files" || SLIM_HEAD_RC=$? + if [ "${SLIM_HEAD_RC}" -ne 0 ] && [ "${SLIM_HEAD_RC}" -ne 22 ]; then + err "CDN unreachable for universal boot files — refusing to flash the cached copies (they may be stale)" + exit 1 + fi + if [ "${SLIM_HEAD_RC}" -eq 0 ]; then # Cache-aware downloads for slim+initramfs. These have no published # hash, and the previous check compared Content-Length only — which is # useless precisely when it matters: consecutive kernel builds come out @@ -1773,8 +1795,13 @@ pull_ota() { # that boots UEFI machines only. local KPART_URL="${CDN_BASE}/native-notepat-latest.vmlinuz.kpart" local PULLED_KPART="${PULL_DIR}/vmlinuz.kpart" - if curl -sf --head "${KPART_URL}" >/dev/null 2>&1; then + local KPART_HEAD_RC=0 + cdn_head "${KPART_URL}" "Chromebook kpart" || KPART_HEAD_RC=$? + if [ "${KPART_HEAD_RC}" -eq 0 ]; then fetch_if_stale "${KPART_URL}" "${PULLED_KPART}" "Chromebook kpart" + elif [ "${KPART_HEAD_RC}" -ne 22 ]; then + err "CDN unreachable for the Chromebook kpart — refusing to flash without it (would silently drop KERN-A)" + exit 1 else rm -f "${PULLED_KPART}" "${PULLED_KPART}.etag" log "No Chromebook kpart published for this release — stick will boot UEFI machines only" diff --git a/fedac/native/scripts/flash-mac.sh b/fedac/native/scripts/flash-mac.sh index 5d39e6552b..b82701ea06 100755 --- a/fedac/native/scripts/flash-mac.sh +++ b/fedac/native/scripts/flash-mac.sh @@ -425,29 +425,58 @@ log "Zapping GPT + clearing first 16 MiB…" sgdisk --zap-all "${USB_DEV}" >/dev/null dd if=/dev/zero of="${USB_DEV}" bs=1m count=16 status=none +write_gpt() { + # sgdisk output is kept visible: a failed primary-header write used to + # vanish into /dev/null and surface only as "partition did not appear". + if [ "${KPART_MB}" -gt 0 ]; then + # Partition 3 sits between ACBOOT and ACEFI on disk; numbering is what + # matters to macOS (disk4s3) and vboot scans every kernel-type partition. + # Type 7f00 = ChromeOS kernel. Attribute bits (cgpt semantics): + # 48-51 priority = 10 (bits 49,51), 52-55 tries = 5 (bits 52,54), + # 56 successful = 1 — the same flags chrx/ChromeOS recovery media use, so + # the firmware never counts the stick down to unbootable. + sgdisk \ + --new=1:0:+${MAIN_MB}M --typecode=1:0700 --change-name=1:ACBOOT \ + --new=3:0:+${KPART_MB}M --typecode=3:7f00 --change-name=3:KERN-A \ + --attributes=3:set:49 --attributes=3:set:51 \ + --attributes=3:set:52 --attributes=3:set:54 \ + --attributes=3:set:56 \ + --new=2:0:0 --typecode=2:ef00 --change-name=2:ACEFI \ + "${USB_DEV}" 2>&1 | sed 's/^/[sgdisk] /' + else + sgdisk \ + --new=1:0:+${MAIN_MB}M --typecode=1:0700 --change-name=1:ACBOOT \ + --new=2:0:0 --typecode=2:ef00 --change-name=2:ACEFI \ + "${USB_DEV}" 2>&1 | sed 's/^/[sgdisk] /' + fi +} +gpt_ok() { + # A readable table lists ACEFI (and KERN-A when requested); a torn write + # prints "Main header: ERROR" instead. + local table + table=$(sgdisk -p "${USB_DEV}" 2>&1) + echo "${table}" | grep -q "ERROR" && return 1 + echo "${table}" | grep -q "ACEFI" || return 1 + [ "${KPART_MB}" -eq 0 ] || echo "${table}" | grep -q "KERN-A" +} if [ "${KPART_MB}" -gt 0 ]; then - # Partition 3 sits between ACBOOT and ACEFI on disk; numbering is what - # matters to macOS (disk4s3) and vboot scans every kernel-type partition. - # Type 7f00 = ChromeOS kernel. Attribute bits (cgpt semantics): - # 48-51 priority = 10 (bits 49,51), 52-55 tries = 5 (bits 52,54), - # 56 successful = 1 — the same flags chrx/ChromeOS recovery media use, so - # the firmware never counts the stick down to unbootable. log "Creating GPT layout (ACBOOT + KERN-A + ACEFI)…" - sgdisk \ - --new=1:0:+${MAIN_MB}M --typecode=1:0700 --change-name=1:ACBOOT \ - --new=3:0:+${KPART_MB}M --typecode=3:7f00 --change-name=3:KERN-A \ - --attributes=3:set:49 --attributes=3:set:51 \ - --attributes=3:set:52 --attributes=3:set:54 \ - --attributes=3:set:56 \ - --new=2:0:0 --typecode=2:ef00 --change-name=2:ACEFI \ - "${USB_DEV}" >/dev/null else log "Creating GPT layout (ACBOOT + ACEFI)…" - sgdisk \ - --new=1:0:+${MAIN_MB}M --typecode=1:0700 --change-name=1:ACBOOT \ - --new=2:0:0 --typecode=2:ef00 --change-name=2:ACEFI \ - "${USB_DEV}" >/dev/null fi +# macOS DiskArbitration re-probes the disk the moment the zap lands and can +# hold it while sgdisk writes the primary header, leaving a torn table +# (backup OK, primary ERROR). Verify and retry rather than discover it later. +for attempt in 1 2 3; do + diskutil unmountDisk force "${USB_DEV}" >/dev/null 2>&1 || true + write_gpt + sleep 1 + gpt_ok && break + [ "${attempt}" -lt 3 ] || die "GPT did not verify after 3 attempts: $(sgdisk -p "${USB_DEV}" 2>&1 | grep -E 'ERROR|Invalid' | tr '\n' ' ')" + log "GPT write did not verify (attempt ${attempt}) — re-zapping and retrying…" + sleep 2 + sgdisk --zap-all "${USB_DEV}" >/dev/null 2>&1 || true +done # Force macOS to re-read the partition table after sgdisk wrote it. The # kernel caches the old layout until we explicitly notify it; without this, @@ -583,7 +612,7 @@ with open(sys.argv[3], 'w') as f: && log "Merged $(python3 -c 'import json,sys; print(len(json.load(open(sys.argv[1]))))' "${WIFI_MERGED}") wifi networks (presets + preserved)" else printf '%s\n' "${WIFI_PRESETS_JSON}" > "${WIFI_MERGED}" - log "Wrote 6 preset wifi networks (no previous USB to preserve from)" + log "Wrote $(python3 -c 'import json,sys; print(len(json.load(open(sys.argv[1]))))' "${WIFI_MERGED}") preset wifi networks (no previous USB to preserve from)" fi fi cp "${WIFI_MERGED}" "${M1}/wifi_creds.json" -- 2.51.2