bootc_lib/bootc_composefs/
selinux.rs1use anyhow::{Context, Result};
2use bootc_initramfs_setup::mount_composefs_image;
3use bootc_mount::tempmount::TempMount;
4use cap_std_ext::cap_std::{ambient_authority, fs::Dir};
5use cap_std_ext::dirext::CapStdExtDirExt;
6use fn_error_context::context;
7
8use crate::bootc_composefs::status::ComposefsCmdline;
9use crate::lsm::selinux_enabled;
10use crate::store::Storage;
11
12const SELINUX_CONFIG_PATH: &str = "etc/selinux/config";
13const SELINUX_TYPE: &str = "SELINUXTYPE=";
14const POLICY_FILE_PREFIX: &str = "policy.";
15
16fn find_latest_policy_file(policy_dir: &Dir) -> Result<String> {
18 let mut highest_policy_version = -1;
19 let mut latest_policy_name = None;
20
21 for entry in policy_dir
22 .entries_utf8()
23 .context("Getting policy dir entries")?
24 {
25 let entry = entry?;
26
27 if !entry.file_type()?.is_file() {
28 continue;
30 }
31
32 let filename = entry.file_name()?;
33
34 match filename.strip_prefix(POLICY_FILE_PREFIX) {
35 Some(version) => {
36 let v_int = version
37 .parse::<i32>()
38 .with_context(|| anyhow::anyhow!("Parsing {version} as int"))?;
39
40 if v_int < highest_policy_version {
41 continue;
42 }
43
44 highest_policy_version = v_int;
45 latest_policy_name = Some(filename.to_string());
46 }
47
48 None => continue,
49 };
50 }
51
52 latest_policy_name.ok_or_else(|| anyhow::anyhow!("Failed to get latest SELinux policy"))
53}
54
55fn compute_policy_file_hash(deployment_root: &Dir, full_path: &str) -> Result<String> {
57 let mut file = deployment_root
58 .open(full_path)
59 .context("Opening policy file")?;
60 let mut hasher = openssl::hash::Hasher::new(openssl::hash::MessageDigest::sha256())?;
61 std::io::copy(&mut file, &mut hasher)?;
62
63 let hash = hex::encode(hasher.finish().context("Computing hash")?);
64 Ok(hash)
65}
66
67#[context("Getting SELinux policy for deployment {depl_id}")]
68fn get_selinux_policy_for_deployment(
69 storage: &Storage,
70 booted_cmdline: &ComposefsCmdline,
71 depl_id: &str,
72) -> Result<Option<String>> {
73 let sysroot_fd = storage.physical_root.reopen_as_ownedfd()?;
74
75 let (deployment_root, _mount_guard) = if *booted_cmdline.digest == *depl_id {
77 (Dir::open_ambient_dir("/", ambient_authority())?, None)
78 } else {
79 let composefs_fd =
80 mount_composefs_image(&sysroot_fd, depl_id, booted_cmdline.allow_missing_fsverity)?;
81 let erofs_tmp_mnt = TempMount::mount_fd(&composefs_fd)?;
82
83 (erofs_tmp_mnt.fd.try_clone()?, Some(erofs_tmp_mnt))
84 };
85
86 if !deployment_root.exists(SELINUX_CONFIG_PATH) {
87 return Ok(None);
88 }
89
90 let selinux_config = deployment_root
91 .read_to_string(SELINUX_CONFIG_PATH)
92 .context("Reading selinux config")?;
93
94 let type_ = selinux_config
95 .lines()
96 .find(|l| l.starts_with(SELINUX_TYPE))
97 .ok_or_else(|| anyhow::anyhow!("Falied to find SELINUXTYPE"))?
98 .split("=")
99 .nth(1)
100 .ok_or_else(|| anyhow::anyhow!("Failed to parse SELINUXTYPE"))?
101 .trim();
102
103 let policy_dir_path = format!("etc/selinux/{type_}/policy");
104
105 let policy_dir = deployment_root
106 .open_dir(&policy_dir_path)
107 .context("Opening selinux policy dir")?;
108
109 let policy_name = find_latest_policy_file(&policy_dir)?;
110
111 let full_path = format!("{policy_dir_path}/{policy_name}");
112
113 let hash = compute_policy_file_hash(&deployment_root, &full_path)?;
114
115 Ok(Some(hash))
116}
117
118#[context("Checking SELinux policy compatibility")]
119pub(crate) fn are_selinux_policies_compatible(
120 storage: &Storage,
121 booted_cmdline: &ComposefsCmdline,
122 depl_id: &str,
123) -> Result<bool> {
124 if !selinux_enabled()? {
125 return Ok(true);
126 }
127
128 let booted_policy_hash =
129 get_selinux_policy_for_deployment(storage, booted_cmdline, &booted_cmdline.digest)?;
130
131 let depl_policy_hash = get_selinux_policy_for_deployment(storage, booted_cmdline, depl_id)?;
132
133 let sl_policy_match = match (booted_policy_hash, depl_policy_hash) {
134 (Some(booted_csum), Some(target_csum)) => booted_csum == target_csum,
136 (Some(_), None) | (None, Some(_)) => false,
138 (None, None) => true,
140 };
141
142 if !sl_policy_match {
143 tracing::debug!("Soft rebooting not allowed due to differing SELinux policies");
144 }
145
146 Ok(sl_policy_match)
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use cap_std_ext::cap_std::ambient_authority;
153 use cap_std_ext::dirext::CapStdExtDirExt;
154
155 #[test]
156 fn test_find_latest_policy_file() -> Result<()> {
157 let tempdir = cap_std_ext::cap_tempfile::tempdir(ambient_authority())?;
158
159 tempdir.atomic_write("policy.30", "policy content 30")?;
161 tempdir.atomic_write("policy.31", "policy content 31")?;
162 tempdir.atomic_write("policy.29", "policy content 29")?;
163 tempdir.atomic_write("not_policy.32", "not a policy file")?;
164 tempdir.atomic_write("other_policy.txt", "invalid policy file")?;
165
166 let result = find_latest_policy_file(&tempdir)?;
167 assert_eq!(result, "policy.31");
168
169 Ok(())
170 }
171
172 #[test]
173 fn test_find_latest_policy_file_with_single_file() -> Result<()> {
174 let tempdir = cap_std_ext::cap_tempfile::tempdir(ambient_authority())?;
175
176 tempdir.atomic_write("policy.25", "single policy file")?;
177
178 let result = find_latest_policy_file(&tempdir)?;
179 assert_eq!(result, "policy.25");
180
181 Ok(())
182 }
183
184 #[test]
185 fn test_find_latest_policy_file_no_policy_files() {
186 let tempdir = cap_std_ext::cap_tempfile::tempdir(ambient_authority()).unwrap();
187
188 tempdir
189 .atomic_write("not_policy.txt", "not a policy file")
190 .unwrap();
191 tempdir.atomic_write("other.txt", "invalid format").unwrap();
192
193 let result = find_latest_policy_file(&tempdir);
194 assert!(result.is_err());
195 assert!(
196 result
197 .unwrap_err()
198 .to_string()
199 .contains("Failed to get latest SELinux policy")
200 );
201 }
202
203 #[test]
204 fn test_find_latest_policy_file_invalid_version() {
205 let tempdir = cap_std_ext::cap_tempfile::tempdir(ambient_authority()).unwrap();
206
207 tempdir
208 .atomic_write("policy.abc", "invalid version")
209 .unwrap();
210
211 let result = find_latest_policy_file(&tempdir);
212 assert!(result.is_err());
213 assert!(
214 result
215 .unwrap_err()
216 .to_string()
217 .contains("Parsing abc as int")
218 );
219 }
220
221 #[test]
222 fn test_find_latest_policy_file_negative_version() -> Result<()> {
223 let tempdir = cap_std_ext::cap_tempfile::tempdir(ambient_authority())?;
224
225 tempdir.atomic_write("policy.5", "positive version")?;
226 tempdir.atomic_write("policy.-1", "negative version")?;
227
228 let result = find_latest_policy_file(&tempdir)?;
229 assert_eq!(result, "policy.5");
230
231 Ok(())
232 }
233
234 #[test]
235 fn test_find_latest_policy_file_skips_directories() -> Result<()> {
236 let tempdir = cap_std_ext::cap_tempfile::tempdir(ambient_authority())?;
237
238 tempdir.create_dir("policy.99")?; tempdir.atomic_write("policy.5", "actual policy file")?;
240
241 let result = find_latest_policy_file(&tempdir)?;
242 assert_eq!(result, "policy.5");
243
244 Ok(())
245 }
246
247 #[test]
248 fn test_compute_policy_file_hash() -> Result<()> {
249 let tempdir = cap_std_ext::cap_tempfile::tempdir(ambient_authority())?;
250
251 let test_content = "test policy content for hashing";
252 tempdir.atomic_write("test_policy.30", test_content)?;
253
254 let hash = compute_policy_file_hash(&tempdir, "test_policy.30")?;
255
256 assert_eq!(hash.len(), 64);
258 assert!(hash.chars().all(|c| c.is_ascii_hexdigit()));
259
260 let hash2 = compute_policy_file_hash(&tempdir, "test_policy.30")?;
262 assert_eq!(hash, hash2);
263
264 Ok(())
265 }
266
267 #[test]
268 fn test_compute_policy_file_hash_different_content() -> Result<()> {
269 let tempdir = cap_std_ext::cap_tempfile::tempdir(ambient_authority())?;
270
271 tempdir.atomic_write("policy1.30", "content 1")?;
272 tempdir.atomic_write("policy2.30", "content 2")?;
273
274 let hash1 = compute_policy_file_hash(&tempdir, "policy1.30")?;
275 let hash2 = compute_policy_file_hash(&tempdir, "policy2.30")?;
276
277 assert_ne!(hash1, hash2);
278
279 Ok(())
280 }
281
282 #[test]
283 fn test_compute_policy_file_hash_nonexistent_file() {
284 let tempdir = cap_std_ext::cap_tempfile::tempdir(ambient_authority()).unwrap();
285
286 let result = compute_policy_file_hash(&tempdir, "nonexistent.30");
287 assert!(result.is_err());
288 assert!(
289 result
290 .unwrap_err()
291 .to_string()
292 .contains("Opening policy file")
293 );
294 }
295
296 #[test]
297 fn test_compute_policy_file_hash_empty_file() -> Result<()> {
298 let tempdir = cap_std_ext::cap_tempfile::tempdir(ambient_authority())?;
299
300 tempdir.atomic_write("empty_policy.30", "")?;
301
302 let hash = compute_policy_file_hash(&tempdir, "empty_policy.30")?;
303
304 assert_eq!(hash.len(), 64);
306 assert!(hash.chars().all(|c| c.is_ascii_hexdigit()));
307
308 assert_eq!(
310 hash,
311 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
312 );
313
314 Ok(())
315 }
316}