Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/initramfs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ composefs.workspace = true
composefs-boot.workspace = true
toml.workspace = true
fn-error-context.workspace = true
bootc-kernel-cmdline = { path = "../kernel_cmdline", version = "0.0.0" }

[lints]
workspace = true
Expand Down
41 changes: 26 additions & 15 deletions crates/initramfs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ use composefs_boot::cmdline::get_cmdline_composefs;

use fn_error_context::context;

use bootc_kernel_cmdline::utf8::Cmdline;

// mount_setattr syscall support
const MOUNT_ATTR_RDONLY: u64 = 0x00000001;

Expand Down Expand Up @@ -74,13 +76,17 @@ fn set_mount_readonly(fd: impl AsFd) -> Result<()> {
mount_setattr(fd, libc::AT_EMPTY_PATH, &attr)
}

// Config file
/// Types of mounts supported by the configuration
#[derive(Clone, Copy, Debug, Deserialize)]
#[serde(rename_all = "lowercase")]
enum MountType {
pub enum MountType {
/// No mount
None,
/// Bind mount
Bind,
/// Overlay mount
Overlay,
/// Transient mount
Transient,
}

Expand All @@ -90,11 +96,14 @@ struct RootConfig {
transient: bool,
}

/// Configuration for mount operations
#[derive(Debug, Default, Deserialize)]
struct MountConfig {
mount: Option<MountType>,
pub struct MountConfig {
/// The type of mount to use
pub mount: Option<MountType>,
#[serde(default)]
transient: bool,
/// Whether this mount should be transient (temporary)
pub transient: bool,
}

#[derive(Deserialize, Default)]
Expand Down Expand Up @@ -138,7 +147,7 @@ pub struct Args {

#[arg(long, help = "Kernel commandline args (for testing)")]
/// Kernel commandline args (for testing)
pub cmdline: Option<String>,
pub cmdline: Option<Cmdline<'static>>,

#[arg(long, help = "Mountpoint (don't replace sysroot, for testing)")]
/// Mountpoint (don't replace sysroot, for testing)
Expand Down Expand Up @@ -265,8 +274,9 @@ pub fn mount_composefs_image(sysroot: &OwnedFd, name: &str, insecure: bool) -> R
Ok(rootfs)
}

/// Mounts a subdirectory with the specified configuration
#[context("Mounting subdirectory")]
fn mount_subdir(
pub fn mount_subdir(
new_root: impl AsFd,
state: impl AsFd,
subdir: &str,
Expand Down Expand Up @@ -331,12 +341,11 @@ pub fn setup_root(args: Args) -> Result<()> {
let sysroot = open_dir(CWD, &args.sysroot)
.with_context(|| format!("Failed to open sysroot {:?}", args.sysroot))?;

let cmdline = match &args.cmdline {
Some(cmdline) => cmdline,
// TODO: Deduplicate this with composefs branch karg parser
None => &std::fs::read_to_string("/proc/cmdline")?,
};
let (image, insecure) = get_cmdline_composefs::<Sha512HashValue>(cmdline)?;
let cmdline = args
.cmdline
.unwrap_or(Cmdline::from_proc().context("Failed to read cmdline")?);

let (image, insecure) = get_cmdline_composefs::<Sha512HashValue>(&cmdline)?;

let new_root = match args.root_fs {
Some(path) => open_root_fs(&path).context("Failed to clone specified root fs")?,
Expand All @@ -348,11 +357,13 @@ pub fn setup_root(args: Args) -> Result<()> {

set_mount_readonly(&sysroot_clone)?;

let mount_target = args.target.unwrap_or(args.sysroot.clone());

// Ideally we build the new root filesystem together before we mount it, but that only works on
// 6.15 and later. Before 6.15 we can't mount into a floating tree, so mount it first. This
// will leave an abandoned clone of the sysroot mounted under it, but that's OK for now.
if cfg!(feature = "pre-6.15") {
mount_at_wrapper(&new_root, CWD, &args.sysroot)?;
mount_at_wrapper(&new_root, CWD, &mount_target)?;
}

if config.root.transient {
Expand All @@ -372,7 +383,7 @@ pub fn setup_root(args: Args) -> Result<()> {
if cfg!(not(feature = "pre-6.15")) {
// Replace the /sysroot with the new composed root filesystem
unmount(&args.sysroot, UnmountFlags::DETACH)?;
mount_at_wrapper(&new_root, CWD, &args.sysroot)?;
mount_at_wrapper(&new_root, CWD, &mount_target)?;
}

Ok(())
Expand Down
87 changes: 54 additions & 33 deletions crates/lib/src/bootc_composefs/boot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,30 @@ fn compute_boot_digest(
Ok(hex::encode(digest))
}

/// Compute SHA256Sum of .linux + .initrd section of the UKI
///
/// # Arguments
/// * entry - BootEntry containing VMlinuz and Initrd
/// * repo - The composefs repository
#[context("Computing boot digest")]
pub(crate) fn compute_boot_digest_uki(uki: &[u8]) -> Result<String> {
let vmlinuz = composefs_boot::uki::get_section(uki, ".linux")
.ok_or_else(|| anyhow::anyhow!(".linux not present"))??;

let initramfs = composefs_boot::uki::get_section(uki, ".initrd")
.ok_or_else(|| anyhow::anyhow!(".initrd not present"))??;

let mut hasher = openssl::hash::Hasher::new(openssl::hash::MessageDigest::sha256())
.context("Creating hasher")?;

hasher.update(&vmlinuz).context("hashing vmlinuz")?;
hasher.update(&initramfs).context("hashing initrd")?;

let digest: &[u8] = &hasher.finish().context("Finishing digest")?;

Ok(hex::encode(digest))
}

/// Given the SHA256 sum of current VMlinuz + Initrd combo, find boot entry with the same SHA256Sum
///
/// # Returns
Expand Down Expand Up @@ -756,10 +780,11 @@ pub(crate) fn setup_composefs_bls_boot(
Ok(boot_digest)
}

struct UKILabels {
struct UKIInfo {
boot_label: String,
version: Option<String>,
os_id: Option<String>,
boot_digest: String,
}

/// Writes a PortableExecutable to ESP along with any PE specific or Global addons
Expand All @@ -773,10 +798,10 @@ fn write_pe_to_esp(
is_insecure_from_opts: bool,
mounted_efi: impl AsRef<Path>,
bootloader: &Bootloader,
) -> Result<Option<UKILabels>> {
) -> Result<Option<UKIInfo>> {
let efi_bin = read_file(file, &repo).context("Reading .efi binary")?;

let mut boot_label: Option<UKILabels> = None;
let mut boot_label: Option<UKIInfo> = None;

// UKI Extension might not even have a cmdline
// TODO: UKI Addon might also have a composefs= cmdline?
Expand Down Expand Up @@ -811,10 +836,13 @@ fn write_pe_to_esp(

let parsed_osrel = OsReleaseInfo::parse(osrel);

boot_label = Some(UKILabels {
let boot_digest = compute_boot_digest_uki(&efi_bin)?;

boot_label = Some(UKIInfo {
boot_label: uki::get_boot_label(&efi_bin).context("Getting UKI boot label")?,
version: parsed_osrel.get_version(),
os_id: parsed_osrel.get_value(&["ID"]),
boot_digest,
});
}

Expand Down Expand Up @@ -964,7 +992,7 @@ fn write_grub_uki_menuentry(
fn write_systemd_uki_config(
esp_dir: &Dir,
setup_type: &BootSetupType,
boot_label: UKILabels,
boot_label: UKIInfo,
id: &Sha512HashValue,
) -> Result<()> {
let os_id = boot_label.os_id.as_deref().unwrap_or("bootc");
Expand Down Expand Up @@ -1035,7 +1063,7 @@ pub(crate) fn setup_composefs_uki_boot(
repo: crate::store::ComposefsRepository,
id: &Sha512HashValue,
entries: Vec<ComposefsBootEntry<Sha512HashValue>>,
) -> Result<()> {
) -> Result<String> {
let (root_path, esp_device, bootloader, is_insecure_from_opts, uki_addons) = match setup_type {
BootSetupType::Setup((root_setup, state, postfetch, ..)) => {
state.require_no_kargs_for_uki()?;
Expand Down Expand Up @@ -1068,7 +1096,7 @@ pub(crate) fn setup_composefs_uki_boot(

let esp_mount = mount_esp(&esp_device).context("Mounting ESP")?;

let mut uki_label: Option<UKILabels> = None;
let mut uki_info: Option<UKIInfo> = None;

for entry in entries {
match entry {
Expand Down Expand Up @@ -1117,28 +1145,26 @@ pub(crate) fn setup_composefs_uki_boot(
)?;

if let Some(label) = ret {
uki_label = Some(label);
uki_info = Some(label);
}
}
};
}

let uki_label = uki_label
.ok_or_else(|| anyhow::anyhow!("Failed to get version and boot label from UKI"))?;
let uki_info =
uki_info.ok_or_else(|| anyhow::anyhow!("Failed to get version and boot label from UKI"))?;

let boot_digest = uki_info.boot_digest.clone();

match bootloader {
Bootloader::Grub => write_grub_uki_menuentry(
root_path,
&setup_type,
uki_label.boot_label,
id,
&esp_device,
)?,
Bootloader::Grub => {
write_grub_uki_menuentry(root_path, &setup_type, uki_info.boot_label, id, &esp_device)?
}

Bootloader::Systemd => write_systemd_uki_config(&esp_mount.fd, &setup_type, uki_label, id)?,
Bootloader::Systemd => write_systemd_uki_config(&esp_mount.fd, &setup_type, uki_info, id)?,
};

Ok(())
Ok(boot_digest)
}

#[context("Setting up composefs boot")]
Expand Down Expand Up @@ -1188,20 +1214,15 @@ pub(crate) fn setup_composefs_boot(
};

let boot_type = BootType::from(entry);
let mut boot_digest: Option<String> = None;

match boot_type {
BootType::Bls => {
let digest = setup_composefs_bls_boot(
BootSetupType::Setup((&root_setup, &state, &postfetch, &fs)),
repo,
&id,
entry,
&mounted_fs,
)?;

boot_digest = Some(digest);
}
let boot_digest = match boot_type {
BootType::Bls => setup_composefs_bls_boot(
BootSetupType::Setup((&root_setup, &state, &postfetch, &fs)),
repo,
&id,
entry,
&mounted_fs,
)?,
BootType::Uki => setup_composefs_uki_boot(
BootSetupType::Setup((&root_setup, &state, &postfetch, &fs)),
repo,
Expand All @@ -1212,7 +1233,7 @@ pub(crate) fn setup_composefs_boot(

write_composefs_state(
&root_setup.physical_root_path,
id,
&id,
&crate::spec::ImageReference::from(state.target_imgref.clone()),
false,
boot_type,
Expand Down
2 changes: 2 additions & 0 deletions crates/lib/src/bootc_composefs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ pub(crate) mod gc;
pub(crate) mod repo;
pub(crate) mod rollback;
pub(crate) mod service;
pub(crate) mod soft_reboot;
pub(crate) mod state;
pub(crate) mod status;
pub(crate) mod switch;
pub(crate) mod update;
pub(crate) mod utils;
78 changes: 78 additions & 0 deletions crates/lib/src/bootc_composefs/soft_reboot.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
use crate::{
bootc_composefs::{
service::start_finalize_stated_svc, status::composefs_deployment_status_from,
},
composefs_consts::COMPOSEFS_CMDLINE,
store::{BootedComposefs, Storage},
};
use anyhow::{Context, Result};
use bootc_initramfs_setup::setup_root;
use bootc_kernel_cmdline::utf8::Cmdline;
use bootc_mount::{bind_mount_from_pidns, PID1};
use camino::Utf8Path;
use fn_error_context::context;
use ostree_ext::systemd_has_soft_reboot;
use std::{fs::create_dir_all, os::unix::process::CommandExt, path::PathBuf, process::Command};

const NEXTROOT: &str = "/run/nextroot";

/// Checks if the provided deployment is soft reboot capable, and soft reboots the system if
/// argument `reboot` is true
#[context("Soft rebooting")]
pub(crate) async fn prepare_soft_reboot_composefs(
storage: &Storage,
booted_cfs: &BootedComposefs,
deployment_id: &String,
reboot: bool,
) -> Result<()> {
if !systemd_has_soft_reboot() {
anyhow::bail!("System does not support soft reboots")
}

if *deployment_id == *booted_cfs.cmdline.digest {
anyhow::bail!("Cannot soft-reboot to currently booted deployment");
}

// We definitely need to re-query the state as some deployment might've been staged
let host = composefs_deployment_status_from(storage, booted_cfs.cmdline).await?;

let all_deployments = host.all_composefs_deployments()?;

let requred_deployment = all_deployments
.iter()
.find(|entry| entry.deployment.verity == *deployment_id)
.ok_or_else(|| anyhow::anyhow!("Deployment '{deployment_id}' not found"))?;

if !requred_deployment.soft_reboot_capable {
anyhow::bail!("Cannot soft-reboot to deployment with a different kernel state");
}

start_finalize_stated_svc()?;

// escape to global mnt namespace
let run = Utf8Path::new("/run");
bind_mount_from_pidns(PID1, &run, &run, false).context("Bind mounting /run")?;

create_dir_all(NEXTROOT).context("Creating nextroot")?;

let cmdline = Cmdline::from(format!("{COMPOSEFS_CMDLINE}={deployment_id}"));

let args = bootc_initramfs_setup::Args {
cmd: vec![],
sysroot: PathBuf::from("/sysroot"),
config: Default::default(),
root_fs: None,
cmdline: Some(cmdline),
target: Some(NEXTROOT.into()),
};

setup_root(args)?;

if reboot {
// Replacing the current process should be fine as we restart userspace anyway
let err = Command::new("systemctl").arg("soft-reboot").exec();
return Err(anyhow::Error::from(err).context("Failed to exec 'systemctl soft-reboot'"));
}

Ok(())
}
Loading
Loading