Skip to content

Commit dd6e419

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 <[email protected]>
1 parent 71dc8e5 commit dd6e419

File tree

4 files changed

+170
-1
lines changed

4 files changed

+170
-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: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
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 mkdir -p /tmp/bootc-test-policy && \\
41+
cd /tmp/bootc-test-policy && \\
42+
echo 'module bootc_test_policy 1.0;' > bootc_test_policy.te && \\
43+
echo 'require {' >> bootc_test_policy.te && \\
44+
echo ' type unconfined_t;' >> bootc_test_policy.te && \\
45+
echo ' class file { read write };' >> bootc_test_policy.te && \\
46+
echo '}' >> bootc_test_policy.te && \\
47+
echo 'type bootc_test_t;' >> bootc_test_policy.te && \\
48+
checkmodule -M -m -o bootc_test_policy.mod bootc_test_policy.te && \\
49+
semodule_package -o bootc_test_policy.pp -m bootc_test_policy.mod && \\
50+
semodule -i bootc_test_policy.pp && \\
51+
# Ensure the policy module is in /usr/etc/selinux for the deployment
52+
mkdir -p /usr/etc/selinux/targeted/modules/active/modules && \\
53+
if [ -d /etc/selinux/targeted/modules/active/modules ]; then \\
54+
cp -a /etc/selinux/targeted/modules/active/modules/* /usr/etc/selinux/targeted/modules/active/modules/ 2>/dev/null || true; \\
55+
fi && \\
56+
rm -rf /tmp/bootc-test-policy
57+
" | save Dockerfile
58+
59+
# Build the derived image
60+
podman build --quiet -t localhost/bootc-derived-policy .
61+
62+
# Verify soft reboot preparation hasn't happened yet
63+
assert (not ("/run/nextroot" | path exists))
64+
65+
# Try to soft reboot - this should fail because policies differ
66+
bootc switch --soft-reboot=auto --transport containers-storage localhost/bootc-derived-policy
67+
let st = bootc status --json | from json
68+
69+
# Verify staged deployment exists
70+
assert ($st.status.staged != null) "Expected staged deployment to exist"
71+
72+
# The staged deployment should NOT be soft-reboot capable because policies differ
73+
assert (not $st.status.staged.softRebootCapable) "Expected soft reboot to be blocked due to SELinux policy difference, but softRebootCapable is true"
74+
75+
# Verify soft reboot preparation didn't happen
76+
assert (not ("/run/nextroot" | path exists)) "Soft reboot should not be prepared when policies differ"
77+
78+
# Reset and do a full reboot instead
79+
ostree admin prepare-soft-reboot --reset
80+
tmt-reboot
81+
}
82+
83+
# The second boot; verify we're in the derived image
84+
def second_boot [] {
85+
tap begin "Verify deployment with different SELinux policy"
86+
87+
# Verify we're in the new deployment
88+
let st = bootc status --json | from json
89+
let booted = $st.status.booted.image
90+
assert ($booted.image.image | str contains "bootc-derived-policy") $"Expected booted image to contain 'bootc-derived-policy', got: ($booted.image.image)"
91+
92+
tap ok
93+
}
94+
95+
def main [] {
96+
# See https://tmt.readthedocs.io/en/stable/stories/features.html#reboot-during-test
97+
match $env.TMT_REBOOT_COUNT? {
98+
null | "0" => initial_build,
99+
"1" => second_boot,
100+
$o => { error make { msg: $"Invalid TMT_REBOOT_COUNT ($o)" } },
101+
}
102+
}
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)