diff --git a/build/lib/scripts/upgrade b/build/lib/scripts/upgrade index 6945c7586..a7559987f 100755 --- a/build/lib/scripts/upgrade +++ b/build/lib/scripts/upgrade @@ -68,21 +68,6 @@ fi EOF -# Promote the USB installer boot entry back to first in EFI boot order. -# The entry number was saved during initial OS install. -if [ -d /sys/firmware/efi ] && [ -f /media/startos/config/efi-installer-entry ]; then - USB_ENTRY=$(cat /media/startos/config/efi-installer-entry) - if [ -n "$USB_ENTRY" ]; then - CURRENT_ORDER=$(efibootmgr | grep BootOrder | sed 's/BootOrder: //') - OTHER_ENTRIES=$(echo "$CURRENT_ORDER" | tr ',' '\n' | grep -v "$USB_ENTRY" | tr '\n' ',' | sed 's/,$//') - if [ -n "$OTHER_ENTRIES" ]; then - efibootmgr -o "$USB_ENTRY,$OTHER_ENTRIES" - else - efibootmgr -o "$USB_ENTRY" - fi - fi -fi - # Sign unsigned kernel modules for Secure Boot SIGN_FILE="$(ls -1 /media/startos/next/usr/lib/linux-kbuild-*/scripts/sign-file 2>/dev/null | head -1)" /media/startos/next/usr/lib/startos/scripts/sign-unsigned-modules \ diff --git a/core/src/context/config.rs b/core/src/context/config.rs index fb76f96ce..4e1dd4827 100644 --- a/core/src/context/config.rs +++ b/core/src/context/config.rs @@ -9,7 +9,6 @@ use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use crate::MAIN_DATA; -use crate::disk::OsPartitionInfo; use crate::prelude::*; use crate::util::serde::IoFormat; use crate::version::VersionT; @@ -120,8 +119,6 @@ impl ClientConfig { pub struct ServerConfig { #[arg(short, long, help = "help.arg.config-file-path")] pub config: Option, - #[arg(skip)] - pub os_partitions: Option, #[arg(long, help = "help.arg.socks-listen-address")] pub socks_listen: Option, #[arg(long, help = "help.arg.revision-cache-size")] @@ -138,7 +135,6 @@ impl ContextConfig for ServerConfig { self.config.take() } fn merge_with(&mut self, other: Self) { - self.os_partitions = self.os_partitions.take().or(other.os_partitions); self.socks_listen = self.socks_listen.take().or(other.socks_listen); self.revision_cache_size = self .revision_cache_size diff --git a/core/src/context/rpc.rs b/core/src/context/rpc.rs index f1fb6343d..61ce35020 100644 --- a/core/src/context/rpc.rs +++ b/core/src/context/rpc.rs @@ -327,12 +327,7 @@ impl RpcContext { let seed = Arc::new(RpcContextSeed { is_closed: AtomicBool::new(false), - os_partitions: config.os_partitions.clone().ok_or_else(|| { - Error::new( - eyre!("{}", t!("context.rpc.os-partition-info-missing")), - ErrorKind::Filesystem, - ) - })?, + os_partitions: OsPartitionInfo::from_fstab().await?, wifi_interface: wifi_interface.clone(), ethernet_interface: find_eth_iface().await?, disk_guid, diff --git a/core/src/disk/mod.rs b/core/src/disk/mod.rs index aea0ad9a3..ed312aede 100644 --- a/core/src/disk/mod.rs +++ b/core/src/disk/mod.rs @@ -1,13 +1,17 @@ +use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use itertools::Itertools; use lazy_format::lazy_format; use rpc_toolkit::{CallRemoteHandler, Context, Empty, HandlerExt, ParentHandler, from_fn_async}; use serde::{Deserialize, Serialize}; +use tokio::process::Command; -use crate::Error; +use crate::{Error, ErrorKind}; use crate::context::{CliContext, RpcContext}; use crate::disk::util::DiskInfo; +use crate::prelude::*; +use crate::util::Invoke; use crate::util::serde::{HandlerExtSerde, WithIoFormat, display_serializable}; pub mod fsck; @@ -21,27 +25,143 @@ pub const REPAIR_DISK_PATH: &str = "/media/startos/config/repair-disk"; #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct OsPartitionInfo { - pub efi: Option, pub bios: Option, pub boot: PathBuf, pub root: PathBuf, - #[serde(skip)] // internal use only + #[serde(default)] + pub extra_boot: BTreeMap, + #[serde(skip)] pub data: Option, } impl OsPartitionInfo { pub fn contains(&self, logicalname: impl AsRef) -> bool { - self.efi - .as_ref() - .map(|p| p == logicalname.as_ref()) - .unwrap_or(false) - || self - .bios - .as_ref() - .map(|p| p == logicalname.as_ref()) - .unwrap_or(false) - || &*self.boot == logicalname.as_ref() - || &*self.root == logicalname.as_ref() + let p = logicalname.as_ref(); + self.bios.as_deref() == Some(p) + || p == &*self.boot + || p == &*self.root + || self.extra_boot.values().any(|v| v == p) } + + /// Build partition info by parsing /etc/fstab and resolving device specs, + /// then discovering the BIOS boot partition (which is never mounted). + pub async fn from_fstab() -> Result { + let fstab = tokio::fs::read_to_string("/etc/fstab") + .await + .with_ctx(|_| (ErrorKind::Filesystem, "/etc/fstab"))?; + + let mut boot = None; + let mut root = None; + let mut extra_boot = BTreeMap::new(); + + for line in fstab.lines() { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { + continue; + } + let mut fields = line.split_whitespace(); + let Some(source) = fields.next() else { + continue; + }; + let Some(target) = fields.next() else { + continue; + }; + + let dev = match resolve_fstab_source(source).await { + Ok(d) => d, + Err(e) => { + tracing::warn!("Failed to resolve fstab source {source}: {e}"); + continue; + } + }; + + match target { + "/" => root = Some(dev), + "/boot" => boot = Some(dev), + t if t.starts_with("/boot/") => { + if let Some(name) = t.strip_prefix("/boot/") { + extra_boot.insert(name.to_string(), dev); + } + } + _ => {} + } + } + + let boot = boot.unwrap_or_default(); + let bios = if !boot.as_os_str().is_empty() { + find_bios_boot_partition(&boot).await.ok().flatten() + } else { + None + }; + + Ok(Self { + bios, + boot, + root: root.unwrap_or_default(), + extra_boot, + data: None, + }) + } +} + +const BIOS_BOOT_TYPE_GUID: &str = "21686148-6449-6e6f-744e-656564726548"; + +/// Find the BIOS boot partition on the same disk as `known_part`. +async fn find_bios_boot_partition(known_part: &Path) -> Result, Error> { + let output = Command::new("lsblk") + .args(["-n", "-l", "-o", "NAME,PKNAME,PARTTYPE"]) + .arg(known_part) + .invoke(ErrorKind::DiskManagement) + .await?; + let text = String::from_utf8(output)?; + + let parent_disk = text.lines().find_map(|line| { + let mut fields = line.split_whitespace(); + let _name = fields.next()?; + let pkname = fields.next()?; + (!pkname.is_empty()).then(|| pkname.to_string()) + }); + + let Some(parent_disk) = parent_disk else { + return Ok(None); + }; + + let output = Command::new("lsblk") + .args(["-n", "-l", "-o", "NAME,PARTTYPE"]) + .arg(format!("/dev/{parent_disk}")) + .invoke(ErrorKind::DiskManagement) + .await?; + let text = String::from_utf8(output)?; + + for line in text.lines() { + let mut fields = line.split_whitespace(); + let Some(name) = fields.next() else { continue }; + let Some(parttype) = fields.next() else { + continue; + }; + if parttype.eq_ignore_ascii_case(BIOS_BOOT_TYPE_GUID) { + return Ok(Some(PathBuf::from(format!("/dev/{name}")))); + } + } + + Ok(None) +} + +/// Resolve an fstab device spec (e.g. /dev/sda1, PARTUUID=..., UUID=...) to a +/// canonical device path. +async fn resolve_fstab_source(source: &str) -> Result { + if source.starts_with('/') { + return Ok( + tokio::fs::canonicalize(source) + .await + .unwrap_or_else(|_| PathBuf::from(source)), + ); + } + // PARTUUID=, UUID=, LABEL= — resolve via blkid + let output = Command::new("blkid") + .args(["-o", "device", "-t", source]) + .invoke(ErrorKind::DiskManagement) + .await?; + Ok(PathBuf::from(String::from_utf8(output)?.trim())) } pub fn disk() -> ParentHandler { diff --git a/core/src/os_install/gpt.rs b/core/src/os_install/gpt.rs index 0fe5d0665..932538d98 100644 --- a/core/src/os_install/gpt.rs +++ b/core/src/os_install/gpt.rs @@ -197,11 +197,19 @@ pub async fn partition( .invoke(crate::ErrorKind::DiskManagement) .await?; + let mut extra_boot = std::collections::BTreeMap::new(); + let bios; + if efi { + extra_boot.insert("efi".to_string(), partition_for(&disk_path, 1)); + bios = None; + } else { + bios = Some(partition_for(&disk_path, 1)); + } Ok(OsPartitionInfo { - efi: efi.then(|| partition_for(&disk_path, 1)), - bios: (!efi).then(|| partition_for(&disk_path, 1)), + bios, boot: partition_for(&disk_path, 2), root: partition_for(&disk_path, 3), + extra_boot, data: data_part, }) } diff --git a/core/src/os_install/mbr.rs b/core/src/os_install/mbr.rs index b121198f8..090fa9554 100644 --- a/core/src/os_install/mbr.rs +++ b/core/src/os_install/mbr.rs @@ -164,10 +164,10 @@ pub async fn partition( .await?; Ok(OsPartitionInfo { - efi: None, bios: None, boot: partition_for(&disk_path, 1), root: partition_for(&disk_path, 2), + extra_boot: Default::default(), data: data_part, }) } diff --git a/core/src/os_install/mod.rs b/core/src/os_install/mod.rs index 6a1c00f35..72918042d 100644 --- a/core/src/os_install/mod.rs +++ b/core/src/os_install/mod.rs @@ -27,53 +27,6 @@ use crate::util::serde::IoFormat; mod gpt; mod mbr; -/// Get the EFI BootCurrent entry number (the entry firmware used to boot). -/// Returns None on non-EFI systems or if BootCurrent is not set. -async fn get_efi_boot_current() -> Result, Error> { - let efi_output = String::from_utf8(Command::new("efibootmgr").invoke(ErrorKind::Grub).await?)?; - - Ok(efi_output - .lines() - .find(|line| line.starts_with("BootCurrent:")) - .and_then(|line| line.strip_prefix("BootCurrent:")) - .map(|s| s.trim().to_string())) -} - -/// Promote a specific boot entry to first in the EFI boot order. -async fn promote_efi_entry(entry: &str) -> Result<(), Error> { - let efi_output = String::from_utf8(Command::new("efibootmgr").invoke(ErrorKind::Grub).await?)?; - - let current_order = efi_output - .lines() - .find(|line| line.starts_with("BootOrder:")) - .and_then(|line| line.strip_prefix("BootOrder:")) - .map(|s| s.trim()) - .unwrap_or(""); - - if current_order.is_empty() || current_order.starts_with(entry) { - return Ok(()); - } - - let other_entries: Vec<&str> = current_order - .split(',') - .filter(|e| e.trim() != entry) - .collect(); - - let new_order = if other_entries.is_empty() { - entry.to_string() - } else { - format!("{},{}", entry, other_entries.join(",")) - }; - - Command::new("efibootmgr") - .arg("-o") - .arg(&new_order) - .invoke(ErrorKind::Grub) - .await?; - - Ok(()) -} - /// Probe a squashfs image to determine its target architecture async fn probe_squashfs_arch(squashfs_path: &Path) -> Result { let output = String::from_utf8( @@ -190,7 +143,7 @@ pub async fn install_os_to( let part_info = partition(disk_path, capacity, partition_table, protect, use_efi).await?; - if let Some(efi) = &part_info.efi { + if let Some(efi) = part_info.extra_boot.get("efi") { Command::new("mkfs.vfat") .arg(efi) .invoke(crate::ErrorKind::DiskManagement) @@ -307,10 +260,7 @@ pub async fn install_os_to( tokio::fs::write( rootfs.path().join("config/config.yaml"), - IoFormat::Yaml.to_vec(&ServerConfig { - os_partitions: Some(part_info.clone()), - ..Default::default() - })?, + IoFormat::Yaml.to_vec(&ServerConfig::default())?, ) .await?; @@ -329,7 +279,7 @@ pub async fn install_os_to( ReadWrite, ) .await?; - let efi = if let Some(efi) = &part_info.efi { + let efi = if let Some(efi) = part_info.extra_boot.get("efi") { Some( MountGuard::mount( &BlockDev::new(efi), @@ -370,8 +320,8 @@ pub async fn install_os_to( include_str!("fstab.template"), boot = part_info.boot.display(), efi = part_info - .efi - .as_ref() + .extra_boot + .get("efi") .map(|p| p.display().to_string()) .unwrap_or_else(|| "# N/A".to_owned()), root = part_info.root.display(), @@ -502,20 +452,6 @@ pub async fn install_os( let use_efi = tokio::fs::metadata("/sys/firmware/efi").await.is_ok(); - // Save the boot entry we booted from (the USB installer) before grub-install - // overwrites the boot order. - let boot_current = if use_efi { - match get_efi_boot_current().await { - Ok(entry) => entry, - Err(e) => { - tracing::warn!("Failed to get EFI BootCurrent: {e}"); - None - } - } - } else { - None - }; - let InstallOsResult { part_info, rootfs, @@ -531,23 +467,6 @@ pub async fn install_os( ) .await?; - // grub-install prepends its new entry to the EFI boot order, overriding the - // USB-first priority. Promote the USB entry (identified by BootCurrent from - // when we booted the installer) back to first, and persist the entry number - // so the upgrade script can do the same. - if let Some(ref entry) = boot_current { - if let Err(e) = promote_efi_entry(entry).await { - tracing::warn!("Failed to restore EFI boot order: {e}"); - } - let efi_entry_path = rootfs.path().join("config/efi-installer-entry"); - if let Err(e) = tokio::fs::write(&efi_entry_path, entry).await { - tracing::warn!("Failed to save EFI installer entry number: {e}"); - } - } - - ctx.config - .mutate(|c| c.os_partitions = Some(part_info.clone())); - let mut setup_info = SetupInfo::default(); setup_info.mok_enrolled = mok_enrolled; diff --git a/core/src/setup.rs b/core/src/setup.rs index ebb177cbc..1b33268b3 100644 --- a/core/src/setup.rs +++ b/core/src/setup.rs @@ -95,8 +95,8 @@ const LIVE_MEDIUM_PATH: &str = "/run/live/medium"; pub async fn list_disks(ctx: SetupContext) -> Result, Error> { let mut disks = crate::disk::util::list( - &ctx.config - .peek(|c| c.os_partitions.clone()) + &crate::disk::OsPartitionInfo::from_fstab() + .await .unwrap_or_default(), ) .await?;