From 67de08243583ec06b1111773a795b00a98f9fea2 Mon Sep 17 00:00:00 2001 From: Evan Goode Date: Wed, 4 Mar 2026 15:27:33 -0500 Subject: [PATCH 1/4] initramfs: Allow mounting overlay with flags Signed-off-by: Evan Goode --- crates/initramfs/src/lib.rs | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/crates/initramfs/src/lib.rs b/crates/initramfs/src/lib.rs index 80016b050..e21b9ddb6 100644 --- a/crates/initramfs/src/lib.rs +++ b/crates/initramfs/src/lib.rs @@ -219,6 +219,7 @@ fn overlay_state( state: impl AsFd, source: &str, mode: Option, + mount_attr_flags: Option, ) -> Result<()> { let upper = ensure_dir(state.as_fd(), "upper", mode)?; let work = ensure_dir(state.as_fd(), "work", mode)?; @@ -232,7 +233,7 @@ fn overlay_state( let fs = fsmount( overlayfs.as_fd(), FsMountFlags::FSMOUNT_CLOEXEC, - MountAttrFlags::empty(), + mount_attr_flags.unwrap_or(MountAttrFlags::empty()), )?; mount_at_wrapper(fs, base, ".").context("Moving mount") @@ -240,8 +241,18 @@ fn overlay_state( /// Mounts a transient overlayfs with passed in fd as the lowerdir #[context("Mounting transient overlayfs")] -pub fn overlay_transient(base: impl AsFd, mode: Option) -> Result<()> { - overlay_state(base, prepare_mount(mount_tmpfs()?)?, "transient", mode) +pub fn overlay_transient( + base: impl AsFd, + mode: Option, + mount_attr_flags: Option, +) -> Result<()> { + overlay_state( + base, + prepare_mount(mount_tmpfs()?)?, + "transient", + mode, + mount_attr_flags, + ) } #[context("Opening rootfs")] @@ -309,8 +320,9 @@ pub fn mount_subdir( open_dir(&state, subdir)?, "overlay", None, + None, ), - MountType::Transient => overlay_transient(open_dir(&new_root, subdir)?, None), + MountType::Transient => overlay_transient(open_dir(&new_root, subdir)?, None, None), } } @@ -373,7 +385,7 @@ pub fn setup_root(args: Args) -> Result<()> { } if config.root.transient { - overlay_transient(&new_root, None)?; + overlay_transient(&new_root, None, None)?; } match composefs::mount::mount_at(&sysroot_clone, &new_root, "sysroot") { From 8d95f8d8f909d6c9db0538a85c690c38e5a2fbdf Mon Sep 17 00:00:00 2001 From: Evan Goode Date: Wed, 4 Mar 2026 16:08:07 -0500 Subject: [PATCH 2/4] usroverlay: Add --read-only flag Signed-off-by: Evan Goode --- crates/lib/src/bootc_composefs/state.rs | 12 ++++++--- crates/lib/src/cli.rs | 35 ++++++++++++++++++------- 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/crates/lib/src/bootc_composefs/state.rs b/crates/lib/src/bootc_composefs/state.rs index 10cff0481..3be32090b 100644 --- a/crates/lib/src/bootc_composefs/state.rs +++ b/crates/lib/src/bootc_composefs/state.rs @@ -21,6 +21,7 @@ use ostree_ext::container::deploy::ORIGIN_CONTAINER; use rustix::{ fd::AsFd, fs::{Mode, OFlags, StatVfsMountFlags, open}, + mount::MountAttrFlags, path::Arg, }; @@ -330,7 +331,7 @@ pub(crate) async fn write_composefs_state( Ok(()) } -pub(crate) fn composefs_usr_overlay() -> Result<()> { +pub(crate) fn composefs_usr_overlay(access_mode: FilesystemOverlayAccessMode) -> Result<()> { let status = get_composefs_usr_overlay_status()?; if status.is_some() { println!("An overlayfs is already mounted on /usr"); @@ -343,9 +344,14 @@ pub(crate) fn composefs_usr_overlay() -> Result<()> { let usr_metadata = usr.metadata(".").context("Getting /usr metadata")?; let usr_mode = Mode::from_raw_mode(usr_metadata.permissions().mode()); - overlay_transient(usr, Some(usr_mode))?; + let mount_attr_flags = match access_mode { + FilesystemOverlayAccessMode::ReadOnly => Some(MountAttrFlags::MOUNT_ATTR_RDONLY), + FilesystemOverlayAccessMode::ReadWrite => None, + }; - println!("A writeable overlayfs is now mounted on /usr"); + overlay_transient(usr, Some(usr_mode), mount_attr_flags)?; + + println!("A {} overlayfs is now mounted on /usr", access_mode); println!("All changes there will be discarded on reboot."); Ok(()) diff --git a/crates/lib/src/cli.rs b/crates/lib/src/cli.rs index c0c1ba216..4737dbd95 100644 --- a/crates/lib/src/cli.rs +++ b/crates/lib/src/cli.rs @@ -50,6 +50,7 @@ use crate::bootc_composefs::{ use crate::deploy::{MergeState, RequiredHostSpec}; use crate::podstorage::set_additional_image_store; use crate::progress_jsonl::{ProgressWriter, RawProgressFd}; +use crate::spec::FilesystemOverlayAccessMode; use crate::spec::Host; use crate::spec::ImageReference; use crate::status::get_host; @@ -265,6 +266,14 @@ pub(crate) struct StatusOpts { pub(crate) verbose: bool, } +/// Add a transient overlayfs on /usr +#[derive(Debug, Parser, PartialEq, Eq)] +pub(crate) struct UsrOverlayOpts { + /// Mount the overlayfs as read-only. + #[clap(long)] + pub(crate) read_only: bool, +} + #[derive(Debug, clap::Subcommand, PartialEq, Eq)] pub(crate) enum InstallOpts { /// Install to the target block device. @@ -753,7 +762,7 @@ pub(crate) enum Opt { /// /// Allows temporary package installation that will be discarded on reboot. #[clap(alias = "usroverlay")] - UsrOverlay, + UsrOverlay(UsrOverlayOpts), /// Install the running container to a target. /// /// Takes a container image and installs it to disk in a bootable format. @@ -1410,13 +1419,16 @@ async fn edit(opts: EditOpts) -> Result<()> { } /// Implementation of `bootc usroverlay` -async fn usroverlay() -> Result<()> { +async fn usroverlay(access_mode: FilesystemOverlayAccessMode) -> Result<()> { // This is just a pass-through today. At some point we may make this a libostree API // or even oxidize it. - Err(Command::new("ostree") - .args(["admin", "unlock"]) - .exec() - .into()) + let args = match access_mode { + // In this context, "--transient" means "read-only overlay" + FilesystemOverlayAccessMode::ReadOnly => ["admin", "unlock", "--transient"].as_slice(), + + FilesystemOverlayAccessMode::ReadWrite => ["admin", "unlock"].as_slice(), + }; + Err(Command::new("ostree").args(args).exec().into()) } /// Perform process global initialization. This should be called as early as possible @@ -1527,12 +1539,17 @@ async fn run_from_opt(opt: Opt) -> Result<()> { Ok(()) } Opt::Edit(opts) => edit(opts).await, - Opt::UsrOverlay => { + Opt::UsrOverlay(opts) => { use crate::store::Environment; let env = Environment::detect()?; + let access_mode = if opts.read_only { + FilesystemOverlayAccessMode::ReadOnly + } else { + FilesystemOverlayAccessMode::ReadWrite + }; match env { - Environment::OstreeBooted => usroverlay().await, - Environment::ComposefsBooted(_) => composefs_usr_overlay(), + Environment::OstreeBooted => usroverlay(access_mode).await, + Environment::ComposefsBooted(_) => composefs_usr_overlay(access_mode), _ => anyhow::bail!("usroverlay only applies on booted hosts"), } } From 5089d27a49884313b185493449c6d4a63f089d9e Mon Sep 17 00:00:00 2001 From: Evan Goode Date: Wed, 4 Mar 2026 16:14:49 -0500 Subject: [PATCH 3/4] doc: Adjust for usroverlay --read-only Signed-off-by: Evan Goode --- crates/lib/src/cli.rs | 6 ++++-- docs/src/man/bootc-usr-overlay.8.md | 14 ++++++++++---- docs/src/man/bootc.8.md | 2 +- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/crates/lib/src/cli.rs b/crates/lib/src/cli.rs index 4737dbd95..5cf4ee8fd 100644 --- a/crates/lib/src/cli.rs +++ b/crates/lib/src/cli.rs @@ -269,7 +269,9 @@ pub(crate) struct StatusOpts { /// Add a transient overlayfs on /usr #[derive(Debug, Parser, PartialEq, Eq)] pub(crate) struct UsrOverlayOpts { - /// Mount the overlayfs as read-only. + /// Mount the overlayfs as read-only. A read-only overlayfs is useful since it may be remounted + /// as read/write in a private mount namespace and written to while the mount point remains + /// read-only to the rest of the system. #[clap(long)] pub(crate) read_only: bool, } @@ -758,7 +760,7 @@ pub(crate) enum Opt { /// /// Shows bootc system state. Outputs YAML by default, human-readable if terminal detected. Status(StatusOpts), - /// Add a transient writable overlayfs on `/usr`. + /// Add a transient overlayfs on `/usr`. /// /// Allows temporary package installation that will be discarded on reboot. #[clap(alias = "usroverlay")] diff --git a/docs/src/man/bootc-usr-overlay.8.md b/docs/src/man/bootc-usr-overlay.8.md index afb032ad1..c7bae74c3 100644 --- a/docs/src/man/bootc-usr-overlay.8.md +++ b/docs/src/man/bootc-usr-overlay.8.md @@ -1,7 +1,7 @@ # NAME -bootc-usr-overlay - Adds a transient writable overlayfs on `/usr` that -will be discarded on reboot +bootc-usr-overlay - Adds a transient overlayfs on `/usr` that will be discarded +on reboot # SYNOPSIS @@ -9,8 +9,8 @@ will be discarded on reboot # DESCRIPTION -Adds a transient writable overlayfs on `/usr` that will be discarded -on reboot. +Adds a transient overlayfs on `/usr` that will be discarded on reboot. The +overlayfs is read/write by default. ## USE CASES @@ -31,7 +31,13 @@ Almost always, a system process will hold a reference to the open mount point. You can however invoke `umount -l /usr` to perform a "lazy unmount". +# OPTIONS + +**--read-only** + + Mount the overlayfs as read-only. A read-only overlayfs is useful since it may be remounted as read/write in a private mount namespace and written to while the mount point remains read-only to the rest of the system + # VERSION diff --git a/docs/src/man/bootc.8.md b/docs/src/man/bootc.8.md index dd9ee7d18..99e673afc 100644 --- a/docs/src/man/bootc.8.md +++ b/docs/src/man/bootc.8.md @@ -30,7 +30,7 @@ pulled and `bootc upgrade`. | **bootc rollback** | Change the bootloader entry ordering; the deployment under `rollback` will be queued for the next boot, and the current will become rollback. If there is a `staged` entry (an unapplied, queued upgrade) then it will be discarded | | **bootc edit** | Apply full changes to the host specification | | **bootc status** | Display status | -| **bootc usr-overlay** | Add a transient writable overlayfs on `/usr` | +| **bootc usr-overlay** | Add a transient overlayfs on `/usr` | | **bootc install** | Install the running container to a target | | **bootc container** | Operations which can be executed as part of a container build | | **bootc composefs-finalize-staged** | | From c0868a3b8408d5b09d13bd16a45cd5c9a7c1f792 Mon Sep 17 00:00:00 2001 From: Evan Goode Date: Wed, 4 Mar 2026 17:57:58 -0500 Subject: [PATCH 4/4] test: usroverlay --read-only Signed-off-by: Evan Goode --- tmt/tests/booted/test-usroverlay.nu | 33 ++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/tmt/tests/booted/test-usroverlay.nu b/tmt/tests/booted/test-usroverlay.nu index c2dccb913..ed406e3c3 100644 --- a/tmt/tests/booted/test-usroverlay.nu +++ b/tmt/tests/booted/test-usroverlay.nu @@ -8,33 +8,46 @@ use std assert use tap.nu use bootc_testlib.nu +def usr_is_writable []: nothing -> bool { + (do -i { /bin/test -w /usr } | complete | get exit_code) == 0 +} + # Status should initially report no overlay in JSON -let status_json_before = bootc status --json | from json -assert ($status_json_before.status.usrOverlay? == null) +let status_before = bootc status --json | from json +assert ($status_before.status.usrOverlay? == null) # We should start out in a non-writable state on each boot -let is_writable = (do -i { /bin/test -w /usr } | complete | get exit_code) == 0 -assert (not $is_writable) +assert (not (usr_is_writable)) def initial_run [] { bootc usroverlay - let is_writable = (do -i { /bin/test -w /usr } | complete | get exit_code) == 0 - assert ($is_writable) + assert (usr_is_writable) # After `usroverlay`, status JSON should report a transient read/write overlay - let status_json_after = bootc status --json | from json - let overlay = $status_json_after.status.usrOverlay + let status_after = bootc status --json | from json + let overlay = $status_after.status.usrOverlay assert ($overlay.accessMode == "readWrite") assert ($overlay.persistence == "transient") bootc_testlib reboot } -# The second boot; verify we're in the derived image def second_boot [] { - # After reboot, usr overlay should be gone + # After reboot, /usr overlay should be gone let status_after_reboot = bootc status --json | from json assert ($status_after_reboot.status.usrOverlay? == null) + # And /usr should not be writable + assert (not (usr_is_writable)) + + # Mount a read-only /usr overlay + bootc usroverlay --read-only + assert (not (usr_is_writable)) + + # After `usroverlay --read-only`, status should report a transient read-only overlay + let status_after_readonly = bootc status --json | from json + let overlay = $status_after_readonly.status.usrOverlay + assert ($overlay.accessMode == "readOnly") + assert ($overlay.persistence == "transient") } def main [] {