From 9cf1afad274fce5e6b8763129992f6f4d3fb4c6d Mon Sep 17 00:00:00 2001 From: Andrew Dunn Date: Sat, 10 Oct 2026 06:58:27 -0400 Subject: [PATCH] install: Reject a config stateroot on composefs-only images The composefs backend only uses the `default` stateroot. #2544 made `--stateroot` fail with it and left a stateroot from the install configuration unchecked, since an image may set one for its ostree installs. An image that ships a UKI, or selects composefs itself with no ostree prepare-root.conf, can't be installed with ostree, so its configured stateroot was always ignored and the install still landed in `default`. Check the configured stateroot for those images too, unless `--stateroot` is passed. With `--composefs-backend` on an image that can also be installed with ostree, it's still ignored. Assisted-by: AI Signed-off-by: Andrew Dunn --- crates/lib/src/install.rs | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/crates/lib/src/install.rs b/crates/lib/src/install.rs index 03eeb22172..2a8605fc6e 100644 --- a/crates/lib/src/install.rs +++ b/crates/lib/src/install.rs @@ -420,10 +420,13 @@ pub(crate) struct InstallComposefsOpts { impl InstallComposefsOpts { /// Check that the options fit together, once `composefs_backend` says /// whether the composefs backend is used (passed, or selected by the image). + /// `config_stateroot` comes from the install configuration and only counts + /// when `--stateroot` isn't passed. pub(crate) fn validate( &self, bootloader: Option<&Bootloader>, stateroot: Option<&str>, + config_stateroot: Option<&str>, ) -> Result<()> { if self.composefs_backend { anyhow::ensure!( @@ -436,6 +439,12 @@ impl InstallComposefsOpts { "--stateroot {stateroot} is not supported with the composefs backend, which only uses {default}" ); } + if let (None, Some(stateroot)) = (stateroot, config_stateroot.filter(|&s| s != default)) + { + anyhow::bail!( + "stateroot {stateroot} in the install configuration is not supported with the composefs backend, which only uses {default}" + ); + } } else { anyhow::ensure!( !self.allow_missing_verity, @@ -1817,9 +1826,16 @@ async fn prepare_install( )?; tracing::debug!("Composefs default: {composefs_default}"); composefs_options.composefs_backend |= composefs_required || composefs_default; + // An image may set a stateroot in its install configuration for its ostree + // installs, so only check it when the image can't be installed with ostree. + let config_stateroot = install_config + .as_ref() + .and_then(|c| c.stateroot.as_deref()) + .filter(|_| composefs_required || composefs_default); composefs_options.validate( config_opts.bootloader.as_ref(), config_opts.stateroot.as_deref(), + config_stateroot, )?; composefs_options.validate_bound_images(config_opts.bound_images, &rootfs)?; @@ -3202,7 +3218,7 @@ mod tests { uki_addon, }; assert_eq!( - opts.validate(bootloader.as_ref(), stateroot).is_ok(), + opts.validate(bootloader.as_ref(), stateroot, None).is_ok(), valid, "{opts:?} {bootloader:?} {stateroot:?}" ); @@ -3213,9 +3229,25 @@ mod tests { ..Default::default() }; assert_eq!( - opts.validate(None, Some("myroot")).unwrap_err().to_string(), + opts.validate(None, Some("myroot"), None) + .unwrap_err() + .to_string(), "--stateroot myroot is not supported with the composefs backend, which only uses default" ); + + assert!(opts.validate(None, None, Some("default")).is_ok()); + assert!(opts.validate(None, Some("default"), Some("myroot")).is_ok()); + assert!( + InstallComposefsOpts::default() + .validate(None, None, Some("myroot")) + .is_ok() + ); + assert_eq!( + opts.validate(None, None, Some("myroot")) + .unwrap_err() + .to_string(), + "stateroot myroot in the install configuration is not supported with the composefs backend, which only uses default" + ); } #[test]