Skip to content

Commit 1dd55c2

Browse files
authored
Merge pull request #1550 from Johan-Liebert1/composefs-initramfs
Move composefs setup root to bootc initramfs
2 parents 694e0ab + 0ef1eca commit 1dd55c2

File tree

5 files changed

+319
-18
lines changed

5 files changed

+319
-18
lines changed

Cargo.lock

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/initramfs/Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@ publish = false
77

88
[dependencies]
99
anyhow.workspace = true
10+
clap = { workspace = true, features = ["std", "help", "usage", "derive"] }
11+
rustix.workspace = true
12+
serde = { workspace = true, features = ["derive"] }
13+
composefs.workspace = true
14+
composefs-boot.workspace = true
15+
toml.workspace = true
1016

1117
[lints]
1218
workspace = true
19+
20+
[features]
21+
default = ['pre-6.15']
22+
rhel9 = ['composefs/rhel9']
23+
'pre-6.15' = ['composefs/pre-6.15']

crates/initramfs/bootc-root-setup.service

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22
Description=bootc setup root
33
Documentation=man:bootc(1)
44
DefaultDependencies=no
5-
# For now
6-
ConditionKernelCommandLine=ostree
5+
ConditionKernelCommandLine=composefs
76
ConditionPathExists=/etc/initrd-release
87
After=sysroot.mount
98
After=ostree-prepare-root.service

crates/initramfs/src/main.rs

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,15 @@
11
//! Code for bootc that goes into the initramfs.
2-
//! At the current time, this is mostly just a no-op.
32
// SPDX-License-Identifier: Apache-2.0 OR MIT
43

4+
mod mount;
5+
56
use anyhow::Result;
67

7-
fn setup_root() -> Result<()> {
8-
let _ = std::fs::metadata("/sysroot/usr")?;
9-
println!("setup OK");
10-
Ok(())
11-
}
8+
use clap::Parser;
9+
use mount::{gpt_workaround, setup_root, Args};
1210

1311
fn main() -> Result<()> {
14-
let v = std::env::args().collect::<Vec<_>>();
15-
let args = match v.as_slice() {
16-
[] => anyhow::bail!("Missing argument".to_string()),
17-
[_, rest @ ..] => rest,
18-
};
19-
match args {
20-
[] => anyhow::bail!("Missing argument".to_string()),
21-
[s] if s == "setup-root" => setup_root(),
22-
[o, ..] => anyhow::bail!(format!("Unknown command {o}")),
23-
}
12+
let args = Args::parse();
13+
gpt_workaround()?;
14+
setup_root(args)
2415
}

crates/initramfs/src/mount.rs

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
//! Mount helpers for bootc-initramfs
2+
3+
use std::{
4+
ffi::OsString,
5+
fmt::Debug,
6+
io::ErrorKind,
7+
os::fd::{AsFd, OwnedFd},
8+
path::{Path, PathBuf},
9+
};
10+
11+
use anyhow::{Context, Result};
12+
use clap::Parser;
13+
use rustix::{
14+
fs::{major, minor, mkdirat, openat, stat, symlink, Mode, OFlags, CWD},
15+
io::Errno,
16+
mount::{
17+
fsconfig_create, fsconfig_set_string, fsmount, open_tree, unmount, FsMountFlags,
18+
MountAttrFlags, OpenTreeFlags, UnmountFlags,
19+
},
20+
};
21+
use serde::Deserialize;
22+
23+
use composefs::{
24+
fsverity::{FsVerityHashValue, Sha256HashValue},
25+
mount::{mount_at, FsHandle},
26+
mountcompat::{overlayfs_set_fd, overlayfs_set_lower_and_data_fds, prepare_mount},
27+
repository::Repository,
28+
};
29+
use composefs_boot::cmdline::get_cmdline_composefs;
30+
31+
// Config file
32+
#[derive(Clone, Copy, Debug, Deserialize)]
33+
#[serde(rename_all = "lowercase")]
34+
enum MountType {
35+
None,
36+
Bind,
37+
Overlay,
38+
Transient,
39+
}
40+
41+
#[derive(Debug, Default, Deserialize)]
42+
struct RootConfig {
43+
#[serde(default)]
44+
transient: bool,
45+
}
46+
47+
#[derive(Debug, Default, Deserialize)]
48+
struct MountConfig {
49+
mount: Option<MountType>,
50+
#[serde(default)]
51+
transient: bool,
52+
}
53+
54+
#[derive(Deserialize, Default)]
55+
struct Config {
56+
#[serde(default)]
57+
etc: MountConfig,
58+
#[serde(default)]
59+
var: MountConfig,
60+
#[serde(default)]
61+
root: RootConfig,
62+
}
63+
64+
/// Command-line arguments
65+
#[derive(Parser, Debug)]
66+
#[command(version)]
67+
pub struct Args {
68+
#[arg(help = "Execute this command (for testing)")]
69+
/// Execute this command (for testing)
70+
pub cmd: Vec<OsString>,
71+
72+
#[arg(
73+
long,
74+
default_value = "/sysroot",
75+
help = "sysroot directory in initramfs"
76+
)]
77+
/// sysroot directory in initramfs
78+
pub sysroot: PathBuf,
79+
80+
#[arg(
81+
long,
82+
default_value = "/usr/lib/composefs/setup-root-conf.toml",
83+
help = "Config path (for testing)"
84+
)]
85+
/// Config path (for testing)
86+
pub config: PathBuf,
87+
88+
// we want to test in a userns, but can't mount erofs there
89+
#[arg(long, help = "Bind mount root-fs from (for testing)")]
90+
/// Bind mount root-fs from (for testing)
91+
pub root_fs: Option<PathBuf>,
92+
93+
#[arg(long, help = "Kernel commandline args (for testing)")]
94+
/// Kernel commandline args (for testing)
95+
pub cmdline: Option<String>,
96+
97+
#[arg(long, help = "Mountpoint (don't replace sysroot, for testing)")]
98+
/// Mountpoint (don't replace sysroot, for testing)
99+
pub target: Option<PathBuf>,
100+
}
101+
102+
// Helpers
103+
fn open_dir(dirfd: impl AsFd, name: impl AsRef<Path> + Debug) -> rustix::io::Result<OwnedFd> {
104+
openat(
105+
dirfd,
106+
name.as_ref(),
107+
OFlags::PATH | OFlags::DIRECTORY | OFlags::CLOEXEC,
108+
Mode::empty(),
109+
)
110+
.inspect_err(|_| {
111+
eprintln!("Failed to open dir {name:?}");
112+
})
113+
}
114+
115+
fn ensure_dir(dirfd: impl AsFd, name: &str) -> rustix::io::Result<OwnedFd> {
116+
match mkdirat(dirfd.as_fd(), name, 0o700.into()) {
117+
Ok(()) | Err(Errno::EXIST) => {}
118+
Err(err) => Err(err)?,
119+
}
120+
open_dir(dirfd, name)
121+
}
122+
123+
fn bind_mount(fd: impl AsFd, path: &str) -> rustix::io::Result<OwnedFd> {
124+
open_tree(
125+
fd.as_fd(),
126+
path,
127+
OpenTreeFlags::OPEN_TREE_CLONE
128+
| OpenTreeFlags::OPEN_TREE_CLOEXEC
129+
| OpenTreeFlags::AT_EMPTY_PATH,
130+
)
131+
.inspect_err(|_| {
132+
eprintln!("Open tree failed for {path}");
133+
})
134+
}
135+
136+
fn mount_tmpfs() -> Result<OwnedFd> {
137+
let tmpfs = FsHandle::open("tmpfs")?;
138+
fsconfig_create(tmpfs.as_fd())?;
139+
Ok(fsmount(
140+
tmpfs.as_fd(),
141+
FsMountFlags::FSMOUNT_CLOEXEC,
142+
MountAttrFlags::empty(),
143+
)?)
144+
}
145+
146+
fn overlay_state(base: impl AsFd, state: impl AsFd, source: &str) -> Result<()> {
147+
let upper = ensure_dir(state.as_fd(), "upper")?;
148+
let work = ensure_dir(state.as_fd(), "work")?;
149+
150+
let overlayfs = FsHandle::open("overlay")?;
151+
fsconfig_set_string(overlayfs.as_fd(), "source", source)?;
152+
overlayfs_set_fd(overlayfs.as_fd(), "workdir", work.as_fd())?;
153+
overlayfs_set_fd(overlayfs.as_fd(), "upperdir", upper.as_fd())?;
154+
overlayfs_set_lower_and_data_fds(&overlayfs, base.as_fd(), None::<OwnedFd>)?;
155+
fsconfig_create(overlayfs.as_fd())?;
156+
let fs = fsmount(
157+
overlayfs.as_fd(),
158+
FsMountFlags::FSMOUNT_CLOEXEC,
159+
MountAttrFlags::empty(),
160+
)?;
161+
162+
Ok(mount_at(fs, base, ".")?)
163+
}
164+
165+
fn overlay_transient(base: impl AsFd) -> Result<()> {
166+
overlay_state(base, prepare_mount(mount_tmpfs()?)?, "transient")
167+
}
168+
169+
fn open_root_fs(path: &Path) -> Result<OwnedFd> {
170+
let rootfs = open_tree(
171+
CWD,
172+
path,
173+
OpenTreeFlags::OPEN_TREE_CLONE | OpenTreeFlags::OPEN_TREE_CLOEXEC,
174+
)?;
175+
176+
// https://github.com/bytecodealliance/rustix/issues/975
177+
// mount_setattr(rootfs.as_fd()), ..., { ... MountAttrFlags::MOUNT_ATTR_RDONLY ... }, ...)?;
178+
179+
Ok(rootfs)
180+
}
181+
182+
/// Prepares a floating mount for composefs and returns the fd
183+
///
184+
/// # Arguments
185+
/// * sysroot - fd for /sysroot
186+
/// * name - Name of the EROFS image to be mounted
187+
/// * insecure - Whether fsverity is optional or not
188+
pub fn mount_composefs_image(sysroot: &OwnedFd, name: &str, insecure: bool) -> Result<OwnedFd> {
189+
let mut repo = Repository::<Sha256HashValue>::open_path(sysroot, "composefs")?;
190+
repo.set_insecure(insecure);
191+
repo.mount(name).context("Failed to mount composefs image")
192+
}
193+
194+
fn mount_subdir(
195+
new_root: impl AsFd,
196+
state: impl AsFd,
197+
subdir: &str,
198+
config: MountConfig,
199+
default: MountType,
200+
) -> Result<()> {
201+
let mount_type = match config.mount {
202+
Some(mt) => mt,
203+
None => match config.transient {
204+
true => MountType::Transient,
205+
false => default,
206+
},
207+
};
208+
209+
match mount_type {
210+
MountType::None => Ok(()),
211+
MountType::Bind => Ok(mount_at(bind_mount(&state, subdir)?, &new_root, subdir)?),
212+
MountType::Overlay => overlay_state(
213+
open_dir(&new_root, subdir)?,
214+
open_dir(&state, subdir)?,
215+
"overlay",
216+
),
217+
MountType::Transient => overlay_transient(open_dir(&new_root, subdir)?),
218+
}
219+
}
220+
221+
pub(crate) fn gpt_workaround() -> Result<()> {
222+
// https://github.com/systemd/systemd/issues/35017
223+
let rootdev = stat("/dev/gpt-auto-root");
224+
225+
let rootdev = match rootdev {
226+
Ok(r) => r,
227+
Err(e) if e.kind() == ErrorKind::NotFound => return Ok(()),
228+
Err(e) => Err(e)?,
229+
};
230+
231+
let target = format!(
232+
"/dev/block/{}:{}",
233+
major(rootdev.st_rdev),
234+
minor(rootdev.st_rdev)
235+
);
236+
symlink(target, "/run/systemd/volatile-root")?;
237+
Ok(())
238+
}
239+
240+
/// Sets up /sysroot for switch-root
241+
pub fn setup_root(args: Args) -> Result<()> {
242+
let config = match std::fs::read_to_string(args.config) {
243+
Ok(text) => toml::from_str(&text)?,
244+
Err(err) if err.kind() == ErrorKind::NotFound => Config::default(),
245+
Err(err) => Err(err)?,
246+
};
247+
248+
let sysroot = open_dir(CWD, &args.sysroot)
249+
.with_context(|| format!("Failed to open sysroot {:?}", args.sysroot))?;
250+
251+
let cmdline = match &args.cmdline {
252+
Some(cmdline) => cmdline,
253+
// TODO: Deduplicate this with composefs branch karg parser
254+
None => &std::fs::read_to_string("/proc/cmdline")?,
255+
};
256+
let (image, insecure) = get_cmdline_composefs::<Sha256HashValue>(cmdline)?;
257+
258+
let new_root = match args.root_fs {
259+
Some(path) => open_root_fs(&path).context("Failed to clone specified root fs")?,
260+
None => mount_composefs_image(&sysroot, &image.to_hex(), insecure)?,
261+
};
262+
263+
// we need to clone this before the next step to make sure we get the old one
264+
let sysroot_clone = bind_mount(&sysroot, "")?;
265+
266+
// Ideally we build the new root filesystem together before we mount it, but that only works on
267+
// 6.15 and later. Before 6.15 we can't mount into a floating tree, so mount it first. This
268+
// will leave an abandoned clone of the sysroot mounted under it, but that's OK for now.
269+
if cfg!(feature = "pre-6.15") {
270+
mount_at(&new_root, CWD, &args.sysroot)?;
271+
}
272+
273+
if config.root.transient {
274+
overlay_transient(&new_root)?;
275+
}
276+
277+
match mount_at(&sysroot_clone, &new_root, "sysroot") {
278+
Ok(()) | Err(Errno::NOENT) => {}
279+
Err(err) => Err(err)?,
280+
}
281+
282+
// etc + var
283+
let state = open_dir(open_dir(&sysroot, "state/deploy")?, image.to_hex())?;
284+
mount_subdir(&new_root, &state, "etc", config.etc, MountType::Overlay)?;
285+
mount_subdir(&new_root, &state, "var", config.var, MountType::Bind)?;
286+
287+
if cfg!(not(feature = "pre-6.15")) {
288+
// Replace the /sysroot with the new composed root filesystem
289+
unmount(&args.sysroot, UnmountFlags::DETACH)?;
290+
mount_at(&new_root, CWD, &args.sysroot)?;
291+
}
292+
293+
Ok(())
294+
}

0 commit comments

Comments
 (0)