Handle authorized Debian upgrades and verified BIOS GPT repair

This commit is contained in:
kaka
2026-11-04 18:46:37 +08:00
parent 3365c09c8d
commit 6c7d1be40a
3 changed files with 95 additions and 18 deletions

View File

@@ -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、私钥写入公开文件。
- 重跑保留身份。信任、公钥、现有配置冲突先停止;不重置网络、防火墙或已有数据库。

View File

@@ -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;归档不替代异机或应用一致性备份。

View File

@@ -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
[[ "$answer" == "$expected" ]]
}
apt_upgrade() {
if "$assume_yes"; then
DEBIAN_FRONTEND=noninteractive APT_LISTCHANGES_FRONTEND=none apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold "$@" </dev/null
else apt-get "$@" </dev/tty; fi
}
# Validate only explicitly supported official source syntax. Unknown layouts stop.
validate_source() {
local file=$1 suite=$2
@@ -16,7 +84,8 @@ validate_source() {
if(!have)return
if(enabled!="" && enabled!="yes" && enabled!="no")reject()
if(enabled=="no"){reset();return}
if(types!="deb" || !valid_uri(uri) || suites=="" || components=="")reject()
if(types!="deb" && types!="deb-src" && types!="deb deb-src" && types!="deb-src deb")reject()
if(!valid_uri(uri) || suites=="" || components=="")reject()
n=split(suites,a,/[ \t]+/);for(i=1;i<=n;i++)if(!valid_suite(a[i]))reject()
if(uri ~ /debian-security\/?$/ && suites!=suite "-security")reject()
if(uri !~ /debian-security\/?$/ && suites ~ /-security/)reject()
@@ -26,8 +95,8 @@ validate_source() {
/^[ \t]*$/ {if(deb822)block();next}
!deb822 {
line=$0;sub(/[ \t]+#.*/,"",line)
if(line !~ /^[ \t]*deb[ \t]+/) {reject();next}
sub(/^[ \t]*deb[ \t]+/,"",line)
if(line !~ /^[ \t]*deb(-src)?[ \t]+/) {reject();next}
sub(/^[ \t]*deb(-src)?[ \t]+/,"",line)
if(line ~ /^\[/){
if(line !~ /^\[[^]]+\][ \t]+/){reject();next}
options=line;sub(/\].*$/,"",options);sub(/^\[/,"",options)
@@ -74,7 +143,7 @@ render_source() {
}
!deb822 {
line=$0
if(match(line,/^[ \t]*deb[ \t]+(\[[^]]+\][ \t]+)?[^ \t]+[ \t]+/)){
if(match(line,/^[ \t]*deb(-src)?[ \t]+(\[[^]]+\][ \t]+)?[^ \t]+[ \t]+/)){
prefix=substr(line,1,RLENGTH);tail=substr(line,RLENGTH+1)
sub(/^bookworm/,"trixie",tail);line=prefix tail
}
@@ -146,7 +215,14 @@ save_phase() {
main() {
local action=${1:---help}
case "$action" in --help|-h) usage; return 0;; --check|--upgrade|--resume|--verify) :;; *) usage; return 1;; esac
[[ $# == 1 && $EUID == 0 && -d /run/systemd/system && ! -e /.dockerenv ]] || { fail 'Run on a native systemd host as root'; return 1; }
shift
local assume_yes=false recovery_confirmed=false repair_bios=false
while (($#)); do
case "$1" in --yes) assume_yes=true;; --recovery-confirmed) recovery_confirmed=true;; --repair-bios-boot) repair_bios=true;; *) fail "Unknown argument: $1"; return 1;; esac
shift
done
! "$assume_yes" || "$recovery_confirmed" || { fail '--yes requires --recovery-confirmed'; return 1; }
[[ $EUID == 0 && -d /run/systemd/system && ! -e /.dockerenv ]] || { fail 'Run on a native systemd host as root'; return 1; }
grep -Eq '^ID="?debian"?$' /etc/os-release || { fail 'Debian required'; return 1; }
local state=/var/lib/platform-debian-upgrade phase='' boot_id answer file
boot_id=$(cat /proc/sys/kernel/random/boot_id)
@@ -159,7 +235,7 @@ main() {
package_checks
if [[ "$action" != --check ]]; then
[[ "$action" == --upgrade || -d "$state" ]] || { fail 'No original upgrade state'; return 1; }
[[ -r /dev/tty && -w /dev/tty ]] || { fail 'A real controlling terminal is required'; return 1; }
"$assume_yes" || [[ -r /dev/tty && -w /dev/tty ]] || { fail 'A real controlling terminal is required'; return 1; }
umask 077
install -d -m 0700 "$state"
[[ ! -L "$state/lock" && ( ! -e "$state/lock" || ( -f "$state/lock" && $(stat -c %h "$state/lock") == 1 ) ) ]] || { fail 'Invalid lock'; return 1; }
@@ -182,13 +258,12 @@ main() {
scan_sources bookworm
[[ $(df -Pk / | awk 'NR==2{print $4}') -ge 5242880 && $(df -Pk /boot | awk 'NR==2{print $4}') -ge 262144 ]] || { fail 'Need 5 GiB free on / and 256 MiB on /boot'; return 1; }
if [[ -d /sys/firmware/efi ]]; then mountpoint -q /boot/efi || { fail 'Mount and review the EFI System Partition first'; return 1; }; fi
if [[ "$action" == --check ]]; then echo 'Source/package/storage checks passed; review bootloader and recovery access before --upgrade.'; return 0; fi
if [[ "$action" == --check ]]; then check_bios_boot; echo 'Source/package/storage/boot-layout checks passed; review recovery access before --upgrade.'; return 0; fi
fi
[[ -r /dev/tty && -w /dev/tty ]] || { fail 'A real controlling terminal is required'; return 1; }
"$assume_yes" || [[ -r /dev/tty && -w /dev/tty ]] || { fail 'A real controlling terminal is required'; return 1; }
[[ ( "$action" == --upgrade && -z "$phase" ) || ( "$action" == --resume && -n "$phase" ) ]] || { fail 'Use --upgrade for a new run or --resume for the existing run'; return 1; }
if [[ -z "$phase" ]]; then
printf 'Bootloader, rescue access and required backups reviewed. Type UPGRADE: ' >/dev/tty
read -r answer </dev/tty; [[ "$answer" == UPGRADE ]] || return 1
confirm_upgrade UPGRADE 'Bootloader, rescue access and required backups reviewed.' || return 1
tar --acls --xattrs -cpf "$state/before.tar" /etc /boot /var/lib/dpkg/status
tar -tf "$state/before.tar" >/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 </dev/tty
apt-get update; apt_upgrade full-upgrade
package_checks
printf '%s\n' "$boot_id" > "$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 </dev/tty; [[ "$answer" == TRIXIE ]] || return 1
confirm_upgrade TRIXIE 'Switch the reviewed Debian sources to trixie.' || return 1
uname -r > "$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 </dev/tty
apt-get full-upgrade </dev/tty
apt_upgrade upgrade --without-new-pkgs
apt_upgrade full-upgrade
package_checks
printf '%s\n' "$boot_id" > "$state/upgrade-boot"
save_phase awaiting_reboot