Skip to content

Commit dcbe65e

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 dcbe65e

File tree

4 files changed

+191
-1
lines changed

4 files changed

+191
-1
lines changed

crates/lib/src/status.rs

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,56 @@ 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+
) -> bool {
103+
// Only check if SELinux is enabled
104+
let Ok(selinux_enabled) = crate::lsm::selinux_enabled() else {
105+
return true; // If we can't determine, assume compatible
106+
};
107+
if !selinux_enabled {
108+
return true;
109+
}
110+
111+
let Ok(booted_fd) = crate::utils::deployment_fd(sysroot, booted_deployment) else {
112+
return false; // Can't check, be conservative
113+
};
114+
let Ok(booted_policy) = crate::lsm::new_sepolicy_at(&booted_fd) else {
115+
return false; // Can't check, be conservative
116+
};
117+
let Ok(target_fd) = crate::utils::deployment_fd(sysroot, target_deployment) else {
118+
return false; // Can't check, be conservative
119+
};
120+
let Ok(target_policy) = crate::lsm::new_sepolicy_at(&target_fd) else {
121+
return false; // Can't check, be conservative
122+
};
123+
124+
match (booted_policy, target_policy) {
125+
(None, None) => true, // Both absent, compatible
126+
(Some(_), None) | (None, Some(_)) => {
127+
// Incompatible: one has policy, other doesn't
128+
false
129+
}
130+
(Some(booted), Some(target)) => {
131+
// Both have policies, checksums must match
132+
// SAFETY: new_sepolicy_at filters out policies without checksums
133+
let Some(booted_csum) = booted.csum() else {
134+
return false; // Can't compare, be conservative
135+
};
136+
let Some(target_csum) = target.csum() else {
137+
return false; // Can't compare, be conservative
138+
};
139+
booted_csum == target_csum
140+
}
141+
}
142+
}
143+
96144
/// Check if a deployment has soft reboot capability
145+
// TODO: Lower SELinux policy check into ostree's deployment_can_soft_reboot API
97146
fn has_soft_reboot_capability(sysroot: &SysrootLock, deployment: &ostree::Deployment) -> bool {
98147
if !ostree_ext::systemd_has_soft_reboot() {
99148
return false;
@@ -113,7 +162,19 @@ fn has_soft_reboot_capability(sysroot: &SysrootLock, deployment: &ostree::Deploy
113162
return false;
114163
}
115164

116-
sysroot.deployment_can_soft_reboot(deployment)
165+
if !sysroot.deployment_can_soft_reboot(deployment) {
166+
return false;
167+
}
168+
169+
// Check SELinux policy compatibility with booted deployment
170+
// Block soft reboot if SELinux policies differ, as policy is not reloaded across soft reboots
171+
if let Some(booted_deployment) = sysroot.booted_deployment() {
172+
if !check_selinux_policy_compatible(sysroot, &booted_deployment, deployment) {
173+
return false;
174+
}
175+
}
176+
177+
true
117178
}
118179

119180
/// 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: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
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+
"FROM localhost/bootc
34+
# Inject a local SELinux policy change by modifying file_contexts
35+
# This will change the policy checksum between deployments
36+
RUN mkdir -p /opt/bootc-test-selinux-policy && \
37+
echo '/opt/bootc-test-selinux-policy /opt/bootc-test-selinux-policy' >> /etc/selinux/targeted/contexts/files/file_contexts.subs_dist || true
38+
" | save Dockerfile
39+
40+
# Build the derived image
41+
podman build -t localhost/bootc-derived-policy .
42+
43+
# Try to soft reboot - this should fail because policies differ
44+
bootc switch --soft-reboot=auto --transport containers-storage localhost/bootc-derived-policy
45+
let st = bootc status --json | from json
46+
47+
# The staged deployment should NOT be soft-reboot capable because policies differ
48+
assert (not $st.status.staged.softRebootCapable) "Expected soft reboot to be blocked due to SELinux policy difference"
49+
50+
print "Soft reboot correctly blocked when SELinux policies differ"
51+
52+
# Reset and do a full reboot instead
53+
ostree admin prepare-soft-reboot --reset
54+
tmt-reboot
55+
}
56+
57+
# The second boot; verify we're in the derived image
58+
def second_boot [] {
59+
tap begin "Verify deployment and test soft reboot with same policy"
60+
61+
# Verify we're in the new deployment
62+
let st = bootc status --json | from json
63+
assert ($st.status.booted.image.name | str contains "bootc-derived-policy")
64+
65+
# Now create another derived image with the SAME policy (no changes)
66+
let td = mktemp -d
67+
cd $td
68+
69+
bootc image copy-to-storage
70+
71+
# Create a derived container that doesn't change the policy
72+
"FROM localhost/bootc-derived-policy
73+
RUN echo 'same policy test' > /usr/share/testfile-same-policy.txt
74+
" | save Dockerfile
75+
76+
podman build -t localhost/bootc-same-policy .
77+
78+
# Try to soft reboot - this should succeed because policies match
79+
bootc switch --soft-reboot=auto --transport containers-storage localhost/bootc-same-policy
80+
let st = bootc status --json | from json
81+
82+
# The staged deployment SHOULD be soft-reboot capable because policies match
83+
assert $st.status.staged.softRebootCapable "Expected soft reboot to be allowed when SELinux policies match"
84+
85+
print "Soft reboot correctly allowed when SELinux policies match"
86+
87+
# See ../bug-soft-reboot.md - TMT cannot handle systemd soft-reboots
88+
ostree admin prepare-soft-reboot --reset
89+
tmt-reboot
90+
}
91+
92+
# The third boot; verify we're in the same-policy deployment
93+
def third_boot [] {
94+
tap begin "Verify same-policy deployment"
95+
96+
assert ("/usr/share/testfile-same-policy.txt" | path exists)
97+
98+
let st = bootc status --json | from json
99+
assert ($st.status.booted.image.name | str contains "bootc-same-policy")
100+
101+
print "Successfully verified soft reboot with SELinux policy checks"
102+
103+
tap ok
104+
}
105+
106+
def main [] {
107+
# See https://tmt.readthedocs.io/en/stable/stories/features.html#reboot-during-test
108+
match $env.TMT_REBOOT_COUNT? {
109+
null | "0" => initial_build,
110+
"1" => second_boot,
111+
"2" => third_boot,
112+
$o => { error make { msg: $"Invalid TMT_REBOOT_COUNT ($o)" } },
113+
}
114+
}
115+
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)