diff --git a/Cargo.lock b/Cargo.lock index ae2b741b9..8f5ac5e81 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -191,6 +191,12 @@ name = "bootc-initramfs-setup" version = "0.1.0" dependencies = [ "anyhow", + "clap", + "composefs", + "composefs-boot", + "rustix 1.0.8", + "serde", + "toml 0.9.5", ] [[package]] diff --git a/crates/initramfs/Cargo.toml b/crates/initramfs/Cargo.toml index 81bd62973..82950f93e 100644 --- a/crates/initramfs/Cargo.toml +++ b/crates/initramfs/Cargo.toml @@ -7,6 +7,17 @@ publish = false [dependencies] anyhow.workspace = true +clap = { workspace = true, features = ["std", "help", "usage", "derive"] } +rustix.workspace = true +serde = { workspace = true, features = ["derive"] } +composefs.workspace = true +composefs-boot.workspace = true +toml.workspace = true [lints] workspace = true + +[features] +default = ['pre-6.15'] +rhel9 = ['composefs/rhel9'] +'pre-6.15' = ['composefs/pre-6.15'] diff --git a/crates/initramfs/bootc-root-setup.service b/crates/initramfs/bootc-root-setup.service index aab7de785..23525c7bc 100644 --- a/crates/initramfs/bootc-root-setup.service +++ b/crates/initramfs/bootc-root-setup.service @@ -2,8 +2,7 @@ Description=bootc setup root Documentation=man:bootc(1) DefaultDependencies=no -# For now -ConditionKernelCommandLine=ostree +ConditionKernelCommandLine=composefs ConditionPathExists=/etc/initrd-release After=sysroot.mount After=ostree-prepare-root.service diff --git a/crates/initramfs/src/main.rs b/crates/initramfs/src/main.rs index 51b5f551d..088c57a5a 100644 --- a/crates/initramfs/src/main.rs +++ b/crates/initramfs/src/main.rs @@ -1,24 +1,15 @@ //! Code for bootc that goes into the initramfs. -//! At the current time, this is mostly just a no-op. // SPDX-License-Identifier: Apache-2.0 OR MIT +mod mount; + use anyhow::Result; -fn setup_root() -> Result<()> { - let _ = std::fs::metadata("/sysroot/usr")?; - println!("setup OK"); - Ok(()) -} +use clap::Parser; +use mount::{gpt_workaround, setup_root, Args}; fn main() -> Result<()> { - let v = std::env::args().collect::>(); - let args = match v.as_slice() { - [] => anyhow::bail!("Missing argument".to_string()), - [_, rest @ ..] => rest, - }; - match args { - [] => anyhow::bail!("Missing argument".to_string()), - [s] if s == "setup-root" => setup_root(), - [o, ..] => anyhow::bail!(format!("Unknown command {o}")), - } + let args = Args::parse(); + gpt_workaround()?; + setup_root(args) } diff --git a/crates/initramfs/src/mount.rs b/crates/initramfs/src/mount.rs new file mode 100644 index 000000000..e1e2516ee --- /dev/null +++ b/crates/initramfs/src/mount.rs @@ -0,0 +1,294 @@ +//! Mount helpers for bootc-initramfs + +use std::{ + ffi::OsString, + fmt::Debug, + io::ErrorKind, + os::fd::{AsFd, OwnedFd}, + path::{Path, PathBuf}, +}; + +use anyhow::{Context, Result}; +use clap::Parser; +use rustix::{ + fs::{major, minor, mkdirat, openat, stat, symlink, Mode, OFlags, CWD}, + io::Errno, + mount::{ + fsconfig_create, fsconfig_set_string, fsmount, open_tree, unmount, FsMountFlags, + MountAttrFlags, OpenTreeFlags, UnmountFlags, + }, +}; +use serde::Deserialize; + +use composefs::{ + fsverity::{FsVerityHashValue, Sha256HashValue}, + mount::{mount_at, FsHandle}, + mountcompat::{overlayfs_set_fd, overlayfs_set_lower_and_data_fds, prepare_mount}, + repository::Repository, +}; +use composefs_boot::cmdline::get_cmdline_composefs; + +// Config file +#[derive(Clone, Copy, Debug, Deserialize)] +#[serde(rename_all = "lowercase")] +enum MountType { + None, + Bind, + Overlay, + Transient, +} + +#[derive(Debug, Default, Deserialize)] +struct RootConfig { + #[serde(default)] + transient: bool, +} + +#[derive(Debug, Default, Deserialize)] +struct MountConfig { + mount: Option, + #[serde(default)] + transient: bool, +} + +#[derive(Deserialize, Default)] +struct Config { + #[serde(default)] + etc: MountConfig, + #[serde(default)] + var: MountConfig, + #[serde(default)] + root: RootConfig, +} + +/// Command-line arguments +#[derive(Parser, Debug)] +#[command(version)] +pub struct Args { + #[arg(help = "Execute this command (for testing)")] + /// Execute this command (for testing) + pub cmd: Vec, + + #[arg( + long, + default_value = "/sysroot", + help = "sysroot directory in initramfs" + )] + /// sysroot directory in initramfs + pub sysroot: PathBuf, + + #[arg( + long, + default_value = "/usr/lib/composefs/setup-root-conf.toml", + help = "Config path (for testing)" + )] + /// Config path (for testing) + pub config: PathBuf, + + // we want to test in a userns, but can't mount erofs there + #[arg(long, help = "Bind mount root-fs from (for testing)")] + /// Bind mount root-fs from (for testing) + pub root_fs: Option, + + #[arg(long, help = "Kernel commandline args (for testing)")] + /// Kernel commandline args (for testing) + pub cmdline: Option, + + #[arg(long, help = "Mountpoint (don't replace sysroot, for testing)")] + /// Mountpoint (don't replace sysroot, for testing) + pub target: Option, +} + +// Helpers +fn open_dir(dirfd: impl AsFd, name: impl AsRef + Debug) -> rustix::io::Result { + openat( + dirfd, + name.as_ref(), + OFlags::PATH | OFlags::DIRECTORY | OFlags::CLOEXEC, + Mode::empty(), + ) + .inspect_err(|_| { + eprintln!("Failed to open dir {name:?}"); + }) +} + +fn ensure_dir(dirfd: impl AsFd, name: &str) -> rustix::io::Result { + match mkdirat(dirfd.as_fd(), name, 0o700.into()) { + Ok(()) | Err(Errno::EXIST) => {} + Err(err) => Err(err)?, + } + open_dir(dirfd, name) +} + +fn bind_mount(fd: impl AsFd, path: &str) -> rustix::io::Result { + open_tree( + fd.as_fd(), + path, + OpenTreeFlags::OPEN_TREE_CLONE + | OpenTreeFlags::OPEN_TREE_CLOEXEC + | OpenTreeFlags::AT_EMPTY_PATH, + ) + .inspect_err(|_| { + eprintln!("Open tree failed for {path}"); + }) +} + +fn mount_tmpfs() -> Result { + let tmpfs = FsHandle::open("tmpfs")?; + fsconfig_create(tmpfs.as_fd())?; + Ok(fsmount( + tmpfs.as_fd(), + FsMountFlags::FSMOUNT_CLOEXEC, + MountAttrFlags::empty(), + )?) +} + +fn overlay_state(base: impl AsFd, state: impl AsFd, source: &str) -> Result<()> { + let upper = ensure_dir(state.as_fd(), "upper")?; + let work = ensure_dir(state.as_fd(), "work")?; + + let overlayfs = FsHandle::open("overlay")?; + fsconfig_set_string(overlayfs.as_fd(), "source", source)?; + overlayfs_set_fd(overlayfs.as_fd(), "workdir", work.as_fd())?; + overlayfs_set_fd(overlayfs.as_fd(), "upperdir", upper.as_fd())?; + overlayfs_set_lower_and_data_fds(&overlayfs, base.as_fd(), None::)?; + fsconfig_create(overlayfs.as_fd())?; + let fs = fsmount( + overlayfs.as_fd(), + FsMountFlags::FSMOUNT_CLOEXEC, + MountAttrFlags::empty(), + )?; + + Ok(mount_at(fs, base, ".")?) +} + +fn overlay_transient(base: impl AsFd) -> Result<()> { + overlay_state(base, prepare_mount(mount_tmpfs()?)?, "transient") +} + +fn open_root_fs(path: &Path) -> Result { + let rootfs = open_tree( + CWD, + path, + OpenTreeFlags::OPEN_TREE_CLONE | OpenTreeFlags::OPEN_TREE_CLOEXEC, + )?; + + // https://github.com/bytecodealliance/rustix/issues/975 + // mount_setattr(rootfs.as_fd()), ..., { ... MountAttrFlags::MOUNT_ATTR_RDONLY ... }, ...)?; + + Ok(rootfs) +} + +/// Prepares a floating mount for composefs and returns the fd +/// +/// # Arguments +/// * sysroot - fd for /sysroot +/// * name - Name of the EROFS image to be mounted +/// * insecure - Whether fsverity is optional or not +pub fn mount_composefs_image(sysroot: &OwnedFd, name: &str, insecure: bool) -> Result { + let mut repo = Repository::::open_path(sysroot, "composefs")?; + repo.set_insecure(insecure); + repo.mount(name).context("Failed to mount composefs image") +} + +fn mount_subdir( + new_root: impl AsFd, + state: impl AsFd, + subdir: &str, + config: MountConfig, + default: MountType, +) -> Result<()> { + let mount_type = match config.mount { + Some(mt) => mt, + None => match config.transient { + true => MountType::Transient, + false => default, + }, + }; + + match mount_type { + MountType::None => Ok(()), + MountType::Bind => Ok(mount_at(bind_mount(&state, subdir)?, &new_root, subdir)?), + MountType::Overlay => overlay_state( + open_dir(&new_root, subdir)?, + open_dir(&state, subdir)?, + "overlay", + ), + MountType::Transient => overlay_transient(open_dir(&new_root, subdir)?), + } +} + +pub(crate) fn gpt_workaround() -> Result<()> { + // https://github.com/systemd/systemd/issues/35017 + let rootdev = stat("/dev/gpt-auto-root"); + + let rootdev = match rootdev { + Ok(r) => r, + Err(e) if e.kind() == ErrorKind::NotFound => return Ok(()), + Err(e) => Err(e)?, + }; + + let target = format!( + "/dev/block/{}:{}", + major(rootdev.st_rdev), + minor(rootdev.st_rdev) + ); + symlink(target, "/run/systemd/volatile-root")?; + Ok(()) +} + +/// Sets up /sysroot for switch-root +pub fn setup_root(args: Args) -> Result<()> { + let config = match std::fs::read_to_string(args.config) { + Ok(text) => toml::from_str(&text)?, + Err(err) if err.kind() == ErrorKind::NotFound => Config::default(), + Err(err) => Err(err)?, + }; + + 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::(cmdline)?; + + let new_root = match args.root_fs { + Some(path) => open_root_fs(&path).context("Failed to clone specified root fs")?, + None => mount_composefs_image(&sysroot, &image.to_hex(), insecure)?, + }; + + // we need to clone this before the next step to make sure we get the old one + let sysroot_clone = bind_mount(&sysroot, "")?; + + // 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(&new_root, CWD, &args.sysroot)?; + } + + if config.root.transient { + overlay_transient(&new_root)?; + } + + match mount_at(&sysroot_clone, &new_root, "sysroot") { + Ok(()) | Err(Errno::NOENT) => {} + Err(err) => Err(err)?, + } + + // etc + var + let state = open_dir(open_dir(&sysroot, "state/deploy")?, image.to_hex())?; + mount_subdir(&new_root, &state, "etc", config.etc, MountType::Overlay)?; + mount_subdir(&new_root, &state, "var", config.var, MountType::Bind)?; + + if cfg!(not(feature = "pre-6.15")) { + // Replace the /sysroot with the new composed root filesystem + unmount(&args.sysroot, UnmountFlags::DETACH)?; + mount_at(&new_root, CWD, &args.sysroot)?; + } + + Ok(()) +}