diff --git a/crates/lib/src/status.rs b/crates/lib/src/status.rs index e8b4192a2..f35330ef4 100644 --- a/crates/lib/src/status.rs +++ b/crates/lib/src/status.rs @@ -93,7 +93,45 @@ impl From for OstreeImageReference { } } +/// Check if SELinux policies are compatible between booted and target deployments. +/// Returns false if SELinux is enabled and the policies differ or have mismatched presence. +fn check_selinux_policy_compatible( + sysroot: &SysrootLock, + booted_deployment: &ostree::Deployment, + target_deployment: &ostree::Deployment, +) -> Result { + // Only check if SELinux is enabled + if !crate::lsm::selinux_enabled()? { + return Ok(true); + } + + let booted_fd = crate::utils::deployment_fd(sysroot, booted_deployment) + .context("Failed to get file descriptor for booted deployment")?; + let booted_policy = crate::lsm::new_sepolicy_at(&booted_fd) + .context("Failed to load SELinux policy from booted deployment")?; + let target_fd = crate::utils::deployment_fd(sysroot, target_deployment) + .context("Failed to get file descriptor for target deployment")?; + let target_policy = crate::lsm::new_sepolicy_at(&target_fd) + .context("Failed to load SELinux policy from target deployment")?; + + let booted_csum = booted_policy.and_then(|p| p.csum()); + let target_csum = target_policy.and_then(|p| p.csum()); + + match (booted_csum, target_csum) { + (None, None) => Ok(true), // Both absent, compatible + (Some(_), None) | (None, Some(_)) => { + // Incompatible: one has policy, other doesn't + Ok(false) + } + (Some(booted_csum), Some(target_csum)) => { + // Both have policies, checksums must match + Ok(booted_csum == target_csum) + } + } +} + /// Check if a deployment has soft reboot capability +// TODO: Lower SELinux policy check into ostree's deployment_can_soft_reboot API fn has_soft_reboot_capability(sysroot: &SysrootLock, deployment: &ostree::Deployment) -> bool { if !ostree_ext::systemd_has_soft_reboot() { return false; @@ -113,7 +151,22 @@ fn has_soft_reboot_capability(sysroot: &SysrootLock, deployment: &ostree::Deploy return false; } - sysroot.deployment_can_soft_reboot(deployment) + if !sysroot.deployment_can_soft_reboot(deployment) { + return false; + } + + // Check SELinux policy compatibility with booted deployment + // Block soft reboot if SELinux policies differ, as policy is not reloaded across soft reboots + if let Some(booted_deployment) = sysroot.booted_deployment() { + // deployment_fd should not fail for valid deployments + if !check_selinux_policy_compatible(sysroot, &booted_deployment, deployment) + .expect("deployment_fd should not fail for valid deployments") + { + return false; + } + } + + true } /// Parse an ostree origin file (a keyfile) and extract the targeted diff --git a/tmt/plans/integration.fmf b/tmt/plans/integration.fmf index 104f90feb..41b22deee 100644 --- a/tmt/plans/integration.fmf +++ b/tmt/plans/integration.fmf @@ -119,3 +119,14 @@ execute: how: fmf test: - /tmt/tests/test-28-factory-reset + +/test-29-soft-reboot-selinux-policy: + summary: Test soft reboot with SELinux policy changes + discover: + how: fmf + test: + - /tmt/tests/test-29-soft-reboot-selinux-policy + adjust: + - when: running_env != image_mode + enabled: false + because: tmt-reboot does not work with systemd reboot in testing farm environment (see bug-soft-reboot.md) diff --git a/tmt/tests/booted/test-soft-reboot-selinux-policy.nu b/tmt/tests/booted/test-soft-reboot-selinux-policy.nu new file mode 100644 index 000000000..477dbdbdb --- /dev/null +++ b/tmt/tests/booted/test-soft-reboot-selinux-policy.nu @@ -0,0 +1,104 @@ +# Verify that soft reboot is blocked when SELinux policies differ +use std assert +use tap.nu + +let soft_reboot_capable = "/usr/lib/systemd/system/soft-reboot.target" | path exists +if not $soft_reboot_capable { + echo "Skipping, system is not soft reboot capable" + return +} + +# Check if SELinux is enabled +let selinux_enabled = "/sys/fs/selinux/enforce" | path exists +if not $selinux_enabled { + echo "Skipping, SELinux is not enabled" + return +} + +# This code runs on *each* boot. +bootc status + +# Run on the first boot +def initial_build [] { + tap begin "Build base image and test soft reboot with SELinux policy change" + + let td = mktemp -d + cd $td + + bootc image copy-to-storage + + # Create a derived container that installs a custom SELinux policy module + # Installing a policy module will change the compiled policy checksum + # Following Colin's suggestion and the composefs-rs example + # We create a minimal policy module and install it + "FROM localhost/bootc +# Install tools needed to build and install SELinux policy modules +RUN dnf install -y selinux-policy-devel checkpolicy policycoreutils + +# Create a minimal SELinux policy module that will change the policy checksum +# We install it to ensure it's part of the deployment filesystem +RUN < bootc_test_policy.te + echo 'require {' >> bootc_test_policy.te + echo ' type unconfined_t;' >> bootc_test_policy.te + echo ' class file { read write };' >> bootc_test_policy.te + echo '}' >> bootc_test_policy.te + echo 'type bootc_test_t;' >> bootc_test_policy.te + checkmodule -M -m -o bootc_test_policy.mod bootc_test_policy.te + semodule_package -o bootc_test_policy.pp -m bootc_test_policy.mod + semodule -i bootc_test_policy.pp + rm -rf /tmp/bootc-test-policy + # Clean up dnf cache and logs, and SELinux policy generation artifacts to satisfy lint checks + dnf clean all + rm -rf /var/log/dnf* /var/log/hawkey.log /var/log/rhsm + rm -rf /var/cache/dnf /var/lib/dnf + rm -rf /var/lib/sepolgen /var/lib/rhsm /var/cache/ldconfig +EORUN +" | save Dockerfile + + # Build the derived image + podman build --quiet -t localhost/bootc-derived-policy . + + # Verify soft reboot preparation hasn't happened yet + assert (not ("/run/nextroot" | path exists)) + + # Try to soft reboot - this should fail because policies differ + bootc switch --soft-reboot=auto --transport containers-storage localhost/bootc-derived-policy + let st = bootc status --json | from json + + # Verify staged deployment exists + assert ($st.status.staged != null) "Expected staged deployment to exist" + + # The staged deployment should NOT be soft-reboot capable because policies differ + assert (not $st.status.staged.softRebootCapable) "Expected soft reboot to be blocked due to SELinux policy difference, but softRebootCapable is true" + + # Verify soft reboot preparation didn't happen + assert (not ("/run/nextroot" | path exists)) "Soft reboot should not be prepared when policies differ" + + # Do a full reboot + tmt-reboot +} + +# The second boot; verify we're in the derived image +def second_boot [] { + tap begin "Verify deployment with different SELinux policy" + + # Verify we're in the new deployment + let st = bootc status --json | from json + let booted = $st.status.booted.image + assert ($booted.image.image | str contains "bootc-derived-policy") $"Expected booted image to contain 'bootc-derived-policy', got: ($booted.image.image)" + + 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_build, + "1" => second_boot, + $o => { error make { msg: $"Invalid TMT_REBOOT_COUNT ($o)" } }, + } +} diff --git a/tmt/tests/test-29-soft-reboot-selinux-policy.fmf b/tmt/tests/test-29-soft-reboot-selinux-policy.fmf new file mode 100644 index 000000000..764e0602f --- /dev/null +++ b/tmt/tests/test-29-soft-reboot-selinux-policy.fmf @@ -0,0 +1,3 @@ +summary: Test soft reboot with SELinux policy changes +test: nu booted/test-soft-reboot-selinux-policy.nu +duration: 30m