diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a87d3ea25..6797e5004 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -162,6 +162,7 @@ jobs: # No fedora-44 due to https://bugzilla.redhat.com/show_bug.cgi?id=2429501 test_os: [fedora-43, centos-9, centos-10] variant: [ostree, composefs-sealeduki-sdboot, composefs-sdboot, composefs-grub] + filesystem: ["ext4", "xfs"] exclude: # centos-9 UKI is experimental/broken (https://github.com/bootc-dev/bootc/issues/1812) - test_os: centos-9 @@ -172,6 +173,10 @@ jobs: variant: composefs-sdboot - test_os: centos-9 variant: composefs-grub + # We only test filesystems for composefs to test if composefs backend will work on fs + # without fsverity + - variant: ostree + filesystem: ext4 runs-on: ubuntu-24.04 @@ -190,6 +195,7 @@ jobs: echo "BOOTC_base=${BASE}" >> $GITHUB_ENV echo "RUST_BACKTRACE=full" >> $GITHUB_ENV echo "RUST_LOG=trace" >> $GITHUB_ENV + echo "BOOTC_filesystem=${{ matrix.filesystem }}" >> $GITHUB_ENV case "${{ matrix.variant }}" in composefs-grub) @@ -213,8 +219,6 @@ jobs: ;; esac - - if [ "${{ matrix.variant }}" = "composefs-sealeduki-sdboot" ]; then BUILDROOTBASE=$(just pullspec-for-os buildroot-base ${{ matrix.test_os }}) echo "BOOTC_buildroot_base=${BUILDROOTBASE}" >> $GITHUB_ENV @@ -244,7 +248,7 @@ jobs: - name: Run TMT integration tests run: | if [[ "${{ matrix.variant }}" = composefs* ]]; then - just "test-${{ matrix.variant }}" + just "test-${{ matrix.variant }}" "${{ matrix.filesystem }}" else just test-tmt integration fi @@ -255,7 +259,7 @@ jobs: if: always() uses: actions/upload-artifact@v6 with: - name: tmt-log-PR-${{ github.event.number }}-${{ matrix.test_os }}-${{ matrix.variant }}-${{ env.ARCH }} + name: tmt-log-PR-${{ github.event.number }}-${{ matrix.test_os }}-${{ matrix.variant }}-${{ matrix.filesystem }}-${{ env.ARCH }} path: /var/tmp/tmt # Test bootc install on Fedora CoreOS (separate job to avoid disk space issues diff --git a/Dockerfile b/Dockerfile index 5422d8e06..019822206 100644 --- a/Dockerfile +++ b/Dockerfile @@ -176,6 +176,7 @@ RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp # We need our newly-built bootc for the compute-composefs-digest command FROM tools as sealed-uki ARG variant +ARG filesystem # Install our bootc package (only needed for the compute-composefs-digest command) RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \ --mount=type=bind,from=packages,src=/,target=/run/packages \ @@ -186,8 +187,15 @@ RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp --mount=type=bind,from=packaging,src=/,target=/run/packaging \ --mount=type=bind,from=base-penultimate,src=/,target=/run/target < Result { /// Prepares a floating mount for composefs and returns the fd /// /// # Arguments -/// * sysroot - fd for /sysroot -/// * name - Name of the EROFS image to be mounted -/// * insecure - Whether fsverity is optional or not +/// * sysroot - fd for /sysroot +/// * name - Name of the EROFS image to be mounted +/// * allow_missing_fsverity - Whether to allow mount without fsverity support #[context("Mounting composefs image")] -pub fn mount_composefs_image(sysroot: &OwnedFd, name: &str, insecure: bool) -> Result { +pub fn mount_composefs_image( + sysroot: &OwnedFd, + name: &str, + allow_missing_fsverity: bool, +) -> Result { let mut repo = Repository::::open_path(sysroot, "composefs")?; - repo.set_insecure(insecure); + repo.set_insecure(allow_missing_fsverity); let rootfs = repo .mount(name) .context("Failed to mount composefs image")?; diff --git a/crates/lib/src/bootc_composefs/boot.rs b/crates/lib/src/bootc_composefs/boot.rs index c4787a917..72c0c77d7 100644 --- a/crates/lib/src/bootc_composefs/boot.rs +++ b/crates/lib/src/bootc_composefs/boot.rs @@ -94,7 +94,6 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use crate::parsers::bls_config::{BLSConfig, BLSConfigType}; -use crate::parsers::grub_menuconfig::MenuEntry; use crate::task::Task; use crate::{ bootc_composefs::repo::get_imgref, @@ -119,6 +118,7 @@ use crate::{ }, spec::{Bootloader, Host}, }; +use crate::{parsers::grub_menuconfig::MenuEntry, store::BootedComposefs}; use crate::install::{RootSetup, State}; @@ -155,7 +155,14 @@ pub(crate) enum BootSetupType<'a> { ), ), /// For `bootc upgrade` - Upgrade((&'a Storage, &'a ComposefsFilesystem, &'a Host)), + Upgrade( + ( + &'a Storage, + &'a BootedComposefs, + &'a ComposefsFilesystem, + &'a Host, + ), + ), } #[derive( @@ -512,7 +519,7 @@ pub(crate) fn setup_composefs_bls_boot( cmdline_options.extend(&root_setup.kargs); - let composefs_cmdline = if state.composefs_options.insecure { + let composefs_cmdline = if state.composefs_options.allow_missing_verity { format!("{COMPOSEFS_CMDLINE}=?{id_hex}") } else { format!("{COMPOSEFS_CMDLINE}={id_hex}") @@ -532,7 +539,7 @@ pub(crate) fn setup_composefs_bls_boot( ) } - BootSetupType::Upgrade((storage, fs, host)) => { + BootSetupType::Upgrade((storage, booted_cfs, fs, host)) => { let sysroot_parent = get_sysroot_parent_dev(&storage.physical_root)?; let bootloader = host.require_composefs_booted()?.bootloader.clone(); @@ -551,7 +558,12 @@ pub(crate) fn setup_composefs_bls_boot( }; // Copy all cmdline args, replacing only `composefs=` - let param = format!("{COMPOSEFS_CMDLINE}={id_hex}"); + let param = if booted_cfs.cmdline.allow_missing_fsverity { + format!("{COMPOSEFS_CMDLINE}=?{id_hex}") + } else { + format!("{COMPOSEFS_CMDLINE}={id_hex}") + }; + let param = Parameter::parse(¶m).context("Failed to create 'composefs=' parameter")?; cmdline.add_or_modify(¶m); @@ -797,7 +809,7 @@ fn write_pe_to_esp( file_path: &Utf8Path, pe_type: PEType, uki_id: &Sha512HashValue, - is_insecure_from_opts: bool, + missing_fsverity_allowed: bool, mounted_efi: impl AsRef, bootloader: &Bootloader, ) -> Result> { @@ -810,17 +822,19 @@ fn write_pe_to_esp( if matches!(pe_type, PEType::Uki) { let cmdline = uki::get_cmdline(&efi_bin).context("Getting UKI cmdline")?; - let (composefs_cmdline, insecure) = + let (composefs_cmdline, missing_verity_allowed_cmdline) = get_cmdline_composefs::(cmdline).context("Parsing composefs=")?; // If the UKI cmdline does not match what the user has passed as cmdline option // NOTE: This will only be checked for new installs and now upgrades/switches - match is_insecure_from_opts { - true if !insecure => { - tracing::warn!("--insecure passed as option but UKI cmdline does not support it"); + match missing_fsverity_allowed { + true if !missing_verity_allowed_cmdline => { + tracing::warn!( + "--allow-missing-fsverity passed as option but UKI cmdline does not support it" + ); } - false if insecure => { + false if missing_verity_allowed_cmdline => { tracing::warn!("UKI cmdline has composefs set as insecure"); } @@ -1065,7 +1079,8 @@ pub(crate) fn setup_composefs_uki_boot( id: &Sha512HashValue, entries: Vec>, ) -> Result { - let (root_path, esp_device, bootloader, is_insecure_from_opts, uki_addons) = match setup_type { + let (root_path, esp_device, bootloader, missing_fsverity_allowed, uki_addons) = match setup_type + { BootSetupType::Setup((root_setup, state, postfetch, ..)) => { state.require_no_kargs_for_uki()?; @@ -1075,12 +1090,12 @@ pub(crate) fn setup_composefs_uki_boot( root_setup.physical_root_path.clone(), esp_part.node.clone(), postfetch.detected_bootloader.clone(), - state.composefs_options.insecure, + state.composefs_options.allow_missing_verity, state.composefs_options.uki_addon.as_ref(), ) } - BootSetupType::Upgrade((storage, _, host)) => { + BootSetupType::Upgrade((storage, booted_cfs, _, host)) => { let sysroot = Utf8PathBuf::from("/sysroot"); // Still needed for root_path let sysroot_parent = get_sysroot_parent_dev(&storage.physical_root)?; let bootloader = host.require_composefs_booted()?.bootloader.clone(); @@ -1089,7 +1104,7 @@ pub(crate) fn setup_composefs_uki_boot( sysroot, get_esp_partition(&sysroot_parent)?.0, bootloader, - false, + booted_cfs.cmdline.allow_missing_fsverity, None, ) } @@ -1140,7 +1155,7 @@ pub(crate) fn setup_composefs_uki_boot( utf8_file_path, entry.pe_type, &id, - is_insecure_from_opts, + missing_fsverity_allowed, esp_mount.dir.path(), &bootloader, )?; @@ -1219,8 +1234,11 @@ pub(crate) async fn setup_composefs_boot( root_setup: &RootSetup, state: &State, image_id: &str, + allow_missing_fsverity: bool, ) -> Result<()> { - let repo = open_composefs_repo(&root_setup.physical_root)?; + let mut repo = open_composefs_repo(&root_setup.physical_root)?; + repo.set_insecure(allow_missing_fsverity); + let mut fs = create_composefs_filesystem(&repo, image_id, None)?; let entries = fs.transform_for_boot(&repo)?; let id = fs.commit_image(&repo, None)?; @@ -1291,6 +1309,7 @@ pub(crate) async fn setup_composefs_boot( &state.source.imageref.name, )) .await?, + allow_missing_fsverity, ) .await?; diff --git a/crates/lib/src/bootc_composefs/finalize.rs b/crates/lib/src/bootc_composefs/finalize.rs index 0f8ffab08..af32b6123 100644 --- a/crates/lib/src/bootc_composefs/finalize.rs +++ b/crates/lib/src/bootc_composefs/finalize.rs @@ -24,7 +24,11 @@ pub(crate) async fn get_etc_diff(storage: &Storage, booted_cfs: &BootedComposefs // Mount the booted EROFS image to get pristine etc let sysroot_fd = storage.physical_root.reopen_as_ownedfd()?; - let composefs_fd = mount_composefs_image(&sysroot_fd, &booted_composefs.verity, false)?; + let composefs_fd = mount_composefs_image( + &sysroot_fd, + &booted_composefs.verity, + booted_cfs.cmdline.allow_missing_fsverity, + )?; let erofs_tmp_mnt = TempMount::mount_fd(&composefs_fd)?; @@ -68,7 +72,11 @@ pub(crate) async fn composefs_backend_finalize( // Mount the booted EROFS image to get pristine etc let sysroot_fd = storage.physical_root.reopen_as_ownedfd()?; - let composefs_fd = mount_composefs_image(&sysroot_fd, &booted_composefs.verity, false)?; + let composefs_fd = mount_composefs_image( + &sysroot_fd, + &booted_composefs.verity, + booted_cfs.cmdline.allow_missing_fsverity, + )?; let erofs_tmp_mnt = TempMount::mount_fd(&composefs_fd)?; diff --git a/crates/lib/src/bootc_composefs/repo.rs b/crates/lib/src/bootc_composefs/repo.rs index 0c497fcdc..7e5318380 100644 --- a/crates/lib/src/bootc_composefs/repo.rs +++ b/crates/lib/src/bootc_composefs/repo.rs @@ -23,6 +23,7 @@ pub(crate) fn open_composefs_repo(rootfs_dir: &Dir) -> Result Result<(String, impl FsVerityHashValue)> { let rootfs_dir = &root_setup.physical_root; @@ -30,7 +31,8 @@ pub(crate) async fn initialize_composefs_repository( .create_dir_all("composefs") .context("Creating dir composefs")?; - let repo = open_composefs_repo(rootfs_dir)?; + let mut repo = open_composefs_repo(rootfs_dir)?; + repo.set_insecure(allow_missing_fsverity); let OstreeExtImgRef { name: image_name, @@ -73,6 +75,7 @@ pub(crate) fn get_imgref(transport: &str, image: &str) -> String { pub(crate) async fn pull_composefs_repo( transport: &String, image: &String, + allow_missing_fsverity: bool, ) -> Result<( crate::store::ComposefsRepository, Vec>, @@ -81,7 +84,8 @@ pub(crate) async fn pull_composefs_repo( )> { let rootfs_dir = Dir::open_ambient_dir("/sysroot", ambient_authority())?; - let repo = open_composefs_repo(&rootfs_dir).context("Opening composefs repo")?; + let mut repo = open_composefs_repo(&rootfs_dir).context("Opening composefs repo")?; + repo.set_insecure(allow_missing_fsverity); let final_imgref = get_imgref(transport, image); @@ -93,7 +97,9 @@ pub(crate) async fn pull_composefs_repo( tracing::info!("ID: {id}, Verity: {}", verity.to_hex()); - let repo = open_composefs_repo(&rootfs_dir)?; + let mut repo = open_composefs_repo(&rootfs_dir)?; + repo.set_insecure(allow_missing_fsverity); + let mut fs: crate::store::ComposefsFilesystem = create_composefs_filesystem(&repo, &id, None) .context("Failed to create composefs filesystem")?; diff --git a/crates/lib/src/bootc_composefs/rollback.rs b/crates/lib/src/bootc_composefs/rollback.rs index fd30e99a2..56e6de3d2 100644 --- a/crates/lib/src/bootc_composefs/rollback.rs +++ b/crates/lib/src/bootc_composefs/rollback.rs @@ -114,36 +114,34 @@ fn rollback_grub_uki_entries(boot_dir: &Dir) -> Result<()> { /// - Grub Type1 boot entries /// - Systemd Typ1 boot entries /// - Systemd UKI (Type2) boot entries [since we use BLS entries for systemd boot] +/// +/// Cases +/// 1. We're actually booted into the deployment that has it's sort_key as 0 +/// a. Just swap the primary and secondary bootloader entries +/// b. If they're already swapped (rollback was queued), re-swap them (unqueue rollback) +/// +/// 2. We're booted into the depl with sort_key 1 (choose the rollback deployment on boot screen) +/// a. Here we assume that rollback is queued as there's no way to differentiate between this +/// case and Case 1-b. This is what ostree does as well #[context("Rolling back {bootloader} entries")] fn rollback_composefs_entries(boot_dir: &Dir, bootloader: Bootloader) -> Result<()> { - use crate::bootc_composefs::state::get_booted_bls; - // Get all boot entries sorted in descending order by sort-key let mut all_configs = get_sorted_type1_boot_entries(&boot_dir, false)?; // TODO(Johan-Liebert): Currently assuming there are only two deployments assert!(all_configs.len() == 2); - // Identify which entry is the currently booted one - let booted_bls = get_booted_bls(&boot_dir)?; - let booted_verity = booted_bls.get_verity()?; - // For rollback: previous gets primary sort-key, booted gets secondary sort-key // Use "bootc" as default os_id for rollback scenarios // TODO: Extract actual os_id from deployment let os_id = "bootc"; - for cfg in &mut all_configs { - let cfg_verity = cfg.get_verity()?; - - if cfg_verity == booted_verity { - // This is the currently booted deployment - it should become secondary - cfg.sort_key = Some(secondary_sort_key(os_id)); - } else { - // This is the previous deployment - it should become primary (rollback target) - cfg.sort_key = Some(primary_sort_key(os_id)); - } - } + // This is the currently booted deployment - it should become secondary + // OR if rollback was queued, it would become primary + all_configs[0].sort_key = Some(primary_sort_key(os_id)); + // This is the previous deployment - it should become primary (rollback target) + // OR if rollback was queued, it would become secondary + all_configs[1].sort_key = Some(secondary_sort_key(os_id)); // Write these boot_dir @@ -156,9 +154,8 @@ fn rollback_composefs_entries(boot_dir: &Dir, bootloader: Bootloader) -> Result< // Write the BLS configs in there for cfg in all_configs { - let cfg_verity = cfg.get_verity()?; // After rollback: previous deployment becomes primary, booted becomes secondary - let priority = if cfg_verity == booted_verity { + let priority = if cfg.sort_key == Some(secondary_sort_key(os_id)) { FILENAME_PRIORITY_SECONDARY } else { FILENAME_PRIORITY_PRIMARY diff --git a/crates/lib/src/bootc_composefs/selinux.rs b/crates/lib/src/bootc_composefs/selinux.rs index 700275264..733f0897a 100644 --- a/crates/lib/src/bootc_composefs/selinux.rs +++ b/crates/lib/src/bootc_composefs/selinux.rs @@ -76,7 +76,8 @@ fn get_selinux_policy_for_deployment( let (deployment_root, _mount_guard) = if *booted_cmdline.digest == *depl_id { (Dir::open_ambient_dir("/", ambient_authority())?, None) } else { - let composefs_fd = mount_composefs_image(&sysroot_fd, depl_id, false)?; + let composefs_fd = + mount_composefs_image(&sysroot_fd, depl_id, booted_cmdline.allow_missing_fsverity)?; let erofs_tmp_mnt = TempMount::mount_fd(&composefs_fd)?; (erofs_tmp_mnt.fd.try_clone()?, Some(erofs_tmp_mnt)) diff --git a/crates/lib/src/bootc_composefs/soft_reboot.rs b/crates/lib/src/bootc_composefs/soft_reboot.rs index 0a71d15b9..b9e8a6e53 100644 --- a/crates/lib/src/bootc_composefs/soft_reboot.rs +++ b/crates/lib/src/bootc_composefs/soft_reboot.rs @@ -108,7 +108,11 @@ pub(crate) async fn prepare_soft_reboot_composefs( create_dir_all(NEXTROOT).context("Creating nextroot")?; - let cmdline = Cmdline::from(format!("{COMPOSEFS_CMDLINE}={deployment_id}")); + let cmdline = if booted_cfs.cmdline.allow_missing_fsverity { + Cmdline::from(format!("{COMPOSEFS_CMDLINE}=?{deployment_id}")) + } else { + Cmdline::from(format!("{COMPOSEFS_CMDLINE}={deployment_id}")) + }; let args = bootc_initramfs_setup::Args { cmd: vec![], diff --git a/crates/lib/src/bootc_composefs/state.rs b/crates/lib/src/bootc_composefs/state.rs index 517281be0..b4350c7f0 100644 --- a/crates/lib/src/bootc_composefs/state.rs +++ b/crates/lib/src/bootc_composefs/state.rs @@ -87,6 +87,7 @@ pub(crate) fn initialize_state( erofs_id: &String, state_path: &Utf8PathBuf, initialize_var: bool, + allow_missing_fsverity: bool, ) -> Result<()> { let sysroot_fd = open( sysroot_path.as_std_path(), @@ -95,7 +96,11 @@ pub(crate) fn initialize_state( ) .context("Opening sysroot")?; - let composefs_fd = bootc_initramfs_setup::mount_composefs_image(&sysroot_fd, &erofs_id, false)?; + let composefs_fd = bootc_initramfs_setup::mount_composefs_image( + &sysroot_fd, + &erofs_id, + allow_missing_fsverity, + )?; let tempdir = TempMount::mount_fd(composefs_fd)?; @@ -234,6 +239,7 @@ pub(crate) async fn write_composefs_state( boot_type: BootType, boot_digest: String, container_details: &ImgConfigManifest, + allow_missing_fsverity: bool, ) -> Result<()> { let state_path = root_path .join(STATE_DIR_RELATIVE) @@ -256,6 +262,7 @@ pub(crate) async fn write_composefs_state( &deployment_id.to_hex(), &state_path, staged.is_none(), + allow_missing_fsverity, )?; let ImageReference { diff --git a/crates/lib/src/bootc_composefs/status.rs b/crates/lib/src/bootc_composefs/status.rs index 14fb0bf7b..282845a4d 100644 --- a/crates/lib/src/bootc_composefs/status.rs +++ b/crates/lib/src/bootc_composefs/status.rs @@ -55,8 +55,7 @@ pub(crate) struct ImgConfigManifest { /// A parsed composefs command line #[derive(Clone)] pub(crate) struct ComposefsCmdline { - #[allow(dead_code)] - pub insecure: bool, + pub allow_missing_fsverity: bool, pub digest: Box, } @@ -69,12 +68,12 @@ struct DeploymentBootInfo<'a> { impl ComposefsCmdline { pub(crate) fn new(s: &str) -> Self { - let (insecure, digest_str) = s + let (allow_missing_fsverity, digest_str) = s .strip_prefix('?') .map(|v| (true, v)) .unwrap_or_else(|| (false, s)); ComposefsCmdline { - insecure, + allow_missing_fsverity, digest: digest_str.into(), } } @@ -82,8 +81,12 @@ impl ComposefsCmdline { impl std::fmt::Display for ComposefsCmdline { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let insecure = if self.insecure { "?" } else { "" }; - write!(f, "{}={}{}", COMPOSEFS_CMDLINE, insecure, self.digest) + let allow_missing_fsverity = if self.allow_missing_fsverity { "?" } else { "" }; + write!( + f, + "{}={}{}", + COMPOSEFS_CMDLINE, allow_missing_fsverity, self.digest + ) } } @@ -807,10 +810,10 @@ mod tests { fn test_composefs_parsing() { const DIGEST: &str = "8b7df143d91c716ecfa5fc1730022f6b421b05cedee8fd52b1fc65a96030ad52"; let v = ComposefsCmdline::new(DIGEST); - assert!(!v.insecure); + assert!(!v.allow_missing_fsverity); assert_eq!(v.digest.as_ref(), DIGEST); let v = ComposefsCmdline::new(&format!("?{}", DIGEST)); - assert!(v.insecure); + assert!(v.allow_missing_fsverity); assert_eq!(v.digest.as_ref(), DIGEST); } diff --git a/crates/lib/src/bootc_composefs/update.rs b/crates/lib/src/bootc_composefs/update.rs index 978a45360..079fad520 100644 --- a/crates/lib/src/bootc_composefs/update.rs +++ b/crates/lib/src/bootc_composefs/update.rs @@ -249,7 +249,12 @@ pub(crate) async fn do_upgrade( ) -> Result<()> { start_finalize_stated_svc()?; - let (repo, entries, id, fs) = pull_composefs_repo(&imgref.transport, &imgref.image).await?; + let (repo, entries, id, fs) = pull_composefs_repo( + &imgref.transport, + &imgref.image, + booted_cfs.cmdline.allow_missing_fsverity, + ) + .await?; let Some(entry) = entries.iter().next() else { anyhow::bail!("No boot entries!"); @@ -265,7 +270,7 @@ pub(crate) async fn do_upgrade( let boot_digest = match boot_type { BootType::Bls => setup_composefs_bls_boot( - BootSetupType::Upgrade((storage, &fs, &host)), + BootSetupType::Upgrade((storage, booted_cfs, &fs, &host)), repo, &id, entry, @@ -273,7 +278,7 @@ pub(crate) async fn do_upgrade( )?, BootType::Uki => setup_composefs_uki_boot( - BootSetupType::Upgrade((storage, &fs, &host)), + BootSetupType::Upgrade((storage, booted_cfs, &fs, &host)), repo, &id, entries, @@ -291,6 +296,7 @@ pub(crate) async fn do_upgrade( boot_type, boot_digest, img_manifest_config, + booted_cfs.cmdline.allow_missing_fsverity, ) .await?; diff --git a/crates/lib/src/cli.rs b/crates/lib/src/cli.rs index 5ff4233b6..d6b00e125 100644 --- a/crates/lib/src/cli.rs +++ b/crates/lib/src/cli.rs @@ -411,6 +411,10 @@ pub(crate) enum ContainerOpts { #[clap(long = "karg", hide = true)] kargs: Vec, + /// Make fs-verity validation optional in case the filesystem doesn't support it + #[clap(long)] + allow_missing_verity: bool, + /// Additional arguments to pass to ukify (after `--`). #[clap(last = true)] args: Vec, @@ -1624,8 +1628,9 @@ async fn run_from_opt(opt: Opt) -> Result<()> { ContainerOpts::Ukify { rootfs, kargs, + allow_missing_verity, args, - } => crate::ukify::build_ukify(&rootfs, &kargs, &args), + } => crate::ukify::build_ukify(&rootfs, &kargs, &args, allow_missing_verity), }, Opt::Completion { shell } => { use clap_complete::aot::generate; diff --git a/crates/lib/src/install.rs b/crates/lib/src/install.rs index a056d03b7..d0c0ab201 100644 --- a/crates/lib/src/install.rs +++ b/crates/lib/src/install.rs @@ -385,7 +385,7 @@ pub(crate) struct InstallComposefsOpts { /// Make fs-verity validation optional in case the filesystem doesn't support it #[clap(long, default_value_t, requires = "composefs_backend")] #[serde(default)] - pub(crate) insecure: bool, + pub(crate) allow_missing_verity: bool, /// The bootloader to use. #[clap(long, requires = "composefs_backend")] @@ -1887,10 +1887,21 @@ async fn install_to_filesystem_impl( if state.composefs_options.composefs_backend { // Load a fd for the mounted target physical root - let (id, verity) = initialize_composefs_repository(state, rootfs).await?; + let (id, verity) = initialize_composefs_repository( + state, + rootfs, + state.composefs_options.allow_missing_verity, + ) + .await?; tracing::info!("id: {id}, verity: {}", verity.to_hex()); - setup_composefs_boot(rootfs, state, &id).await?; + setup_composefs_boot( + rootfs, + state, + &id, + state.composefs_options.allow_missing_verity, + ) + .await?; } else { ostree_install(state, rootfs, cleanup).await?; } diff --git a/crates/lib/src/parsers/bls_config.rs b/crates/lib/src/parsers/bls_config.rs index 66716b88e..a783053bc 100644 --- a/crates/lib/src/parsers/bls_config.rs +++ b/crates/lib/src/parsers/bls_config.rs @@ -13,6 +13,7 @@ use std::collections::HashMap; use std::fmt::Display; use uapi_version::Version; +use crate::bootc_composefs::status::ComposefsCmdline; use crate::composefs_consts::COMPOSEFS_CMDLINE; #[derive(Debug, PartialEq, Eq, Default)] @@ -189,15 +190,16 @@ impl BLSConfig { let kv = cmdline .find(COMPOSEFS_CMDLINE) - .ok_or(anyhow::anyhow!("No composefs= param"))?; + .ok_or_else(|| anyhow::anyhow!("No composefs= param"))?; let value = kv .value() - .ok_or(anyhow::anyhow!("Empty composefs= param"))?; + .ok_or_else(|| anyhow::anyhow!("Empty composefs= param"))?; - let value = value.to_owned(); + let cfs_cmdline = ComposefsCmdline::new(value); - Ok(value) + // TODO(Johan-Liebert1): We lose the info here that this is insecure + Ok(cfs_cmdline.digest.to_string().clone()) } BLSConfigType::Unknown => anyhow::bail!("Unknown config type"), diff --git a/crates/lib/src/store/mod.rs b/crates/lib/src/store/mod.rs index 260fe96b5..b5d2470d1 100644 --- a/crates/lib/src/store/mod.rs +++ b/crates/lib/src/store/mod.rs @@ -167,7 +167,7 @@ impl BootedStorage { Environment::ComposefsBooted(cmdline) => { let (physical_root, run) = get_physical_root_and_run()?; let mut composefs = ComposefsRepository::open_path(&physical_root, COMPOSEFS)?; - if cmdline.insecure { + if cmdline.allow_missing_fsverity { composefs.set_insecure(true); } let composefs = Arc::new(composefs); diff --git a/crates/lib/src/ukify.rs b/crates/lib/src/ukify.rs index 05e5d86ed..7419de2fb 100644 --- a/crates/lib/src/ukify.rs +++ b/crates/lib/src/ukify.rs @@ -30,6 +30,7 @@ pub(crate) fn build_ukify( rootfs: &Utf8Path, extra_kargs: &[String], args: &[OsString], + allow_missing_fsverity: bool, ) -> Result<()> { // Warn if --karg is used (temporary workaround) if !extra_kargs.is_empty() { @@ -83,7 +84,11 @@ pub(crate) fn build_ukify( let mut cmdline = crate::bootc_kargs::get_kargs_in_root(&root, std::env::consts::ARCH)?; // Add the composefs digest - let composefs_param = format!("{COMPOSEFS_CMDLINE}={composefs_digest}"); + let composefs_param = if allow_missing_fsverity { + format!("{COMPOSEFS_CMDLINE}=?{composefs_digest}") + } else { + format!("{COMPOSEFS_CMDLINE}={composefs_digest}") + }; cmdline.extend(&Cmdline::from(composefs_param)); // Add any extra kargs provided via --karg @@ -129,7 +134,7 @@ mod tests { let tempdir = tempfile::tempdir().unwrap(); let path = Utf8Path::from_path(tempdir.path()).unwrap(); - let result = build_ukify(path, &[], &[]); + let result = build_ukify(path, &[], &[], false); assert!(result.is_err()); let err = format!("{:#}", result.unwrap_err()); assert!( @@ -147,7 +152,7 @@ mod tests { fs::create_dir_all(tempdir.path().join("boot/EFI/Linux")).unwrap(); fs::write(tempdir.path().join("boot/EFI/Linux/test.efi"), b"fake uki").unwrap(); - let result = build_ukify(path, &[], &[]); + let result = build_ukify(path, &[], &[], false); assert!(result.is_err()); let err = format!("{:#}", result.unwrap_err()); assert!( diff --git a/crates/xtask/src/tmt.rs b/crates/xtask/src/tmt.rs index e244229c1..4f7e0b9da 100644 --- a/crates/xtask/src/tmt.rs +++ b/crates/xtask/src/tmt.rs @@ -485,10 +485,15 @@ pub(crate) fn run_tmt(sh: &Shell, args: &RunTmtArgs) -> Result<()> { } if args.composefs_backend { - // TODO(Johan-Liebert1): Filesystem should be a parameter and we should test - // insecure with xfs - opts.push("--filesystem=ext4".into()); + let filesystem = args.filesystem.as_deref().unwrap_or("ext4"); + opts.push(format!("--filesystem={}", filesystem)); opts.push("--composefs-backend".into()); + + if filesystem == "xfs" { + // As xfs doesn't support fsverity + opts.push("--allow-missing-verity".into()); + } + opts.extend(COMPOSEFS_KERNEL_ARGS.map(|x| x.into())); } diff --git a/crates/xtask/src/xtask.rs b/crates/xtask/src/xtask.rs index cb6afe29f..056bd780e 100644 --- a/crates/xtask/src/xtask.rs +++ b/crates/xtask/src/xtask.rs @@ -127,6 +127,9 @@ pub(crate) struct RunTmtArgs { #[arg(long, requires = "composefs_backend")] pub(crate) bootloader: Option, + + #[arg(long, requires = "composefs_backend")] + pub(crate) filesystem: Option, } /// Arguments for tmt-provision command diff --git a/tmt/plans/integration.fmf b/tmt/plans/integration.fmf index 1c8af4881..3aa0f9907 100644 --- a/tmt/plans/integration.fmf +++ b/tmt/plans/integration.fmf @@ -181,4 +181,11 @@ execute: how: fmf test: - /tmt/tests/tests/test-34-user-agent + +/plan-36-rollback: + summary: Test bootc rollback functionality through image switch and rollback cycle + discover: + how: fmf + test: + - /tmt/tests/tests/test-36-rollback # END GENERATED PLANS diff --git a/tmt/tests/booted/test-rollback.nu b/tmt/tests/booted/test-rollback.nu new file mode 100644 index 000000000..0f2e2ee89 --- /dev/null +++ b/tmt/tests/booted/test-rollback.nu @@ -0,0 +1,117 @@ +# number: 36 +# tmt: +# summary: Test bootc rollback functionality +# duration: 30m +# +# This test verifies bootc rollback functionality: +# 1. Captures the initial deployment state +# 2. Switches to a different image +# 3. Verifies the switch was successful +# 4. Performs bootc rollback +# 5. Reboots and verifies we're back to the original deployment + +use std assert +use tap.nu +use bootc_testlib.nu + +bootc status +journalctl --list-boots + +let st = bootc status --json | from json +let booted = $st.status.booted.image + +def imgsrc [] { + $env.BOOTC_upgrade_image? | default "localhost/bootc-derived-local" +} + +# Run on the first boot - capture initial state and switch to new image +def initial_switch [] { + tap begin "bootc rollback test" + + print "=== Initial boot - capturing state and switching image ===" + + # Store initial deployment information for later verification + let initial_st = bootc status --json | from json + let initial_image = $initial_st.status.booted.image + + $initial_image | to json | save /var/bootc-initial-state.json + + let imgsrc = imgsrc + + if ($imgsrc | str ends-with "-local") { + bootc image copy-to-storage + + print "Building derived container" + "FROM localhost/bootc +RUN echo 'This is the rollback target image' > /usr/share/bootc-rollback-marker +" | save Dockerfile + + podman build -t $imgsrc . + print $"Built derived image: ($imgsrc)" + } + + print $"Switching to ($imgsrc)" + bootc switch --transport containers-storage $imgsrc + + print "Switch completed, rebooting to new image..." + tmt-reboot +} + +# Check that we successfully switched to the new image and then rollback +def second_boot_rollback [] { + print "=== Second boot - verifying switch and performing rollback ===" + + # Verify we're running the new image + assert equal $booted.image.image $"(imgsrc)" + print "Successfully switched to new image" + + assert ("/usr/share/bootc-rollback-marker" | path exists) + print "New image artifacts verified" + + print "Performing bootc rollback..." + bootc rollback + + print "Rollback initiated, rebooting to previous deployment..." + tmt-reboot +} + +def back_to_first_depl [boot_count] { + print $"=== ($boot_count) boot - verifying rollback success ===" + + # Load the original state we saved and verify we're back to the original image + let original_state = cat /var/bootc-initial-state.json | from json + + assert equal $booted.image $original_state.image + print $"Successfully rolled back to original image: ($booted.image.image)" + + if ("/usr/share/bootc-rollback-marker" | path exists) { + error make { msg: "Rollback target marker still present - rollback may have failed" } + } +} + +# Verify that rollback was successful and we're back to original deployment +def third_boot_verify [] { + back_to_first_depl Third + + # Finally test a double rollback, to make sure the rollback state is queued then unqueued + bootc rollback + bootc rollback + + tmt-reboot +} + +def fourth_boot_verify [] { + back_to_first_depl Fourth + tap ok +} + +def main [] { + # See https://tmt.readthedocs.io/en/stable/stories/features.html#reboot-during-test + match $env.TMT_REBOOT_COUNT? { + null | "0" => initial_switch, + "1" => second_boot_rollback, + "2" => third_boot_verify, + "3" => fourth_boot_verify, + $o => { error make { msg: $"Invalid TMT_REBOOT_COUNT ($o)" } }, + } +} diff --git a/tmt/tests/tests.fmf b/tmt/tests/tests.fmf index 4d808880e..a1f5980e2 100644 --- a/tmt/tests/tests.fmf +++ b/tmt/tests/tests.fmf @@ -101,3 +101,8 @@ summary: Verify bootc sends correct User-Agent header to registries duration: 10m test: python3 booted/test-user-agent.py + +/test-36-rollback: + summary: Test bootc rollback functionality through image switch and rollback cycle + duration: 30m + test: nu booted/test-rollback.nu