1
0
mirror of https://github.com/containers/bootc.git synced 2026-02-05 15:45:53 +01:00

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 <gursmangat@gmail.com>
This commit is contained in:
gursewak1997
2025-11-14 10:44:38 -08:00
committed by Colin Walters
parent f49a6bac30
commit 3ad82d0ee6
4 changed files with 172 additions and 1 deletions

View File

@@ -93,7 +93,45 @@ impl From<ImageReference> 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<bool> {
// 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

View File

@@ -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)

View File

@@ -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 <<EORUN
set -eux
mkdir -p /tmp/bootc-test-policy
cd /tmp/bootc-test-policy
echo 'module bootc_test_policy 1.0;' > 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)" } },
}
}

View File

@@ -0,0 +1,3 @@
summary: Test soft reboot with SELinux policy changes
test: nu booted/test-soft-reboot-selinux-policy.nu
duration: 30m