Skip to content
Open
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
27 changes: 27 additions & 0 deletions crates/blockdev/src/blockdev.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ pub struct Device {
pub path: Option<String>,
/// Partition table type (e.g., "gpt", "dos"). Only present on whole disk devices.
pub pttype: Option<String>,
/// Whether the device is read-only (e.g. a loopback over an immutable
/// rootfs image on a live ISO). `None` if not reported by `lsblk`.
pub ro: Option<bool>,
}

impl Device {
Expand Down Expand Up @@ -504,6 +507,30 @@ pub fn list_dev_by_dir(dir: &Dir) -> Result<Device> {
list_dev(&Utf8PathBuf::from(source))
}

/// Determine whether the block device backing the filesystem mounted at the
/// given directory is physically read-only.
///
/// This is the case for e.g. a live ISO, where `/sysroot` is a loopback device
/// over an immutable rootfs image and the kernel will reject any attempt to
/// remount it read-write.
///
/// Returns `Ok(None)` when the backing device cannot be determined (for example
/// when the filesystem source is not a real block device, such as an overlay),
/// leaving the decision to the caller.
pub fn is_dir_backing_device_ro(dir: &Dir) -> Result<Option<bool>> {
let fsinfo = bootc_mount::inspect_filesystem_of_dir(dir)?;
let source = &fsinfo.source;
// Only real block device nodes can be queried via lsblk; sources like
// "overlay" or a ZFS dataset are not physical devices we can interrogate
// for a read-only flag here.
if !source.starts_with("/dev/") {
tracing::debug!("Filesystem source {source} is not a block device node");
return Ok(None);
}
let dev = list_dev(&Utf8PathBuf::from(source))?;
Ok(dev.ro)
}

pub struct LoopbackDevice {
pub dev: Option<Utf8PathBuf>,
// Handle to the cleanup helper process
Expand Down
1 change: 1 addition & 0 deletions crates/lib/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -999,6 +999,7 @@ pub(crate) async fn get_storage() -> Result<crate::store::BootedStorage> {
let r = BootedStorage::new(env)
.await?
.ok_or_else(|| anyhow!("System not booted via bootc"))?;
r.require_writable()?;
Ok(r)
}

Expand Down
6 changes: 6 additions & 0 deletions crates/lib/src/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,12 @@ pub struct HostStatus {

/// The state of the overlay mounted on /usr
pub usr_overlay: Option<FilesystemOverlay>,

/// Set to true if the physical root (`/sysroot`) is on a read-only medium
/// (e.g. a live ISO) and so cannot be mutated; commands that would change
/// the system (upgrade, switch, etc.) are not available.
#[serde(default)]
pub read_only: bool,
}

pub(crate) struct DeploymentEntry<'a> {
Expand Down
8 changes: 7 additions & 1 deletion crates/lib/src/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,8 @@ pub(crate) fn get_status(
rollback_queued,
ty,
usr_overlay,
// Set by callers that have storage context (e.g. get_host).
read_only: false,
};
Ok((deployments, host))
}
Expand All @@ -452,7 +454,7 @@ pub(crate) async fn get_host() -> Result<Host> {
return Ok(Host::default());
};

let host = match storage.kind() {
let mut host = match storage.kind() {
Ok(kind) => match kind {
BootedStorageKind::Ostree(booted_ostree) => {
let (_deployments, host) = get_status(&booted_ostree)?;
Expand All @@ -469,6 +471,10 @@ pub(crate) async fn get_host() -> Result<Host> {
}
};

// Surface whether the physical root is on a read-only medium (e.g. a live
// ISO), so consumers know mutating operations are unavailable.
host.status.read_only = storage.is_ro;

Ok(host)
}

Expand Down
85 changes: 73 additions & 12 deletions crates/lib/src/store/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ use ostree_ext::container_utils::ostree_booted;
use ostree_ext::prelude::FileExt;
use ostree_ext::sysroot::SysrootLock;
use ostree_ext::{gio, ostree};
use rustix::fs::Mode;
use rustix::{fd::AsFd, fs::Mode, fs::StatVfsMountFlags};

use composefs::fsverity::Sha512HashValue;
use composefs::repository::RepositoryConfig;
Expand Down Expand Up @@ -302,15 +302,51 @@ pub(crate) enum BootedStorageKind<'a> {
}

/// Open the physical root (/sysroot) and /run directories for a booted system.
fn get_physical_root_and_run() -> Result<(Dir, Dir)> {
let physical_root = {
let d = Dir::open_ambient_dir("/sysroot", cap_std::ambient_authority())
.context("Opening /sysroot")?;
open_dir_remount_rw(&d, ".".into())?
};
///
/// The returned boolean is `true` if `/sysroot` is on a physically read-only
/// medium (e.g. a live ISO) that cannot be remounted writable.
///
/// Note that ostree mounts `/sysroot` read-only by default even on writable
/// systems and remounts it writable on demand, so being read-only is not by
/// itself meaningful: we only conclude the system is read-only if a remount
/// attempt *fails* and the backing block device confirms it.
fn get_physical_root_and_run() -> Result<(Dir, Dir, bool)> {
let d = Dir::open_ambient_dir("/sysroot", cap_std::ambient_authority())
.context("Opening /sysroot")?;
let is_ro = sysroot_is_read_only(&d)?;
let physical_root = d.open_dir(".").context("Opening /sysroot")?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Do we need to reopen sysroot here?

let run =
Dir::open_ambient_dir("/run", cap_std::ambient_authority()).context("Opening /run")?;
Ok((physical_root, run))
Ok((physical_root, run, is_ro))
}

/// Determine whether `/sysroot` (given as `d`) is on a physically read-only
/// block device, remounting it read-write as a side effect when necessary.
///
/// ostree mounts `/sysroot` read-only by default even on writable systems and
/// remounts it writable on demand, so the read-only mount flag alone is not
/// meaningful. The authoritative signal is the backing block device's read-only
/// flag, which is set for a live ISO (a read-only loop device over an immutable
/// rootfs image) and propagates from a whole disk to its partitions. We check
/// that first and avoid even attempting a remount that is bound to fail.
fn sysroot_is_read_only(d: &Dir) -> Result<bool> {
let st = rustix::fs::fstatvfs(d.as_fd())?;
if !st.f_flag.contains(StatVfsMountFlags::RDONLY) {
// Already writable: the common case.
return Ok(false);
}

// The backing block device is physically read-only (e.g. a live ISO); there
// is no point attempting a remount, and write operations are not possible.
if matches!(bootc_blockdev::is_dir_backing_device_ro(d), Ok(Some(true))) {
return Ok(true);
}

// The mount is read-only but the device is writable: this is the normal
// ostree case where /sysroot is mounted read-only by default. Remount it

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Here and a few places else: we shouldn't only be mentioning ostree as the composefs backend also does the same

// writable on demand; a failure here is an unexpected error.
open_dir_remount_rw(d, ".".into())?;
Ok(false)
}

impl BootedStorage {
Expand All @@ -321,7 +357,7 @@ impl BootedStorage {
pub(crate) async fn new(env: Environment) -> Result<Option<Self>> {
let r = match &env {
Environment::ComposefsBooted(cmdline) => {
let (physical_root, run) = get_physical_root_and_run()?;
let (physical_root, run, is_ro) = get_physical_root_and_run()?;
let mut composefs = ComposefsRepository::open_path(&physical_root, COMPOSEFS)?;
if cmdline.allow_missing_fsverity {
composefs.set_insecure();
Expand All @@ -346,6 +382,7 @@ impl BootedStorage {
let storage = Storage {
physical_root,
physical_root_path: Utf8PathBuf::from("/sysroot"),
is_ro,
run,
boot_dir: Some(boot_dir),
esp: Some(esp_mount),
Expand All @@ -372,16 +409,25 @@ impl BootedStorage {
// remount /sysroot as writable, and we call set_mount_namespace_in_use()
// to indicate we're in a mount namespace. Without actually being in a
// mount namespace, this would leave the global /sysroot writable.
let (physical_root, run) = get_physical_root_and_run()?;
let (physical_root, run, is_ro) = get_physical_root_and_run()?;

let sysroot = ostree::Sysroot::new_default();
sysroot.set_mount_namespace_in_use();
let sysroot = ostree_ext::sysroot::SysrootLock::new_from_sysroot(&sysroot).await?;
// On a read-only sysroot (e.g. a live ISO) we must NOT mark the mount
// namespace as in-use, otherwise ostree will attempt to remount /sysroot
// read-write on write operations and fail. We also avoid taking the write
// lock (which would write a lockfile to the read-only filesystem).
let sysroot = if is_ro {
ostree_ext::sysroot::SysrootLock::from_assumed_locked(&sysroot)
} else {
sysroot.set_mount_namespace_in_use();
ostree_ext::sysroot::SysrootLock::new_from_sysroot(&sysroot).await?
};
sysroot.load(gio::Cancellable::NONE)?;

let storage = Storage {
physical_root,
physical_root_path: Utf8PathBuf::from("/sysroot"),
is_ro,
run,
boot_dir: None,
esp: None,
Expand Down Expand Up @@ -432,6 +478,10 @@ pub(crate) struct Storage {
/// This is `/sysroot` on a running system, or the target mount point during install.
pub physical_root_path: Utf8PathBuf,

/// True if the physical root (`/sysroot`) is on a physically read-only
/// block device (e.g. a live ISO) and cannot be made writable.
pub(crate) is_ro: bool,

/// The 'boot' directory, useful and `Some` only for composefs systems
/// For grub booted systems, this points to `/sysroot/boot`
/// For systemd booted systems, this points to the ESP
Expand Down Expand Up @@ -498,6 +548,7 @@ impl Storage {
Ok(Self {
physical_root,
physical_root_path,
is_ro: false,
run,
boot_dir: None,
esp: None,
Expand All @@ -507,6 +558,16 @@ impl Storage {
})
}

/// Ensure the storage is writable, erroring with a clear message if the
/// underlying `/sysroot` is on a read-only block device (e.g. a live ISO).
pub(crate) fn require_writable(&self) -> Result<()> {
anyhow::ensure!(
!self.is_ro,
"Cannot perform this operation: /sysroot is on a read-only block device (e.g. a live ISO)"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nit: This one just feels like the LLM taking things too literally

Suggested change
"Cannot perform this operation: /sysroot is on a read-only block device (e.g. a live ISO)"
"Cannot perform this operation: /sysroot is on a read-only block device"

);
Ok(())
}

/// Returns `boot_dir` if it exists
pub(crate) fn require_boot_dir(&self) -> Result<&Dir> {
self.boot_dir
Expand Down
6 changes: 6 additions & 0 deletions docs/src/host-v1.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"$ref": "#/$defs/HostStatus",
"default": {
"booted": null,
"readOnly": false,
"rollback": null,
"rollbackQueued": false,
"staged": null,
Expand Down Expand Up @@ -323,6 +324,11 @@
"$ref": "#/$defs/BootEntry"
}
},
"readOnly": {
"description": "Set to true if the physical root (`/sysroot`) is on a read-only medium\n(e.g. a live ISO) and so cannot be mutated; commands that would change\nthe system (upgrade, switch, etc.) are not available.",
"type": "boolean",
"default": false
},
"rollback": {
"description": "The previously booted image",
"anyOf": [
Expand Down
Loading