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 = mount_composefs_image(&sysroot_fd, depl_id, false)?;
80 let erofs_tmp_mnt = TempMount::mount_fd(&composefs_fd)?;
81
82 (erofs_tmp_mnt.fd.try_clone()?, Some(erofs_tmp_mnt))
83 };
84
85 if !deployment_root.exists(SELINUX_CONFIG_PATH) {
86 return Ok(None);
87 }
88
89 let selinux_config = deployment_root
90 .read_to_string(SELINUX_CONFIG_PATH)
91 .context("Reading selinux config")?;
92
93 let type_ = selinux_config
94 .lines()
95 .find(|l| l.starts_with(SELINUX_TYPE))
96 .ok_or_else(|| anyhow::anyhow!("Falied to find SELINUXTYPE"))?
97 .split("=")
98 .nth(1)
99 .ok_or_else(|| anyhow::anyhow!("Failed to parse SELINUXTYPE"))?
100 .trim();
101
102 let policy_dir_path = format!("etc/selinux/{type_}/policy");
103
104 let policy_dir = deployment_root
105 .open_dir(&policy_dir_path)
106 .context("Opening selinux policy dir")?;
107
108 let policy_name = find_latest_policy_file(&policy_dir)?;
109
110 let full_path = format!("{policy_dir_path}/{policy_name}");
111
112 let hash = compute_policy_file_hash(&deployment_root, &full_path)?;
113
114 Ok(Some(hash))
115}
116
117#[context("Checking SELinux policy compatibility")]
118pub(crate) fn are_selinux_policies_compatible(
119 storage: &Storage,
120 booted_cmdline: &ComposefsCmdline,
121 depl_id: &str,
122) -> Result<bool> {
123 if !selinux_enabled()? {
124 return Ok(true);
125 }
126
127 let booted_policy_hash =
128 get_selinux_policy_for_deployment(storage, booted_cmdline, &booted_cmdline.digest)?;
129
130 let depl_policy_hash = get_selinux_policy_for_deployment(storage, booted_cmdline, depl_id)?;
131
132 let sl_policy_match = match (booted_policy_hash, depl_policy_hash) {
133 (Some(booted_csum), Some(target_csum)) => booted_csum == target_csum,
135 (Some(_), None) | (None, Some(_)) => false,
137 (None, None) => true,
139 };
140
141 if !sl_policy_match {
142 tracing::debug!("Soft rebooting not allowed due to differing SELinux policies");
143 }
144
145 Ok(sl_policy_match)
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use cap_std_ext::cap_std::ambient_authority;
152 use cap_std_ext::dirext::CapStdExtDirExt;
153
154 #[test]
155 fn test_find_latest_policy_file() -> Result<()> {
156 let tempdir = cap_std_ext::cap_tempfile::tempdir(ambient_authority())?;
157
158 tempdir.atomic_write("policy.30", "policy content 30")?;
160 tempdir.atomic_write("policy.31", "policy content 31")?;
161 tempdir.atomic_write("policy.29", "policy content 29")?;
162 tempdir.atomic_write("not_policy.32", "not a policy file")?;
163 tempdir.atomic_write("other_policy.txt", "invalid policy file")?;
164
165 let result = find_latest_policy_file(&tempdir)?;
166 assert_eq!(result, "policy.31");
167
168 Ok(())
169 }
170
171 #[test]
172 fn test_find_latest_policy_file_with_single_file() -> Result<()> {
173 let tempdir = cap_std_ext::cap_tempfile::tempdir(ambient_authority())?;
174
175 tempdir.atomic_write("policy.25", "single policy file")?;
176
177 let result = find_latest_policy_file(&tempdir)?;
178 assert_eq!(result, "policy.25");
179
180 Ok(())
181 }
182
183 #[test]
184 fn test_find_latest_policy_file_no_policy_files() {
185 let tempdir = cap_std_ext::cap_tempfile::tempdir(ambient_authority()).unwrap();
186
187 tempdir
188 .atomic_write("not_policy.txt", "not a policy file")
189 .unwrap();
190 tempdir.atomic_write("other.txt", "invalid format").unwrap();
191
192 let result = find_latest_policy_file(&tempdir);
193 assert!(result.is_err());
194 assert!(
195 result
196 .unwrap_err()
197 .to_string()
198 .contains("Failed to get latest SELinux policy")
199 );
200 }
201
202 #[test]
203 fn test_find_latest_policy_file_invalid_version() {
204 let tempdir = cap_std_ext::cap_tempfile::tempdir(ambient_authority()).unwrap();
205
206 tempdir
207 .atomic_write("policy.abc", "invalid version")
208 .unwrap();
209
210 let result = find_latest_policy_file(&tempdir);
211 assert!(result.is_err());
212 assert!(
213 result
214 .unwrap_err()
215 .to_string()
216 .contains("Parsing abc as int")
217 );
218 }
219
220 #[test]
221 fn test_find_latest_policy_file_negative_version() -> Result<()> {
222 let tempdir = cap_std_ext::cap_tempfile::tempdir(ambient_authority())?;
223
224 tempdir.atomic_write("policy.5", "positive version")?;
225 tempdir.atomic_write("policy.-1", "negative version")?;
226
227 let result = find_latest_policy_file(&tempdir)?;
228 assert_eq!(result, "policy.5");
229
230 Ok(())
231 }
232
233 #[test]
234 fn test_find_latest_policy_file_skips_directories() -> Result<()> {
235 let tempdir = cap_std_ext::cap_tempfile::tempdir(ambient_authority())?;
236
237 tempdir.create_dir("policy.99")?; tempdir.atomic_write("policy.5", "actual policy file")?;
239
240 let result = find_latest_policy_file(&tempdir)?;
241 assert_eq!(result, "policy.5");
242
243 Ok(())
244 }
245
246 #[test]
247 fn test_compute_policy_file_hash() -> Result<()> {
248 let tempdir = cap_std_ext::cap_tempfile::tempdir(ambient_authority())?;
249
250 let test_content = "test policy content for hashing";
251 tempdir.atomic_write("test_policy.30", test_content)?;
252
253 let hash = compute_policy_file_hash(&tempdir, "test_policy.30")?;
254
255 assert_eq!(hash.len(), 64);
257 assert!(hash.chars().all(|c| c.is_ascii_hexdigit()));
258
259 let hash2 = compute_policy_file_hash(&tempdir, "test_policy.30")?;
261 assert_eq!(hash, hash2);
262
263 Ok(())
264 }
265
266 #[test]
267 fn test_compute_policy_file_hash_different_content() -> Result<()> {
268 let tempdir = cap_std_ext::cap_tempfile::tempdir(ambient_authority())?;
269
270 tempdir.atomic_write("policy1.30", "content 1")?;
271 tempdir.atomic_write("policy2.30", "content 2")?;
272
273 let hash1 = compute_policy_file_hash(&tempdir, "policy1.30")?;
274 let hash2 = compute_policy_file_hash(&tempdir, "policy2.30")?;
275
276 assert_ne!(hash1, hash2);
277
278 Ok(())
279 }
280
281 #[test]
282 fn test_compute_policy_file_hash_nonexistent_file() {
283 let tempdir = cap_std_ext::cap_tempfile::tempdir(ambient_authority()).unwrap();
284
285 let result = compute_policy_file_hash(&tempdir, "nonexistent.30");
286 assert!(result.is_err());
287 assert!(
288 result
289 .unwrap_err()
290 .to_string()
291 .contains("Opening policy file")
292 );
293 }
294
295 #[test]
296 fn test_compute_policy_file_hash_empty_file() -> Result<()> {
297 let tempdir = cap_std_ext::cap_tempfile::tempdir(ambient_authority())?;
298
299 tempdir.atomic_write("empty_policy.30", "")?;
300
301 let hash = compute_policy_file_hash(&tempdir, "empty_policy.30")?;
302
303 assert_eq!(hash.len(), 64);
305 assert!(hash.chars().all(|c| c.is_ascii_hexdigit()));
306
307 assert_eq!(
309 hash,
310 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
311 );
312
313 Ok(())
314 }
315}