Handle authorized Debian upgrades and verified BIOS GPT repair
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user