Skip to content

Commit 00e3739

Browse files
committed
initramfs: Mount /sysroot readonly for composefs by default
This implements readonly mounting of /sysroot for composefs systems, matching the behavior that ostree systems already have. Previously, composefs left /sysroot mounted read-write, which was inconsistent and meant the readonly tests had to be skipped for composefs. The implementation uses a direct `libc::syscall` wrapper for `mount_setattr` since rustix doesn't yet provide this API. The `MOUNT_ATTR_RDONLY` flag is applied recursively to three mount points during initramfs setup: - The composefs rootfs image mount (becomes `/` after switch-root) - The test root filesystem mount (used in testing scenarios) - The sysroot clone mount (becomes `/sysroot` in the booted system) With this change, the readonly /sysroot tests in test-status.nu now run for both ostree and composefs systems without conditional checks. Assisted-by: Claude Code (Sonnet 4.5) Signed-off-by: Colin Walters <[email protected]>
1 parent 71dc8e5 commit 00e3739

File tree

4 files changed

+68
-23
lines changed

4 files changed

+68
-23
lines changed

Cargo.lock

Lines changed: 1 addition & 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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ publish = false
88
[dependencies]
99
anyhow.workspace = true
1010
clap = { workspace = true, features = ["std", "help", "usage", "derive"] }
11+
libc.workspace = true
1112
rustix.workspace = true
1213
serde = { workspace = true, features = ["derive"] }
1314
composefs.workspace = true

crates/initramfs/src/lib.rs

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::{
44
ffi::OsString,
55
fmt::Debug,
66
io::ErrorKind,
7-
os::fd::{AsFd, OwnedFd},
7+
os::fd::{AsFd, AsRawFd, OwnedFd},
88
path::{Path, PathBuf},
99
};
1010

@@ -31,6 +31,49 @@ use composefs_boot::cmdline::get_cmdline_composefs;
3131

3232
use fn_error_context::context;
3333

34+
// mount_setattr syscall support
35+
const MOUNT_ATTR_RDONLY: u64 = 0x00000001;
36+
37+
#[repr(C)]
38+
struct MountAttr {
39+
attr_set: u64,
40+
attr_clr: u64,
41+
propagation: u64,
42+
userns_fd: u64,
43+
}
44+
45+
/// Set mount attributes using mount_setattr syscall
46+
#[context("Setting mount attributes")]
47+
#[allow(unsafe_code)]
48+
fn mount_setattr(fd: impl AsFd, flags: libc::c_int, attr: &MountAttr) -> Result<()> {
49+
let ret = unsafe {
50+
libc::syscall(
51+
libc::SYS_mount_setattr,
52+
fd.as_fd().as_raw_fd(),
53+
c"".as_ptr(),
54+
flags,
55+
attr as *const MountAttr,
56+
std::mem::size_of::<MountAttr>(),
57+
)
58+
};
59+
if ret == -1 {
60+
Err(std::io::Error::last_os_error())?;
61+
}
62+
Ok(())
63+
}
64+
65+
/// Set mount to readonly recursively
66+
#[context("Setting mount readonly recursively")]
67+
fn set_mount_readonly_recursive(fd: impl AsFd) -> Result<()> {
68+
let attr = MountAttr {
69+
attr_set: MOUNT_ATTR_RDONLY,
70+
attr_clr: 0,
71+
propagation: 0,
72+
userns_fd: 0,
73+
};
74+
mount_setattr(fd, libc::AT_EMPTY_PATH | libc::AT_RECURSIVE, &attr)
75+
}
76+
3477
// Config file
3578
#[derive(Clone, Copy, Debug, Deserialize)]
3679
#[serde(rename_all = "lowercase")]
@@ -193,8 +236,7 @@ fn open_root_fs(path: &Path) -> Result<OwnedFd> {
193236
OpenTreeFlags::OPEN_TREE_CLONE | OpenTreeFlags::OPEN_TREE_CLOEXEC,
194237
)?;
195238

196-
// https://github.com/bytecodealliance/rustix/issues/975
197-
// mount_setattr(rootfs.as_fd()), ..., { ... MountAttrFlags::MOUNT_ATTR_RDONLY ... }, ...)?;
239+
set_mount_readonly_recursive(&rootfs)?;
198240

199241
Ok(rootfs)
200242
}
@@ -209,7 +251,13 @@ fn open_root_fs(path: &Path) -> Result<OwnedFd> {
209251
pub fn mount_composefs_image(sysroot: &OwnedFd, name: &str, insecure: bool) -> Result<OwnedFd> {
210252
let mut repo = Repository::<Sha512HashValue>::open_path(sysroot, "composefs")?;
211253
repo.set_insecure(insecure);
212-
repo.mount(name).context("Failed to mount composefs image")
254+
let rootfs = repo
255+
.mount(name)
256+
.context("Failed to mount composefs image")?;
257+
258+
set_mount_readonly_recursive(&rootfs)?;
259+
260+
Ok(rootfs)
213261
}
214262

215263
#[context("Mounting subdirectory")]
@@ -292,6 +340,8 @@ pub fn setup_root(args: Args) -> Result<()> {
292340
// we need to clone this before the next step to make sure we get the old one
293341
let sysroot_clone = bind_mount(&sysroot, "")?;
294342

343+
set_mount_readonly_recursive(&sysroot_clone)?;
344+
295345
// Ideally we build the new root filesystem together before we mount it, but that only works on
296346
// 6.15 and later. Before 6.15 we can't mount into a floating tree, so mount it first. This
297347
// will leave an abandoned clone of the sysroot mounted under it, but that's OK for now.

tmt/tests/booted/readonly/001-test-status.nu

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,19 @@ use tap.nu
33

44
tap begin "verify bootc status output formats"
55

6+
# Verify /sysroot is not writable initially (read-only operations should not make it writable)
7+
let is_writable = (do -i { /bin/test -w /sysroot } | complete | get exit_code) == 0
8+
assert (not $is_writable) "/sysroot should not be writable initially"
9+
10+
# Double-check with findmnt
11+
let mnt = (findmnt /sysroot -J | from json)
12+
let opts = ($mnt.filesystems.0.options | split row ",")
13+
assert ($opts | any { |o| $o == "ro" }) "/sysroot should be mounted read-only"
14+
615
let st = bootc status --json | from json
716
# Detect composefs by checking if composefs field is present
817
let is_composefs = ($st.status.booted.composefs? != null)
918

10-
# FIXME: Should be mounting /sysroot readonly in composefs by default
11-
if not $is_composefs {
12-
# Verify /sysroot is not writable initially (read-only operations should not make it writable)
13-
let is_writable = (do -i { /bin/test -w /sysroot } | complete | get exit_code) == 0
14-
assert (not $is_writable) "/sysroot should not be writable initially"
15-
16-
# Double-check with findmnt
17-
let mnt = (findmnt /sysroot -J | from json)
18-
let opts = ($mnt.filesystems.0.options | split row ",")
19-
assert ($opts | any { |o| $o == "ro" }) "/sysroot should be mounted read-only"
20-
}
21-
22-
let st = bootc status --json | from json
2319
assert equal $st.apiVersion org.containers.bootc/v1
2420

2521
let st = bootc status --json --format-version=0 | from json
@@ -43,11 +39,8 @@ if not $is_composefs {
4339
assert (($st.status | get rollback | default null) == null)
4440
assert (($st.status | get staged | default null) == null)
4541

46-
# FIXME: See above re /sysroot ro
47-
if not $is_composefs {
48-
# Verify /sysroot is still not writable after bootc status (regression test for PR #1718)
49-
let is_writable = (do -i { /bin/test -w /sysroot } | complete | get exit_code) == 0
50-
assert (not $is_writable) "/sysroot should remain read-only after bootc status"
51-
}
42+
# Verify /sysroot is still not writable after bootc status (regression test for PR #1718)
43+
let is_writable = (do -i { /bin/test -w /sysroot } | complete | get exit_code) == 0
44+
assert (not $is_writable) "/sysroot should remain read-only after bootc status"
5245

5346
tap ok

0 commit comments

Comments
 (0)