Skip to content
Open
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
63 changes: 62 additions & 1 deletion crates/lib/src/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,56 @@ 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,
) -> 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;
Expand All @@ -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
Expand Down
11 changes: 11 additions & 0 deletions tmt/plans/integration.fmf
Original file line number Diff line number Diff line change
Expand Up @@ -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)
115 changes: 115 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,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)" } },
}
}

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
Loading