diff --git a/crates/lib/src/status.rs b/crates/lib/src/status.rs index e8b4192a2..983b4b83f 100644 --- a/crates/lib/src/status.rs +++ b/crates/lib/src/status.rs @@ -93,7 +93,56 @@ 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, +) -> bool { + // Only check if SELinux is enabled + let Ok(selinux_enabled) = crate::lsm::selinux_enabled() else { + return true; // If we can't determine, assume compatible + }; + if !selinux_enabled { + return true; + } + + let Ok(booted_fd) = crate::utils::deployment_fd(sysroot, booted_deployment) else { + return false; // Can't check, be conservative + }; + let Ok(booted_policy) = crate::lsm::new_sepolicy_at(&booted_fd) else { + return false; // Can't check, be conservative + }; + let Ok(target_fd) = crate::utils::deployment_fd(sysroot, target_deployment) else { + return false; // Can't check, be conservative + }; + let Ok(target_policy) = crate::lsm::new_sepolicy_at(&target_fd) else { + return false; // Can't check, be conservative + }; + + match (booted_policy, target_policy) { + (None, None) => true, // Both absent, compatible + (Some(_), None) | (None, Some(_)) => { + // Incompatible: one has policy, other doesn't + false + } + (Some(booted), Some(target)) => { + // Both have policies, checksums must match + // SAFETY: new_sepolicy_at filters out policies without checksums + let Some(booted_csum) = booted.csum() else { + return false; // Can't compare, be conservative + }; + let Some(target_csum) = target.csum() else { + return false; // Can't compare, be conservative + }; + 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 +162,19 @@ 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() { + if !check_selinux_policy_compatible(sysroot, &booted_deployment, deployment) { + 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 fe74cc737..5b685afcd 100644 --- a/tmt/plans/integration.fmf +++ b/tmt/plans/integration.fmf @@ -112,3 +112,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..147513df8 --- /dev/null +++ b/tmt/tests/booted/test-soft-reboot-selinux-policy.nu @@ -0,0 +1,115 @@ +# 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 injects a local SELinux policy module + # This modifies the policy in a way that changes the policy checksum + # Following Colin's suggestion: inject a local selinux policy module + "FROM localhost/bootc +# Inject a local SELinux policy change by modifying file_contexts +# This will change the policy checksum between deployments +RUN mkdir -p /opt/bootc-test-selinux-policy && \ + echo '/opt/bootc-test-selinux-policy /opt/bootc-test-selinux-policy' >> /etc/selinux/targeted/contexts/files/file_contexts.subs_dist || true +" | save Dockerfile + + # Build the derived image + podman build -t localhost/bootc-derived-policy . + + # 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 + + # 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" + + print "Soft reboot correctly blocked when SELinux policies differ" + + # Reset and do a full reboot instead + ostree admin prepare-soft-reboot --reset + tmt-reboot +} + +# The second boot; verify we're in the derived image +def second_boot [] { + tap begin "Verify deployment and test soft reboot with same policy" + + # Verify we're in the new deployment + let st = bootc status --json | from json + assert ($st.status.booted.image.name | str contains "bootc-derived-policy") + + # Now create another derived image with the SAME policy (no changes) + let td = mktemp -d + cd $td + + bootc image copy-to-storage + + # Create a derived container that doesn't change the policy + "FROM localhost/bootc-derived-policy +RUN echo 'same policy test' > /usr/share/testfile-same-policy.txt +" | save Dockerfile + + podman build -t localhost/bootc-same-policy . + + # Try to soft reboot - this should succeed because policies match + bootc switch --soft-reboot=auto --transport containers-storage localhost/bootc-same-policy + let st = bootc status --json | from json + + # The staged deployment SHOULD be soft-reboot capable because policies match + assert $st.status.staged.softRebootCapable "Expected soft reboot to be allowed when SELinux policies match" + + print "Soft reboot correctly allowed when SELinux policies match" + + # See ../bug-soft-reboot.md - TMT cannot handle systemd soft-reboots + ostree admin prepare-soft-reboot --reset + tmt-reboot +} + +# The third boot; verify we're in the same-policy deployment +def third_boot [] { + tap begin "Verify same-policy deployment" + + assert ("/usr/share/testfile-same-policy.txt" | path exists) + + let st = bootc status --json | from json + assert ($st.status.booted.image.name | str contains "bootc-same-policy") + + print "Successfully verified soft reboot with SELinux policy checks" + + 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, + "2" => third_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