From 006d882eaa1bbb86215c039045bfb1648ecacb84 Mon Sep 17 00:00:00 2001 From: gursewak1997 Date: Fri, 14 Nov 2025 10:44:38 -0800 Subject: [PATCH] Soft reboot: Detect SELinux policy deltas Add check to prevent soft reboot when SELinux policies differ between booted and target deployments, since policy is not reloaded across soft reboots. Assisted-by: Cursor (Auto) Signed-off-by: gursewak1997 --- crates/lib/src/status.rs | 55 ++++++++++++- tmt/plans/integration.fmf | 11 +++ .../booted/test-soft-reboot-selinux-policy.nu | 82 +++++++++++++++++++ .../test-29-soft-reboot-selinux-policy.fmf | 3 + 4 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 tmt/tests/booted/test-soft-reboot-selinux-policy.nu create mode 100644 tmt/tests/test-29-soft-reboot-selinux-policy.fmf 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 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..b3b092d45 --- /dev/null +++ b/tmt/tests/booted/test-soft-reboot-selinux-policy.nu @@ -0,0 +1,82 @@ +# 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 + # We use semanage fcontext to add a file context, which will actually + # change the compiled policy checksum. This requires policycoreutils-python-utils. + "FROM localhost/bootc +# Install policycoreutils-python-utils if semanage is not available +RUN if ! command -v semanage >/dev/null 2>&1; then dnf install -y policycoreutils-python-utils; fi +# Inject a local SELinux policy change using semanage +# This will change the policy checksum between deployments +RUN mkdir -p /opt/bootc-test-selinux-policy && \ + semanage fcontext -a -t usr_t \"/opt/bootc-test-selinux-policy(/.*)?\" +" | 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 with different SELinux 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") + + print "Successfully verified soft reboot is blocked when SELinux policies differ" + + 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