From 6c7d1be40abf5be3319f9e126496f3a477f789e5 Mon Sep 17 00:00:00 2001 From: kaka Date: Wed, 4 Nov 2026 18:46:37 +0800 Subject: [PATCH] Handle authorized Debian upgrades and verified BIOS GPT repair --- AGENTS.md | 2 +- README.md | 4 +- upgrade-debian12-13.sh | 107 +++++++++++++++++++++++++++++++++++------ 3 files changed, 95 insertions(+), 18 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 854d760..47dd50d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,7 +1,7 @@ # 原生 Shell 仓库 - 直接在本仓库维护原生 Bash 源码;目标机不需要 Python 载荷或私有仓库。 -- install.sh 是 Debian 13 的最小 Komodo 接入;独立 upgrade-debian12-13.sh 由人执行。 +- install.sh 是 Debian 13 的最小 Komodo 接入;独立 upgrade-debian12-13.sh 默认交互执行,已有升级与恢复授权可用 --yes --recovery-confirmed;受限 BIOS/GPT 修复需显式 --repair-bios-boot。 - 支持 curl -fsSL URL | bash -s -- 参数;管道模式不能依赖脚本所在目录。交互升级读取 /dev/tty。 - 数据参数按字符串处理,不 eval 或 source 用户配置;不把 API/DNS token、私钥写入公开文件。 - 重跑保留身份。信任、公钥、现有配置冲突先停止;不重置网络、防火墙或已有数据库。 diff --git a/README.md b/README.md index c845952..6f4d993 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,9 @@ bash upgrade-debian12-13.sh --verify ``` resume/verify 可使用同一固定版本 URL 再次下载执行,或保存该版本的文件后执行。 -升级使用真实终端 /dev/tty,APT 配置冲突由人判断,不自动重启、不自动 autoremove。 +默认升级使用真实终端 /dev/tty,APT 配置冲突由操作者判断。已经授权维护并确认救援入口时,可显式传 `--yes --recovery-confirmed`,使用非交互 APT 并保留本地配置;仍不自动重启、不自动 autoremove。升级和重启后的 resume 分别执行,不能并发运行。 + +官方 `deb-src` 和 Deb822 `Types: deb deb-src` 现在随二进制源一起校验和转换,不必临时禁用。BIOS/GPT 缺少 BIOS Boot 分区会在 check 阶段报错;已授权磁盘引导修复可给 upgrade/resume 增加 `--repair-bios-boot`。只自动处理 512 字节扇区、单个 Linux 分区从 2048 扇区开始、GPT first-lba 为 34 的已验证布局,在空闲 34–2047 扇区新增 BIOS Boot 分区。修改前备份 GPT 和磁盘首尾,修改后逐字校验原分区条目并重新安装 GRUB;其他布局停止,不移动或缩小已有分区。该选项不替代提供商救援入口和异机备份。 仅接受显式官方 Debian 源的受支持格式;第三方源、混合版本、复杂 Deb822 需先人工处理。 检查 APT、磁盘、UEFI 挂载;实际引导布局、救援入口和应用备份由操作者在升级前确认。 状态与系统归档在 /var/lib/platform-debian-upgrade;归档不替代异机或应用一致性备份。 diff --git a/upgrade-debian12-13.sh b/upgrade-debian12-13.sh index 5f178f3..d167f98 100644 --- a/upgrade-debian12-13.sh +++ b/upgrade-debian12-13.sh @@ -2,8 +2,76 @@ # Independent manual upgrade. Never invoked by install.sh. set -euo pipefail export LC_ALL=C -usage() { echo 'Usage: bash upgrade-debian12-13.sh --check|--upgrade|--resume|--verify'; } +usage() { echo 'Usage: bash upgrade-debian12-13.sh --check|--upgrade|--resume|--verify [--yes --recovery-confirmed] [--repair-bios-boot]'; } fail() { printf 'ERROR: %s\n' "$*" >&2; return 1; } +# Only the verified single-partition, 512-byte-sector GPT layout is repairable. +bios_gap_plan() { + awk ' + /^label: / {if($2!="gpt")bad=1; label++} + /^sector-size: / {if($2!=512)bad=1; sector++} + /^first-lba: / {if($2!=34)bad=1; first++} + /^\/dev\// { + count++; line=$0 + if(line !~ /start=[ ]*2048,/ || line !~ /type=0FC63DAF-8483-4772-8E79-3D69D8477DE4/)bad=1 + } + END {if(bad || label!=1 || sector!=1 || first!=1 || count!=1)exit 1; print "34 2014"}' "$1" +} +check_bios_boot() { + [[ ! -d /sys/firmware/efi ]] || return 0 + local root_device parent disk dump before after sectors + root_device=$(grub-probe --target=device /boot) || return 1 + [[ "$root_device" =~ ^/dev/(vd[a-z]|sd[a-z]|nvme[0-9]+n[0-9]+p)[0-9]+$ ]] || { fail 'Review unsupported boot device layout'; return 1; } + parent=$(lsblk -ndo PKNAME "$root_device") + [[ "$parent" =~ ^(vd[a-z]|sd[a-z]|nvme[0-9]+n[0-9]+)$ ]] || { fail 'A single physical boot disk is required'; return 1; } + disk=/dev/$parent + dump=$(sfdisk --dump "$disk") || return 1 + [[ "$dump" == *'label: gpt'* ]] || return 0 + if grep -qi 'type=21686148-6449-6E6F-744E-656564454649' <<< "$dump"; then + if [[ -f "$state/bios-gpt-before.sfdisk" && ! -f "$state/bios-gpt-after.sfdisk" && "$action" != --check ]]; then + cmp -s <(grep '^/dev/' "$state/bios-gpt-before.sfdisk") <(grep '^/dev/' <<< "$dump" | grep -vi 'type=21686148-6449-6E6F-744E-656564454649') || { fail 'Original partition entry changed during interrupted repair'; return 1; } + grub-install --target=i386-pc "$disk" + update-grub + printf '%s\n' "$dump" > "$state/bios-gpt-after.sfdisk" + fi + return 0 + fi + if [[ "$action" == --check || "$repair_bios" != true ]]; then + fail 'BIOS/GPT has no BIOS Boot partition; review --repair-bios-boot with recovery access'; return 1 + fi + [[ -s "$state/before.tar" ]] || { fail 'Original upgrade backup required'; return 1; } + before=$state/bios-gpt-before.sfdisk + if [[ -e "$before" ]]; then + cmp -s "$before" <(printf '%s\n' "$dump") || { fail 'Boot layout changed after backup'; return 1; } + else printf '%s\n' "$dump" > "$before"; fi + bios_gap_plan "$before" >/dev/null || { fail 'No supported empty BIOS boot gap; no partition changes'; return 1; } + sectors=$(blockdev --getsz "$disk") + [[ "$sectors" =~ ^[0-9]+$ && "$sectors" -gt 4096 ]] || return 1 + [[ -e "$state/bios-first-1m.bin" ]] || dd if="$disk" of="$state/bios-first-1m.bin" bs=512 count=2048 status=none + [[ -e "$state/bios-last-33.bin" ]] || dd if="$disk" of="$state/bios-last-33.bin" bs=512 skip=$((sectors-33)) count=33 status=none + [[ $(stat -c %s "$state/bios-first-1m.bin") == 1048576 && $(stat -c %s "$state/bios-last-33.bin") == 16896 ]] || return 1 + printf 'start=34, size=2014, type=21686148-6449-6E6F-744E-656564454649, name="BIOS boot"\n' | + sfdisk --append --no-reread --no-tell-kernel --wipe never --wipe-partitions never "$disk" + sfdisk --verify "$disk" + after=$(sfdisk --dump "$disk") + # Preserve every byte of the original partition entry (UUID, offset and size). + cmp -s <(grep '^/dev/' "$before") <(grep '^/dev/' <<< "$after" | grep -vi 'type=21686148-6449-6E6F-744E-656564454649') || { fail 'Original partition entry changed; inspect saved GPT before proceeding'; return 1; } + partx -a "$disk" || partx -u "$disk" + grub-install --target=i386-pc "$disk" + update-grub + printf '%s\n' "$after" > "$state/bios-gpt-after.sfdisk" +} +confirm_upgrade() { + local expected=$1 prompt=$2 answer + if "$assume_yes"; then return 0; fi + printf '%s Type %s: ' "$prompt" "$expected" >/dev/tty + read -r answer /dev/tty - read -r answer /dev/null : > "$state/identities.sha256" @@ -197,9 +272,10 @@ main() { fi case "$phase" in prepared|bookworm_updated|switching_sources|sources_switched|awaiting_reboot|verified) :;; *) fail 'Unknown recorded phase'; return 1;; esac if [[ "$phase" == awaiting_reboot || "$phase" == verified ]]; then echo 'Upgrade already finished; reboot manually and use --verify.'; return 0; fi + check_bios_boot if [[ "$phase" == prepared ]]; then scan_sources bookworm - apt-get update; apt-get full-upgrade "$state/bookworm-boot" phase=bookworm_updated; save_phase "$phase" @@ -209,8 +285,7 @@ main() { ! reboot_pending || { fail 'Reboot before continuing'; return 1; } [[ ! -e "$state/reboot-required" || "$boot_id" != "$(cat "$state/bookworm-boot")" ]] || return 1 scan_sources bookworm - printf 'Switch the reviewed Debian sources to trixie. Type TRIXIE: ' >/dev/tty - read -r answer "$state/bookworm-kernel" install -d -m 0700 "$state/sources" printf '%s\n' "${source_files[@]}" > "$state/source-files" @@ -228,8 +303,8 @@ main() { scan_sources trixie while IFS= read -r file; do cmp -s "$file" "$state/sources$file.target" || { fail 'Target source changed'; return 1; }; done < "$state/source-files" apt-get update - apt-get upgrade --without-new-pkgs "$state/upgrade-boot" save_phase awaiting_reboot