Skip to content

Commit 006d882

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 006d882

File tree

4 files changed

+150
-1
lines changed

4 files changed

+150
-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: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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 injects a local SELinux policy module
31+
# This modifies the policy in a way that changes the policy checksum
32+
# Following Colin's suggestion: inject a local selinux policy module
33+
# We use semanage fcontext to add a file context, which will actually
34+
# change the compiled policy checksum. This requires policycoreutils-python-utils.
35+
"FROM localhost/bootc
36+
# Install policycoreutils-python-utils if semanage is not available
37+
RUN if ! command -v semanage >/dev/null 2>&1; then dnf install -y policycoreutils-python-utils; fi
38+
# Inject a local SELinux policy change using semanage
39+
# This will change the policy checksum between deployments
40+
RUN mkdir -p /opt/bootc-test-selinux-policy && \
41+
semanage fcontext -a -t usr_t \"/opt/bootc-test-selinux-policy(/.*)?\"
42+
" | save Dockerfile
43+
44+
# Build the derived image
45+
podman build -t localhost/bootc-derived-policy .
46+
47+
# Try to soft reboot - this should fail because policies differ
48+
bootc switch --soft-reboot=auto --transport containers-storage localhost/bootc-derived-policy
49+
let st = bootc status --json | from json
50+
51+
# The staged deployment should NOT be soft-reboot capable because policies differ
52+
assert (not $st.status.staged.softRebootCapable) "Expected soft reboot to be blocked due to SELinux policy difference"
53+
54+
print "Soft reboot correctly blocked when SELinux policies differ"
55+
56+
# Reset and do a full reboot instead
57+
ostree admin prepare-soft-reboot --reset
58+
tmt-reboot
59+
}
60+
61+
# The second boot; verify we're in the derived image
62+
def second_boot [] {
63+
tap begin "Verify deployment with different SELinux policy"
64+
65+
# Verify we're in the new deployment
66+
let st = bootc status --json | from json
67+
assert ($st.status.booted.image.name | str contains "bootc-derived-policy")
68+
69+
print "Successfully verified soft reboot is blocked when SELinux policies differ"
70+
71+
tap ok
72+
}
73+
74+
def main [] {
75+
# See https://tmt.readthedocs.io/en/stable/stories/features.html#reboot-during-test
76+
match $env.TMT_REBOOT_COUNT? {
77+
null | "0" => initial_build,
78+
"1" => second_boot,
79+
$o => { error make { msg: $"Invalid TMT_REBOOT_COUNT ($o)" } },
80+
}
81+
}
82+
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)