1use std::borrow::Cow;
2use std::io::Write;
3use std::os::fd::AsRawFd;
4use std::os::unix::process::CommandExt;
5use std::path::Path;
6use std::process::Command;
7
8use anyhow::{Context, Result};
9use bootc_utils::CommandRunExt;
10use camino::{Utf8Path, Utf8PathBuf};
11use cap_std::fs::Dir;
12use cap_std::fs::{DirBuilder, OpenOptions};
13use cap_std::io_lifetimes::AsFilelike;
14use cap_std_ext::cap_std;
15use cap_std_ext::cap_std::fs::{Metadata, MetadataExt};
16use cap_std_ext::dirext::CapStdExtDirExt;
17use fn_error_context::context;
18use ostree_ext::gio;
19use ostree_ext::ostree;
20use rustix::fd::AsFd;
21
22const SELINUXFS: &str = "/sys/fs/selinux";
24const SELINUX_XATTR: &[u8] = b"security.selinux\0";
26const SELF_CURRENT: &str = "/proc/self/attr/current";
27
28#[context("Querying selinux availability")]
29pub(crate) fn selinux_enabled() -> Result<bool> {
30 Path::new("/proc/1/root/sys/fs/selinux/enforce")
31 .try_exists()
32 .map_err(Into::into)
33}
34
35fn get_current_security_context() -> Result<String> {
37 std::fs::read_to_string(SELF_CURRENT).with_context(|| format!("Reading {SELF_CURRENT}"))
38}
39
40#[context("Testing install_t")]
45fn test_install_t() -> Result<bool> {
46 let tmpf = tempfile::NamedTempFile::new()?;
47 let st = Command::new("chcon")
50 .args(["-t", "invalid_bootcinstall_testlabel_t"])
51 .arg(tmpf.path())
52 .stderr(std::process::Stdio::null())
53 .status()?;
54 Ok(st.success())
55}
56
57#[context("Ensuring selinux install_t type")]
72pub(crate) fn selinux_ensure_install() -> Result<bool> {
73 let guardenv = "_bootc_selinuxfs_mounted";
74 let current = get_current_security_context()?;
75 tracing::debug!("Current security context is {current}");
76 if let Some(p) = std::env::var_os(guardenv) {
77 let p = Path::new(&p);
78 if p.exists() {
79 tracing::debug!("Removing temporary file");
80 std::fs::remove_file(p).context("Removing {p:?}")?;
81 } else {
82 tracing::debug!("Assuming we now have a privileged (e.g. install_t) label");
83 }
84 return test_install_t();
85 }
86 if test_install_t()? {
87 tracing::debug!("We have install_t");
88 return Ok(true);
89 }
90 tracing::debug!("Lacking install_t capabilities; copying self to temporary file for re-exec");
91 let mut tmpf = tempfile::NamedTempFile::new()?;
95 let srcpath = std::env::current_exe()?;
96 let mut src = std::fs::File::open(&srcpath)?;
97 let meta = src.metadata()?;
98 std::io::copy(&mut src, &mut tmpf).context("Copying self to tempfile for selinux re-exec")?;
99 tmpf.as_file_mut()
100 .set_permissions(meta.permissions())
101 .context("Setting permissions of tempfile")?;
102 let container_root = Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
103 let policy = ostree::SePolicy::new_at(container_root.as_raw_fd(), gio::Cancellable::NONE)?;
104 let label = require_label(&policy, "/usr/bin/ostree".into(), libc::S_IFREG | 0o755)?;
105 set_security_selinux(tmpf.as_fd(), label.as_bytes())?;
106 let tmpf: Utf8PathBuf = tmpf.keep()?.1.try_into().unwrap();
107 tracing::debug!("Created {tmpf:?}");
108
109 let mut cmd = Command::new(&tmpf);
110 cmd.env(guardenv, tmpf);
111 cmd.env(bootc_utils::reexec::ORIG, srcpath);
112 cmd.args(std::env::args_os().skip(1));
113 cmd.arg0(bootc_utils::NAME);
114 cmd.log_debug();
115 Err(anyhow::Error::msg(cmd.exec()).context("execve"))
116}
117
118pub(crate) fn have_selinux_policy(root: &Dir) -> Result<bool> {
120 root.try_exists("etc/selinux/config").map_err(Into::into)
122}
123
124#[must_use]
128#[derive(Debug)]
129#[allow(dead_code)]
130pub(crate) struct SetEnforceGuard(Option<()>);
131
132impl SetEnforceGuard {
133 pub(crate) fn new() -> Self {
134 SetEnforceGuard(Some(()))
135 }
136
137 #[allow(dead_code)]
138 pub(crate) fn consume(mut self) -> Result<()> {
139 self.0.take().unwrap();
141 selinux_set_permissive(false)
143 }
144}
145
146impl Drop for SetEnforceGuard {
147 fn drop(&mut self) {
148 if let Some(()) = self.0.take() {
150 let _ = selinux_set_permissive(false);
151 }
152 }
153}
154
155#[context("Ensuring selinux install_t type")]
158pub(crate) fn selinux_ensure_install_or_setenforce() -> Result<Option<SetEnforceGuard>> {
159 if selinux_ensure_install()? {
162 return Ok(None);
163 }
164 let g = if std::env::var_os("BOOTC_SETENFORCE0_FALLBACK").is_some() {
165 tracing::warn!("Failed to enter install_t; temporarily setting permissive mode");
166 selinux_set_permissive(true)?;
167 Some(SetEnforceGuard::new())
168 } else {
169 let current = get_current_security_context()?;
170 anyhow::bail!(
171 "Failed to enter install_t (running as {current}) - use BOOTC_SETENFORCE0_FALLBACK=1 to override"
172 );
173 };
174 Ok(g)
175}
176
177pub(crate) fn new_sepolicy_at(fd: impl AsFd) -> Result<Option<ostree::SePolicy>> {
179 let fd = fd.as_fd();
180 let cancellable = gio::Cancellable::NONE;
181 let sepolicy = ostree::SePolicy::new_at(fd.as_raw_fd(), cancellable)?;
182 let r = if sepolicy.csum().is_none() {
183 None
184 } else {
185 Some(sepolicy)
186 };
187 Ok(r)
188}
189
190#[context("Setting SELinux permissive mode")]
191#[allow(dead_code)]
192pub(crate) fn selinux_set_permissive(permissive: bool) -> Result<()> {
193 let enforce_path = &Utf8Path::new(SELINUXFS).join("enforce");
194 if !enforce_path.exists() {
195 return Ok(());
196 }
197 let mut f = std::fs::File::options().write(true).open(enforce_path)?;
198 f.write_all(if permissive { b"0" } else { b"1" })?;
199 tracing::debug!(
200 "Set SELinux mode: {}",
201 if permissive {
202 "permissive"
203 } else {
204 "enforcing"
205 }
206 );
207 Ok(())
208}
209
210pub(crate) fn xattrs_have_selinux(xattrs: &ostree::glib::Variant) -> bool {
212 let n = xattrs.n_children();
213 for i in 0..n {
214 let child = xattrs.child_value(i);
215 let key = child.child_value(0);
216 let key = key.data_as_bytes();
217 if key == SELINUX_XATTR {
218 return true;
219 }
220 }
221 false
222}
223
224pub(crate) fn require_label(
226 policy: &ostree::SePolicy,
227 destname: &Utf8Path,
228 mode: u32,
229) -> Result<ostree::glib::GString> {
230 policy
231 .label(destname.as_str(), mode, ostree::gio::Cancellable::NONE)?
232 .ok_or_else(|| {
233 anyhow::anyhow!(
234 "No label found in policy '{:?}' for {destname})",
235 policy.csum()
236 )
237 })
238}
239
240pub(crate) fn set_security_selinux(fd: std::os::fd::BorrowedFd, label: &[u8]) -> Result<()> {
242 rustix::fs::fsetxattr(
243 fd,
244 "security.selinux",
245 label,
246 rustix::fs::XattrFlags::empty(),
247 )
248 .context("fsetxattr(security.selinux)")
249}
250
251pub(crate) enum SELinuxLabelState {
254 Unlabeled,
255 Unsupported,
256 Labeled,
257}
258
259pub(crate) fn has_security_selinux(root: &Dir, path: &Utf8Path) -> Result<SELinuxLabelState> {
261 let mut buf = [0u8; 2048];
263 let fdpath = format!("/proc/self/fd/{}/{path}", root.as_raw_fd());
264 match rustix::fs::lgetxattr(fdpath, "security.selinux", &mut buf) {
265 Ok(_) => Ok(SELinuxLabelState::Labeled),
266 Err(rustix::io::Errno::OPNOTSUPP) => Ok(SELinuxLabelState::Unsupported),
267 Err(rustix::io::Errno::NODATA) => Ok(SELinuxLabelState::Unlabeled),
268 Err(e) => Err(e).with_context(|| format!("Failed to look up context for {path:?}")),
269 }
270}
271
272pub(crate) fn set_security_selinux_path(root: &Dir, path: &Utf8Path, label: &[u8]) -> Result<()> {
277 let fdpath = format!("/proc/self/fd/{}/", root.as_raw_fd());
278 let fdpath = &Path::new(&fdpath).join(path);
279 rustix::fs::lsetxattr(
280 fdpath,
281 "security.selinux",
282 label,
283 rustix::fs::XattrFlags::empty(),
284 )?;
285 Ok(())
286}
287
288pub(crate) fn ensure_labeled(
292 root: &Dir,
293 path: &Utf8Path,
294 metadata: &Metadata,
295 policy: &ostree::SePolicy,
296) -> Result<SELinuxLabelState> {
297 let r = has_security_selinux(root, path)?;
298 if matches!(r, SELinuxLabelState::Unlabeled) {
299 relabel(root, metadata, path, None, policy)?;
300 }
301 Ok(r)
302}
303
304pub(crate) fn relabel(
308 root: &Dir,
309 metadata: &Metadata,
310 path: &Utf8Path,
311 as_path: Option<&Utf8Path>,
312 policy: &ostree::SePolicy,
313) -> Result<()> {
314 assert!(!path.starts_with("/"));
315 let as_path = as_path
316 .map(Cow::Borrowed)
317 .unwrap_or_else(|| Utf8Path::new("/").join(path).into());
318 let label = require_label(policy, &as_path, metadata.mode())?;
319 tracing::trace!("Setting label for {path} to {label}");
320 set_security_selinux_path(root, &path, label.as_bytes())
321}
322
323pub(crate) fn relabel_recurse_inner(
324 root: &Dir,
325 path: &mut Utf8PathBuf,
326 mut as_path: Option<&mut Utf8PathBuf>,
327 policy: &ostree::SePolicy,
328) -> Result<()> {
329 let self_meta = root.dir_metadata()?;
331 relabel(
332 root,
333 &self_meta,
334 path,
335 as_path.as_ref().map(|p| p.as_path()),
336 policy,
337 )?;
338
339 for ent in root.read_dir(&path)? {
341 let ent = ent?;
342 let metadata = ent.metadata()?;
343 let name = ent.file_name();
344 let name = name
345 .to_str()
346 .ok_or_else(|| anyhow::anyhow!("Invalid non-UTF-8 filename: {name:?}"))?;
347 path.push(name);
349 if let Some(p) = as_path.as_mut() {
350 p.push(name);
351 }
352
353 if metadata.is_dir() {
354 let as_path = as_path.as_deref_mut();
355 relabel_recurse_inner(root, path, as_path, policy)?;
356 } else {
357 let as_path = as_path.as_ref().map(|p| p.as_path());
358 relabel(root, &metadata, &path, as_path, policy)?
359 }
360 let r = path.pop();
362 assert!(r);
363 if let Some(p) = as_path.as_mut() {
364 let r = p.pop();
365 assert!(r);
366 }
367 }
368
369 Ok(())
370}
371
372pub(crate) fn relabel_recurse(
374 root: &Dir,
375 path: impl AsRef<Utf8Path>,
376 as_path: Option<&Utf8Path>,
377 policy: &ostree::SePolicy,
378) -> Result<()> {
379 let mut path = path.as_ref().to_owned();
380 assert!(!path.starts_with("/"));
382 let mut as_path = as_path.map(|v| v.to_owned());
383 if let Some(as_path) = as_path.as_deref() {
385 assert!(as_path.starts_with("/"));
386 }
387 relabel_recurse_inner(root, &mut path, as_path.as_mut(), policy)
388}
389
390pub(crate) fn ensure_dir_labeled_recurse(
393 root: &Dir,
394 path: &mut Utf8PathBuf,
395 policy: &ostree::SePolicy,
396 skip: Option<(libc::dev_t, libc::ino64_t)>,
397) -> Result<()> {
398 let path_for_read = if path.as_str().is_empty() {
402 Utf8Path::new(".")
403 } else {
404 &*path
405 };
406
407 let mut n = 0u64;
408
409 let metadata = root.symlink_metadata(path_for_read)?;
410 match ensure_labeled(root, path, &metadata, policy)? {
411 SELinuxLabelState::Unlabeled => {
412 n += 1;
413 }
414 SELinuxLabelState::Unsupported => return Ok(()),
415 SELinuxLabelState::Labeled => {}
416 }
417
418 for ent in root.read_dir(path_for_read)? {
419 let ent = ent?;
420 let metadata = ent.metadata()?;
421 if let Some((skip_dev, skip_ino)) = skip.as_ref().copied() {
422 if (metadata.dev(), metadata.ino()) == (skip_dev, skip_ino) {
423 tracing::debug!("Skipping dev={skip_dev} inode={skip_ino}");
424 continue;
425 }
426 }
427 let name = ent.file_name();
428 let name = name
429 .to_str()
430 .ok_or_else(|| anyhow::anyhow!("Invalid non-UTF-8 filename: {name:?}"))?;
431 path.push(name);
432
433 if metadata.is_dir() {
434 ensure_dir_labeled_recurse(root, path, policy, skip)?;
435 } else {
436 match ensure_labeled(root, path, &metadata, policy)? {
437 SELinuxLabelState::Unlabeled => {
438 n += 1;
439 }
440 SELinuxLabelState::Unsupported => break,
441 SELinuxLabelState::Labeled => {}
442 }
443 }
444 path.pop();
445 }
446
447 if n > 0 {
448 tracing::debug!("Relabeled {n} objects in {path}");
449 }
450 Ok(())
451}
452
453pub(crate) fn ensure_dir_labeled(
455 root: &Dir,
456 destname: impl AsRef<Utf8Path>,
457 as_path: Option<&Utf8Path>,
458 mode: rustix::fs::Mode,
459 policy: Option<&ostree::SePolicy>,
460) -> Result<()> {
461 use std::borrow::Cow;
462
463 let destname = destname.as_ref();
464 let local_destname = if destname.as_str().is_empty() {
466 ".".into()
467 } else {
468 destname
469 };
470 tracing::debug!("Labeling {local_destname}");
471 let label = policy
472 .map(|policy| {
473 let as_path = as_path
474 .map(Cow::Borrowed)
475 .unwrap_or_else(|| Utf8Path::new("/").join(destname).into());
476 require_label(policy, &as_path, libc::S_IFDIR | mode.as_raw_mode())
477 })
478 .transpose()
479 .with_context(|| format!("Labeling {local_destname}"))?;
480 tracing::trace!("Label for {local_destname} is {label:?}");
481
482 root.ensure_dir_with(local_destname, &DirBuilder::new())
483 .with_context(|| format!("Opening {local_destname}"))?;
484 let dirfd = cap_std_ext::cap_primitives::fs::open(
485 &root.as_filelike_view(),
486 local_destname.as_std_path(),
487 OpenOptions::new().read(true),
488 )
489 .context("opendir")?;
490 let dirfd = dirfd.as_fd();
491 rustix::fs::fchmod(dirfd, mode).context("fchmod")?;
492 if let Some(label) = label {
493 set_security_selinux(dirfd, label.as_bytes())?;
494 }
495
496 Ok(())
497}
498
499pub(crate) fn atomic_replace_labeled<F>(
501 root: &Dir,
502 destname: impl AsRef<Utf8Path>,
503 mode: rustix::fs::Mode,
504 policy: Option<&ostree::SePolicy>,
505 f: F,
506) -> Result<()>
507where
508 F: FnOnce(&mut std::io::BufWriter<cap_std_ext::cap_tempfile::TempFile>) -> Result<()>,
509{
510 let destname = destname.as_ref();
511 let label = policy
512 .map(|policy| {
513 let abs_destname = Utf8Path::new("/").join(destname);
514 require_label(policy, &abs_destname, libc::S_IFREG | mode.as_raw_mode())
515 })
516 .transpose()?;
517
518 root.atomic_replace_with(destname, |w| {
519 let fd = w.get_mut();
521 let fd = fd.as_file_mut();
522 let fd = fd.as_fd();
523 rustix::fs::fchmod(fd, mode).context("fchmod")?;
525 if let Some(label) = label {
527 tracing::debug!("Setting label for {destname} to {label}");
528 set_security_selinux(fd, label.as_bytes())?;
529 } else {
530 tracing::debug!("No label for {destname}");
531 }
532 f(w)
534 })
535}
536
537#[cfg(test)]
538mod tests {
539 use super::*;
540 use gio::glib::Variant;
541
542 #[test]
543 fn test_selinux_xattr() {
544 let notfound: &[&[(&[u8], &[u8])]] = &[&[], &[(b"foo", b"bar")]];
545 for case in notfound {
546 assert!(!xattrs_have_selinux(&Variant::from(case)));
547 }
548 let found: &[(&[u8], &[u8])] = &[(b"foo", b"bar"), (SELINUX_XATTR, b"foo_t")];
549 assert!(xattrs_have_selinux(&Variant::from(found)));
550 }
551}