diff --git a/Cargo.lock b/Cargo.lock index 80e7a7911..aba4f1e92 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -162,6 +162,7 @@ dependencies = [ "composefs", "composefs-boot", "fn-error-context", + "libc", "rustix", "serde", "toml", diff --git a/crates/initramfs/Cargo.toml b/crates/initramfs/Cargo.toml index 2617cbbf4..94bebd858 100644 --- a/crates/initramfs/Cargo.toml +++ b/crates/initramfs/Cargo.toml @@ -8,6 +8,7 @@ publish = false [dependencies] anyhow.workspace = true clap = { workspace = true, features = ["std", "help", "usage", "derive"] } +libc.workspace = true rustix.workspace = true serde = { workspace = true, features = ["derive"] } composefs.workspace = true diff --git a/crates/initramfs/src/lib.rs b/crates/initramfs/src/lib.rs index 4766c12a1..e15ad48ca 100644 --- a/crates/initramfs/src/lib.rs +++ b/crates/initramfs/src/lib.rs @@ -4,7 +4,7 @@ use std::{ ffi::OsString, fmt::Debug, io::ErrorKind, - os::fd::{AsFd, OwnedFd}, + os::fd::{AsFd, AsRawFd, OwnedFd}, path::{Path, PathBuf}, }; @@ -31,6 +31,49 @@ use composefs_boot::cmdline::get_cmdline_composefs; use fn_error_context::context; +// mount_setattr syscall support +const MOUNT_ATTR_RDONLY: u64 = 0x00000001; + +#[repr(C)] +struct MountAttr { + attr_set: u64, + attr_clr: u64, + propagation: u64, + userns_fd: u64, +} + +/// Set mount attributes using mount_setattr syscall +#[context("Setting mount attributes")] +#[allow(unsafe_code)] +fn mount_setattr(fd: impl AsFd, flags: libc::c_int, attr: &MountAttr) -> Result<()> { + let ret = unsafe { + libc::syscall( + libc::SYS_mount_setattr, + fd.as_fd().as_raw_fd(), + c"".as_ptr(), + flags, + attr as *const MountAttr, + std::mem::size_of::(), + ) + }; + if ret == -1 { + Err(std::io::Error::last_os_error())?; + } + Ok(()) +} + +/// Set mount to readonly +#[context("Setting mount readonly")] +fn set_mount_readonly(fd: impl AsFd) -> Result<()> { + let attr = MountAttr { + attr_set: MOUNT_ATTR_RDONLY, + attr_clr: 0, + propagation: 0, + userns_fd: 0, + }; + mount_setattr(fd, libc::AT_EMPTY_PATH, &attr) +} + // Config file #[derive(Clone, Copy, Debug, Deserialize)] #[serde(rename_all = "lowercase")] @@ -193,8 +236,7 @@ fn open_root_fs(path: &Path) -> Result { OpenTreeFlags::OPEN_TREE_CLONE | OpenTreeFlags::OPEN_TREE_CLOEXEC, )?; - // https://github.com/bytecodealliance/rustix/issues/975 - // mount_setattr(rootfs.as_fd()), ..., { ... MountAttrFlags::MOUNT_ATTR_RDONLY ... }, ...)?; + set_mount_readonly(&rootfs)?; Ok(rootfs) } @@ -209,7 +251,13 @@ fn open_root_fs(path: &Path) -> Result { 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") + let rootfs = repo + .mount(name) + .context("Failed to mount composefs image")?; + + set_mount_readonly(&rootfs)?; + + Ok(rootfs) } #[context("Mounting subdirectory")] @@ -292,6 +340,8 @@ pub fn setup_root(args: Args) -> Result<()> { // we need to clone this before the next step to make sure we get the old one let sysroot_clone = bind_mount(&sysroot, "")?; + set_mount_readonly_recursive(&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. diff --git a/tmt/tests/booted/readonly/001-test-status.nu b/tmt/tests/booted/readonly/001-test-status.nu index a4a119ae7..80c29028a 100644 --- a/tmt/tests/booted/readonly/001-test-status.nu +++ b/tmt/tests/booted/readonly/001-test-status.nu @@ -3,23 +3,19 @@ use tap.nu tap begin "verify bootc status output formats" +# Verify /sysroot is not writable initially (read-only operations should not make it writable) +let is_writable = (do -i { /bin/test -w /sysroot } | complete | get exit_code) == 0 +assert (not $is_writable) "/sysroot should not be writable initially" + +# Double-check with findmnt +let mnt = (findmnt /sysroot -J | from json) +let opts = ($mnt.filesystems.0.options | split row ",") +assert ($opts | any { |o| $o == "ro" }) "/sysroot should be mounted read-only" + let st = bootc status --json | from json # Detect composefs by checking if composefs field is present let is_composefs = ($st.status.booted.composefs? != null) -# FIXME: Should be mounting /sysroot readonly in composefs by default -if not $is_composefs { - # Verify /sysroot is not writable initially (read-only operations should not make it writable) - let is_writable = (do -i { /bin/test -w /sysroot } | complete | get exit_code) == 0 - assert (not $is_writable) "/sysroot should not be writable initially" - - # Double-check with findmnt - let mnt = (findmnt /sysroot -J | from json) - let opts = ($mnt.filesystems.0.options | split row ",") - assert ($opts | any { |o| $o == "ro" }) "/sysroot should be mounted read-only" -} - -let st = bootc status --json | from json assert equal $st.apiVersion org.containers.bootc/v1 let st = bootc status --json --format-version=0 | from json @@ -43,11 +39,8 @@ if not $is_composefs { assert (($st.status | get rollback | default null) == null) assert (($st.status | get staged | default null) == null) -# FIXME: See above re /sysroot ro -if not $is_composefs { - # Verify /sysroot is still not writable after bootc status (regression test for PR #1718) - let is_writable = (do -i { /bin/test -w /sysroot } | complete | get exit_code) == 0 - assert (not $is_writable) "/sysroot should remain read-only after bootc status" -} +# Verify /sysroot is still not writable after bootc status (regression test for PR #1718) +let is_writable = (do -i { /bin/test -w /sysroot } | complete | get exit_code) == 0 +assert (not $is_writable) "/sysroot should remain read-only after bootc status" tap ok