Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 54 additions & 1 deletion crates/lib/src/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down
11 changes: 11 additions & 0 deletions tmt/plans/integration.fmf
Original file line number Diff line number Diff line change
Expand Up @@ -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)
104 changes: 104 additions & 0 deletions tmt/tests/booted/test-soft-reboot-selinux-policy.nu
Original file line number Diff line number Diff line change
@@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say "Ask for a soft reboot, but we won't get one 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)" } },
}
}
3 changes: 3 additions & 0 deletions tmt/tests/test-29-soft-reboot-selinux-policy.fmf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
summary: Test soft reboot with SELinux policy changes
test: nu booted/test-soft-reboot-selinux-policy.nu
duration: 30m