Skip to content

Commit 4e4c5b9

Browse files
committed
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>
1 parent 71dc8e5 commit 4e4c5b9

File tree

4 files changed

+172
-1
lines changed

4 files changed

+172
-1
lines changed

crates/lib/src/status.rs

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,45 @@ impl From<ImageReference> for OstreeImageReference {
9393
}
9494
}
9595

96+
/// Check if SELinux policies are compatible between booted and target deployments.
97+
/// Returns false if SELinux is enabled and the policies differ or have mismatched presence.
98+
fn check_selinux_policy_compatible(
99+
sysroot: &SysrootLock,
100+
booted_deployment: &ostree::Deployment,
101+
target_deployment: &ostree::Deployment,
102+
) -> Result<bool> {
103+
// Only check if SELinux is enabled
104+
if !crate::lsm::selinux_enabled()? {
105+
return Ok(true);
106+
}
107+
108+
let booted_fd = crate::utils::deployment_fd(sysroot, booted_deployment)
109+
.context("Failed to get file descriptor for booted deployment")?;
110+
let booted_policy = crate::lsm::new_sepolicy_at(&booted_fd)
111+
.context("Failed to load SELinux policy from booted deployment")?;
112+
let target_fd = crate::utils::deployment_fd(sysroot, target_deployment)
113+
.context("Failed to get file descriptor for target deployment")?;
114+
let target_policy = crate::lsm::new_sepolicy_at(&target_fd)
115+
.context("Failed to load SELinux policy from target deployment")?;
116+
117+
let booted_csum = booted_policy.and_then(|p| p.csum());
118+
let target_csum = target_policy.and_then(|p| p.csum());
119+
120+
match (booted_csum, target_csum) {
121+
(None, None) => Ok(true), // Both absent, compatible
122+
(Some(_), None) | (None, Some(_)) => {
123+
// Incompatible: one has policy, other doesn't
124+
Ok(false)
125+
}
126+
(Some(booted_csum), Some(target_csum)) => {
127+
// Both have policies, checksums must match
128+
Ok(booted_csum == target_csum)
129+
}
130+
}
131+
}
132+
96133
/// Check if a deployment has soft reboot capability
134+
// TODO: Lower SELinux policy check into ostree's deployment_can_soft_reboot API
97135
fn has_soft_reboot_capability(sysroot: &SysrootLock, deployment: &ostree::Deployment) -> bool {
98136
if !ostree_ext::systemd_has_soft_reboot() {
99137
return false;
@@ -113,7 +151,22 @@ fn has_soft_reboot_capability(sysroot: &SysrootLock, deployment: &ostree::Deploy
113151
return false;
114152
}
115153

116-
sysroot.deployment_can_soft_reboot(deployment)
154+
if !sysroot.deployment_can_soft_reboot(deployment) {
155+
return false;
156+
}
157+
158+
// Check SELinux policy compatibility with booted deployment
159+
// Block soft reboot if SELinux policies differ, as policy is not reloaded across soft reboots
160+
if let Some(booted_deployment) = sysroot.booted_deployment() {
161+
// deployment_fd should not fail for valid deployments
162+
if !check_selinux_policy_compatible(sysroot, &booted_deployment, deployment)
163+
.expect("deployment_fd should not fail for valid deployments")
164+
{
165+
return false;
166+
}
167+
}
168+
169+
true
117170
}
118171

119172
/// Parse an ostree origin file (a keyfile) and extract the targeted

tmt/plans/integration.fmf

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,14 @@ execute:
112112
how: fmf
113113
test:
114114
- /tmt/tests/test-28-factory-reset
115+
116+
/test-29-soft-reboot-selinux-policy:
117+
summary: Test soft reboot with SELinux policy changes
118+
discover:
119+
how: fmf
120+
test:
121+
- /tmt/tests/test-29-soft-reboot-selinux-policy
122+
adjust:
123+
- when: running_env != image_mode
124+
enabled: false
125+
because: tmt-reboot does not work with systemd reboot in testing farm environment (see bug-soft-reboot.md)
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# Verify that soft reboot is blocked when SELinux policies differ
2+
use std assert
3+
use tap.nu
4+
5+
let soft_reboot_capable = "/usr/lib/systemd/system/soft-reboot.target" | path exists
6+
if not $soft_reboot_capable {
7+
echo "Skipping, system is not soft reboot capable"
8+
return
9+
}
10+
11+
# Check if SELinux is enabled
12+
let selinux_enabled = "/sys/fs/selinux/enforce" | path exists
13+
if not $selinux_enabled {
14+
echo "Skipping, SELinux is not enabled"
15+
return
16+
}
17+
18+
# This code runs on *each* boot.
19+
bootc status
20+
21+
# Run on the first boot
22+
def initial_build [] {
23+
tap begin "Build base image and test soft reboot with SELinux policy change"
24+
25+
let td = mktemp -d
26+
cd $td
27+
28+
bootc image copy-to-storage
29+
30+
# Create a derived container that installs a custom SELinux policy module
31+
# Installing a policy module will change the compiled policy checksum
32+
# Following Colin's suggestion and the composefs-rs example
33+
# We create a minimal policy module and install it
34+
"FROM localhost/bootc
35+
# Install tools needed to build and install SELinux policy modules
36+
RUN dnf install -y selinux-policy-devel checkpolicy policycoreutils
37+
38+
# Create a minimal SELinux policy module that will change the policy checksum
39+
# We install it to ensure it's part of the deployment filesystem
40+
RUN <<EORUN
41+
set -eux
42+
mkdir -p /tmp/bootc-test-policy
43+
cd /tmp/bootc-test-policy
44+
echo 'module bootc_test_policy 1.0;' > bootc_test_policy.te
45+
echo 'require {' >> bootc_test_policy.te
46+
echo ' type unconfined_t;' >> bootc_test_policy.te
47+
echo ' class file { read write };' >> bootc_test_policy.te
48+
echo '}' >> bootc_test_policy.te
49+
echo 'type bootc_test_t;' >> bootc_test_policy.te
50+
checkmodule -M -m -o bootc_test_policy.mod bootc_test_policy.te
51+
semodule_package -o bootc_test_policy.pp -m bootc_test_policy.mod
52+
semodule -i bootc_test_policy.pp
53+
rm -rf /tmp/bootc-test-policy
54+
# Clean up dnf cache and logs, and SELinux policy generation artifacts to satisfy lint checks
55+
dnf clean all
56+
rm -rf /var/log/dnf* /var/log/hawkey.log /var/log/rhsm
57+
rm -rf /var/cache/dnf /var/lib/dnf
58+
rm -rf /var/lib/sepolgen /var/lib/rhsm /var/cache/ldconfig
59+
EORUN
60+
" | save Dockerfile
61+
62+
# Build the derived image
63+
podman build --quiet -t localhost/bootc-derived-policy .
64+
65+
# Verify soft reboot preparation hasn't happened yet
66+
assert (not ("/run/nextroot" | path exists))
67+
68+
# Try to soft reboot - this should fail because policies differ
69+
bootc switch --soft-reboot=auto --transport containers-storage localhost/bootc-derived-policy
70+
let st = bootc status --json | from json
71+
72+
# Verify staged deployment exists
73+
assert ($st.status.staged != null) "Expected staged deployment to exist"
74+
75+
# The staged deployment should NOT be soft-reboot capable because policies differ
76+
assert (not $st.status.staged.softRebootCapable) "Expected soft reboot to be blocked due to SELinux policy difference, but softRebootCapable is true"
77+
78+
# Verify soft reboot preparation didn't happen
79+
assert (not ("/run/nextroot" | path exists)) "Soft reboot should not be prepared when policies differ"
80+
81+
# Do a full reboot
82+
tmt-reboot
83+
}
84+
85+
# The second boot; verify we're in the derived image
86+
def second_boot [] {
87+
tap begin "Verify deployment with different SELinux policy"
88+
89+
# Verify we're in the new deployment
90+
let st = bootc status --json | from json
91+
let booted = $st.status.booted.image
92+
assert ($booted.image.image | str contains "bootc-derived-policy") $"Expected booted image to contain 'bootc-derived-policy', got: ($booted.image.image)"
93+
94+
tap ok
95+
}
96+
97+
def main [] {
98+
# See https://tmt.readthedocs.io/en/stable/stories/features.html#reboot-during-test
99+
match $env.TMT_REBOOT_COUNT? {
100+
null | "0" => initial_build,
101+
"1" => second_boot,
102+
$o => { error make { msg: $"Invalid TMT_REBOOT_COUNT ($o)" } },
103+
}
104+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
summary: Test soft reboot with SELinux policy changes
2+
test: nu booted/test-soft-reboot-selinux-policy.nu
3+
duration: 30m

0 commit comments

Comments
 (0)