Skip to content
Merged
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
6 changes: 6 additions & 0 deletions Cargo.lock

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

11 changes: 11 additions & 0 deletions crates/initramfs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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']
3 changes: 1 addition & 2 deletions crates/initramfs/bootc-root-setup.service
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 7 additions & 16 deletions crates/initramfs/src/main.rs
Original file line number Diff line number Diff line change
@@ -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::<Vec<_>>();
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)
}
294 changes: 294 additions & 0 deletions crates/initramfs/src/mount.rs
Original file line number Diff line number Diff line change
@@ -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<MountType>,
#[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<OsString>,

#[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<PathBuf>,

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

#[arg(long, help = "Mountpoint (don't replace sysroot, for testing)")]
/// Mountpoint (don't replace sysroot, for testing)
pub target: Option<PathBuf>,
Comment on lines +97 to +99
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The command-line argument target is defined here but it appears to be unused in the setup_root function. The logic in setup_root always uses args.sysroot as the mount destination. If target is intended for testing purposes to provide an alternative mount point, it should be utilized in the mounting logic.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well spotted!

}

// Helpers
fn open_dir(dirfd: impl AsFd, name: impl AsRef<Path> + Debug) -> rustix::io::Result<OwnedFd> {
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<OwnedFd> {
match mkdirat(dirfd.as_fd(), name, 0o700.into()) {
Ok(()) | Err(Errno::EXIST) => {}
Err(err) => Err(err)?,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The use of Err(err)? in the match arm is a bit unconventional and can be confusing. It constructs a Result::Err variant only to immediately use the ? operator on it. A more direct and clearer way to propagate the error is to use a return statement.

Suggested change
Err(err) => Err(err)?,
Err(err) => return Err(err),

}
open_dir(dirfd, name)
}

fn bind_mount(fd: impl AsFd, path: &str) -> rustix::io::Result<OwnedFd> {
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<OwnedFd> {
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::<OwnedFd>)?;
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<OwnedFd> {
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<OwnedFd> {
let mut repo = Repository::<Sha256HashValue>::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,
},
};
Comment on lines +201 to +207
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The nested match statement to determine mount_type can be simplified for better readability. Using Option::unwrap_or with an if expression can make the logic more concise and easier to follow.

    let mount_type = config.mount.unwrap_or(if config.transient {
        MountType::Transient
    } else {
        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")?,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a new problem per se but this raises the now-more-obvious issue that we added a kargs parser into bootc, and not into composefs-boot...

Except though of course the "detect composefs" karg parsing is only on the composefs branch...

Let's just add a // TODO: Deduplicate this with composefs branch karg parser?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about using the new parser, but it's in bootc-lib and including the entire create for just one function didn't make much sense. Maybe we should move the cmdline parser to bootc/utils?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now, I've just added the TODO comment

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah +1 to moving the cmdline parser to bootc-utils, I'll go do that now

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feels slightly large for utils? Some of the other crates we're adding are pretty small so a bootc-kargs crate seems nicer to me?

Or, slicing this differently...a general thing that's weird now is that lib contains most of the code.

But we could redo things so that there's e.g.:

  • utils
  • lib (general grab-bag of stuff like kargs, but not quite "utils")
  • core (what is currently lib)
  • cli

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 I'll ask claude nicely to redo it into its own crate. I'll probably call it bootc-kernel-cmdline though to disambiguate from the stuff in crates/lib/src/bootc_kargs.rs.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

➡️ #1554

};
let (image, insecure) = get_cmdline_composefs::<Sha256HashValue>(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)?,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The use of Err(err)? in this match arm is a bit unconventional. While correct, it can be made more explicit and readable. Using return Err(err.into()) would clearly show the error propagation and the conversion to an anyhow::Error.

        Err(err) => return Err(err.into()),

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW with stuff like this I personally am fine one marks it resolved without changing anything

}

// 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(())
}