1use std::borrow::Cow;
2#[cfg(feature = "selinux")]
3use std::ffi::CString;
4use std::io::Write;
5use std::os::fd::AsRawFd;
6use std::os::unix::process::CommandExt;
7use std::path::Path;
8use std::process::Command;
9
10use anyhow::{Context, Result};
11use bootc_utils::CommandRunExt;
12use camino::{Utf8Path, Utf8PathBuf};
13use cap_std::fs::Dir;
14use cap_std::fs::{DirBuilder, OpenOptions};
15use cap_std::io_lifetimes::AsFilelike;
16use cap_std_ext::cap_std;
17use cap_std_ext::cap_std::fs::{Metadata, MetadataExt};
18use cap_std_ext::dirext::CapStdExtDirExt;
19use fn_error_context::context;
20use ostree_ext::gio;
21use ostree_ext::ostree;
22use rustix::fd::AsFd;
23
24const SELINUXFS: &str = "/sys/fs/selinux";
26const SELINUX_XATTR: &[u8] = b"security.selinux\0";
28#[cfg(feature = "selinux")]
30const SELINUX_INITIAL_SID_UNLABELED: &str = "unlabeled";
31const SELF_CURRENT: &str = "/proc/self/attr/current";
32
33#[cfg(feature = "selinux")]
38pub(crate) fn selinux_enabled() -> bool {
39 selinux::kernel_support() != selinux::KernelSupport::Unsupported
40}
41
42#[cfg(not(feature = "selinux"))]
44pub(crate) fn selinux_enabled() -> bool {
45 false
46}
47
48#[cfg(feature = "selinux")]
53#[context("Querying host SELinux availability")]
54pub(crate) fn host_selinux_enabled() -> Result<bool> {
55 Path::new("/proc/1/root/sys/fs/selinux/enforce")
56 .try_exists()
57 .map_err(Into::into)
58}
59
60#[cfg(not(feature = "selinux"))]
62pub(crate) fn host_selinux_enabled() -> Result<bool> {
63 Ok(false)
64}
65
66fn get_current_security_context() -> Result<String> {
68 std::fs::read_to_string(SELF_CURRENT).with_context(|| format!("Reading {SELF_CURRENT}"))
69}
70
71#[context("Testing install_t")]
76fn test_install_t() -> Result<bool> {
77 let tmpf = tempfile::NamedTempFile::new()?;
78 let st = Command::new("chcon")
81 .args(["-t", "invalid_bootcinstall_testlabel_t"])
82 .arg(tmpf.path())
83 .stderr(std::process::Stdio::null())
84 .status()?;
85 Ok(st.success())
86}
87
88#[context("Ensuring selinux install_t type")]
103pub(crate) fn selinux_ensure_install(reexec_env: &[(&str, &str)]) -> Result<bool> {
104 let guardenv = "_bootc_selinuxfs_mounted";
105 let current = get_current_security_context()?;
106 tracing::debug!("Current security context is {current}");
107 if let Some(p) = std::env::var_os(guardenv) {
108 let p = Path::new(&p);
109 if p.exists() {
110 tracing::debug!("Removing temporary file");
111 std::fs::remove_file(p).context("Removing {p:?}")?;
112 } else {
113 tracing::debug!("Assuming we now have a privileged (e.g. install_t) label");
114 }
115 return test_install_t();
116 }
117 if test_install_t()? {
118 tracing::debug!("We have install_t");
119 return Ok(true);
120 }
121 tracing::debug!("Lacking install_t capabilities; copying self to temporary file for re-exec");
122 let mut tmpf = tempfile::NamedTempFile::new()?;
126 let srcpath = std::env::current_exe()?;
127 let mut src = std::fs::File::open(&srcpath)?;
128 let meta = src.metadata()?;
129 std::io::copy(&mut src, &mut tmpf).context("Copying self to tempfile for selinux re-exec")?;
130 tmpf.as_file_mut()
131 .set_permissions(meta.permissions())
132 .context("Setting permissions of tempfile")?;
133 let container_root = Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
134 let policy = ostree::SePolicy::new_at(container_root.as_raw_fd(), gio::Cancellable::NONE)?;
135 let label = require_label(&policy, "/usr/bin/ostree".into(), libc::S_IFREG | 0o755)?;
136 set_security_selinux(tmpf.as_fd(), label.as_bytes())?;
137 let tmpf: Utf8PathBuf = tmpf.keep()?.1.try_into().unwrap();
138 tracing::debug!("Created {tmpf:?}");
139
140 let mut cmd = Command::new(&tmpf);
141 cmd.env(guardenv, tmpf);
142 cmd.env(bootc_utils::reexec::ORIG, srcpath);
143 bootc_utils::reexec::prepare_reexec(&mut cmd, reexec_env);
144 cmd.log_debug();
145 Err(anyhow::Error::msg(cmd.exec()).context("execve"))
146}
147
148pub(crate) fn have_selinux_policy(root: &Dir) -> Result<bool> {
150 root.try_exists("etc/selinux/config").map_err(Into::into)
152}
153
154#[must_use]
158#[derive(Debug)]
159#[allow(dead_code)]
160pub(crate) struct SetEnforceGuard(Option<()>);
161
162impl SetEnforceGuard {
163 pub(crate) fn new() -> Self {
164 SetEnforceGuard(Some(()))
165 }
166
167 #[allow(dead_code)]
168 pub(crate) fn consume(mut self) -> Result<()> {
169 self.0.take().unwrap();
171 selinux_set_permissive(false)
173 }
174}
175
176impl Drop for SetEnforceGuard {
177 fn drop(&mut self) {
178 if let Some(()) = self.0.take() {
180 let _ = selinux_set_permissive(false);
181 }
182 }
183}
184
185#[context("Ensuring selinux install_t type")]
188pub(crate) fn selinux_ensure_install_or_setenforce(
189 reexec_env: &[(&str, &str)],
190) -> Result<Option<SetEnforceGuard>> {
191 if selinux_ensure_install(reexec_env)? {
194 return Ok(None);
195 }
196 let g = if std::env::var_os("BOOTC_SETENFORCE0_FALLBACK").is_some() {
197 tracing::warn!("Failed to enter install_t; temporarily setting permissive mode");
198 selinux_set_permissive(true)?;
199 Some(SetEnforceGuard::new())
200 } else {
201 let current = get_current_security_context()?;
202 anyhow::bail!(
203 "Failed to enter install_t (running as {current}) - use BOOTC_SETENFORCE0_FALLBACK=1 to override"
204 );
205 };
206 Ok(g)
207}
208
209pub(crate) fn new_sepolicy_at(fd: impl AsFd) -> Result<Option<ostree::SePolicy>> {
211 let fd = fd.as_fd();
212 let cancellable = gio::Cancellable::NONE;
213 let sepolicy = ostree::SePolicy::new_at(fd.as_raw_fd(), cancellable)?;
214 let r = if sepolicy.csum().is_none() {
215 None
216 } else {
217 Some(sepolicy)
218 };
219 Ok(r)
220}
221
222#[context("Setting SELinux permissive mode")]
223#[allow(dead_code)]
224pub(crate) fn selinux_set_permissive(permissive: bool) -> Result<()> {
225 let enforce_path = &Utf8Path::new(SELINUXFS).join("enforce");
226 if !enforce_path.exists() {
227 return Ok(());
228 }
229 let mut f = std::fs::File::options().write(true).open(enforce_path)?;
230 f.write_all(if permissive { b"0" } else { b"1" })?;
231 tracing::debug!(
232 "Set SELinux mode: {}",
233 if permissive {
234 "permissive"
235 } else {
236 "enforcing"
237 }
238 );
239 Ok(())
240}
241
242pub(crate) fn xattrs_have_selinux(xattrs: &ostree::glib::Variant) -> bool {
244 let n = xattrs.n_children();
245 for i in 0..n {
246 let child = xattrs.child_value(i);
247 let key = child.child_value(0);
248 let key = key.data_as_bytes();
249 if key == SELINUX_XATTR {
250 return true;
251 }
252 }
253 false
254}
255
256pub(crate) fn require_label(
258 policy: &ostree::SePolicy,
259 destname: &Utf8Path,
260 mode: u32,
261) -> Result<ostree::glib::GString> {
262 policy
263 .label(destname.as_str(), mode, ostree::gio::Cancellable::NONE)?
264 .ok_or_else(|| {
265 anyhow::anyhow!(
266 "No label found in policy '{:?}' for {destname})",
267 policy.csum()
268 )
269 })
270}
271
272pub(crate) fn optional_label(
274 policy: &ostree::SePolicy,
275 destname: &Utf8Path,
276 mode: u32,
277) -> Result<Option<ostree::glib::GString>> {
278 policy
279 .label(destname.as_str(), mode, ostree::gio::Cancellable::NONE)
280 .map_err(Into::into)
281}
282
283pub(crate) fn set_security_selinux(fd: std::os::fd::BorrowedFd, label: &[u8]) -> Result<()> {
285 rustix::fs::fsetxattr(
286 fd,
287 "security.selinux",
288 label,
289 rustix::fs::XattrFlags::empty(),
290 )
291 .context("fsetxattr(security.selinux)")
292}
293
294#[derive(Debug, PartialEq, Eq)]
297pub(crate) enum SELinuxLabelState {
298 Unlabeled,
299 Unsupported,
300 Labeled,
301}
302
303#[cfg(feature = "selinux")]
307#[derive(Debug)]
308struct UnlabeledType(CString);
309
310#[cfg(not(feature = "selinux"))]
313#[derive(Debug)]
314enum UnlabeledType {}
315
316impl UnlabeledType {
317 #[cfg(feature = "selinux")]
319 fn query() -> Result<Option<Self>> {
320 if !selinux_enabled() {
321 return Ok(None);
322 }
323 let context = selinux::SecurityContext::of_initial_kernel_context(
324 SELINUX_INITIAL_SID_UNLABELED,
325 true,
326 )
327 .context("Querying the SELinux unlabeled initial context")?;
328 context_type(context.as_bytes()).map(|t| Some(Self(t)))
329 }
330
331 #[cfg(not(feature = "selinux"))]
332 fn query() -> Result<Option<Self>> {
333 Ok(None)
334 }
335
336 #[cfg(feature = "selinux")]
338 fn matches(&self, context: &[u8]) -> Result<bool> {
339 Ok(context_type(context)?.as_c_str() == self.0.as_c_str())
340 }
341
342 #[cfg(not(feature = "selinux"))]
343 fn matches(&self, _context: &[u8]) -> Result<bool> {
344 match *self {}
345 }
346}
347
348#[cfg(feature = "selinux")]
349fn context_type(context: &[u8]) -> Result<CString> {
350 let context = context.strip_suffix(b"\0").unwrap_or(context);
352 let context = CString::new(context).context("SELinux context contains an interior NUL")?;
353 context
354 .to_str()
355 .context("SELinux context is not valid UTF-8")?;
356 selinux::OpaqueSecurityContext::from_c_str(&context)
357 .context("Parsing SELinux context")?
358 .the_type()
359 .context("Reading SELinux context type")
360}
361
362#[cfg(feature = "selinux")]
364pub(crate) fn has_security_selinux(root: &Dir, path: &Utf8Path) -> Result<SELinuxLabelState> {
365 let unlabeled_type = UnlabeledType::query()?.context("SELinux is not enabled")?;
366 has_security_selinux_inner(root, path, Some(&unlabeled_type))
367}
368
369fn has_security_selinux_inner(
370 root: &Dir,
371 path: &Utf8Path,
372 unlabeled_type: Option<&UnlabeledType>,
373) -> Result<SELinuxLabelState> {
374 let mut buf = [0u8; 2048];
376 let fdpath = format!("/proc/self/fd/{}/{path}", root.as_raw_fd());
377 match rustix::fs::lgetxattr(fdpath, "security.selinux", &mut buf) {
378 Ok(len) => {
379 let Some(unlabeled_type) = unlabeled_type else {
382 return Ok(SELinuxLabelState::Labeled);
383 };
384 let unlabeled = unlabeled_type
385 .matches(&buf[..len])
386 .with_context(|| format!("Parsing SELinux context for {path:?}"))?;
387 if unlabeled {
388 Ok(SELinuxLabelState::Unlabeled)
389 } else {
390 Ok(SELinuxLabelState::Labeled)
391 }
392 }
393 Err(rustix::io::Errno::OPNOTSUPP) => Ok(SELinuxLabelState::Unsupported),
394 Err(rustix::io::Errno::NODATA) => Ok(SELinuxLabelState::Unlabeled),
395 Err(e) => Err(e).with_context(|| format!("Failed to look up context for {path:?}")),
396 }
397}
398
399pub(crate) fn set_security_selinux_path(root: &Dir, path: &Utf8Path, label: &[u8]) -> Result<()> {
404 let fdpath = format!("/proc/self/fd/{}/", root.as_raw_fd());
405 let fdpath = &Path::new(&fdpath).join(path);
406 rustix::fs::lsetxattr(
407 fdpath,
408 "security.selinux",
409 label,
410 rustix::fs::XattrFlags::empty(),
411 )?;
412 Ok(())
413}
414
415fn ensure_labeled_inner(
416 root: &Dir,
417 path: &Utf8Path,
418 metadata: &Metadata,
419 policy: &ostree::SePolicy,
420 unlabeled_type: Option<&UnlabeledType>,
421) -> Result<SELinuxLabelState> {
422 let r = has_security_selinux_inner(root, path, unlabeled_type)?;
423 if matches!(r, SELinuxLabelState::Unlabeled) {
424 relabel(root, metadata, path, None, policy)?;
425 }
426 Ok(r)
427}
428
429pub(crate) fn relabel(
433 root: &Dir,
434 metadata: &Metadata,
435 path: &Utf8Path,
436 as_path: Option<&Utf8Path>,
437 policy: &ostree::SePolicy,
438) -> Result<()> {
439 assert!(!path.starts_with("/"));
440 let as_path = as_path
441 .map(Cow::Borrowed)
442 .unwrap_or_else(|| Utf8Path::new("/").join(path).into());
443 let label = require_label(policy, &as_path, metadata.mode())?;
444 tracing::trace!("Setting label for {path} to {label}");
445 set_security_selinux_path(root, &path, label.as_bytes())
446}
447
448pub(crate) fn relabel_recurse_inner(
449 root: &Dir,
450 path: &mut Utf8PathBuf,
451 mut as_path: Option<&mut Utf8PathBuf>,
452 policy: &ostree::SePolicy,
453) -> Result<()> {
454 let self_meta = root.dir_metadata()?;
456 relabel(
457 root,
458 &self_meta,
459 path,
460 as_path.as_ref().map(|p| p.as_path()),
461 policy,
462 )?;
463
464 for ent in root.read_dir(&path)? {
466 let ent = ent?;
467 let metadata = ent.metadata()?;
468 let name = ent.file_name();
469 let name = name
470 .to_str()
471 .ok_or_else(|| anyhow::anyhow!("Invalid non-UTF-8 filename: {name:?}"))?;
472 path.push(name);
474 if let Some(p) = as_path.as_mut() {
475 p.push(name);
476 }
477
478 if metadata.is_dir() {
479 let as_path = as_path.as_deref_mut();
480 relabel_recurse_inner(root, path, as_path, policy)?;
481 } else {
482 let as_path = as_path.as_ref().map(|p| p.as_path());
483 relabel(root, &metadata, &path, as_path, policy)?
484 }
485 let r = path.pop();
487 assert!(r);
488 if let Some(p) = as_path.as_mut() {
489 let r = p.pop();
490 assert!(r);
491 }
492 }
493
494 Ok(())
495}
496
497pub(crate) fn relabel_recurse(
499 root: &Dir,
500 path: impl AsRef<Utf8Path>,
501 as_path: Option<&Utf8Path>,
502 policy: &ostree::SePolicy,
503) -> Result<()> {
504 let mut path = path.as_ref().to_owned();
505 assert!(!path.starts_with("/"));
507 let mut as_path = as_path.map(|v| v.to_owned());
508 if let Some(as_path) = as_path.as_deref() {
510 assert!(as_path.starts_with("/"));
511 }
512 relabel_recurse_inner(root, &mut path, as_path.as_mut(), policy)
513}
514
515pub(crate) fn ensure_dir_labeled_recurse(
522 root: &Dir,
523 path: &mut Utf8PathBuf,
524 policy: &ostree::SePolicy,
525 skip: Option<(libc::dev_t, libc::ino64_t)>,
526) -> Result<()> {
527 use cap_std_ext::dirext::WalkConfiguration;
528 use std::ops::ControlFlow;
529
530 let unlabeled_type = UnlabeledType::query()?;
533 let path_for_read = if path.as_str().is_empty() {
537 Utf8Path::new(".")
538 } else {
539 &*path
540 };
541
542 let mut n = 0u64;
543
544 let metadata = root.symlink_metadata(path_for_read)?;
546 match ensure_labeled_inner(root, path, &metadata, policy, unlabeled_type.as_ref())? {
547 SELinuxLabelState::Unlabeled => {
548 n += 1;
549 }
550 SELinuxLabelState::Unsupported => return Ok(()),
551 SELinuxLabelState::Labeled => {}
552 }
553 let config = WalkConfiguration::default()
554 .noxdev()
555 .skip_mountpoints()
556 .path_base(path_for_read.as_std_path());
557
558 root.open_dir(path_for_read)?
559 .walk::<_, anyhow::Error>(&config, |component| {
560 let metadata = component.entry.metadata()?;
561
562 if let Some((skip_dev, skip_ino)) = skip {
564 if (metadata.dev(), metadata.ino()) == (skip_dev, skip_ino) {
565 tracing::debug!("Skipping dev={skip_dev} inode={skip_ino}");
566 if component.file_type.is_dir() {
571 return Ok(ControlFlow::Break(()));
572 } else {
573 return Ok(ControlFlow::Continue(()));
574 }
575 }
576 }
577
578 let path = Utf8Path::from_path(component.path)
579 .ok_or_else(|| anyhow::anyhow!("Invalid non-UTF-8 path: {:?}", component.path))?;
580
581 match ensure_labeled_inner(root, path, &metadata, policy, unlabeled_type.as_ref())? {
582 SELinuxLabelState::Unlabeled => {
583 n += 1;
584 }
585 SELinuxLabelState::Unsupported => {
590 anyhow::bail!(
591 "Unexpected SELinuxLabelState::Unsupported during walk at {path}"
592 );
593 }
594 SELinuxLabelState::Labeled => {}
595 }
596
597 Ok(ControlFlow::Continue(()))
598 })?;
599
600 if n > 0 {
601 tracing::debug!("Relabeled {n} objects in {path}");
602 }
603 Ok(())
604}
605
606pub(crate) fn ensure_dir_labeled(
608 root: &Dir,
609 destname: impl AsRef<Utf8Path>,
610 as_path: Option<&Utf8Path>,
611 mode: rustix::fs::Mode,
612 policy: Option<&ostree::SePolicy>,
613) -> Result<()> {
614 use std::borrow::Cow;
615
616 let destname = destname.as_ref();
617 let local_destname = if destname.as_str().is_empty() {
619 ".".into()
620 } else {
621 destname
622 };
623 tracing::debug!("Labeling {local_destname}");
624 let label = policy
625 .map(|policy| {
626 let as_path = as_path
627 .map(Cow::Borrowed)
628 .unwrap_or_else(|| Utf8Path::new("/").join(destname).into());
629 require_label(policy, &as_path, libc::S_IFDIR | mode.as_raw_mode())
630 })
631 .transpose()
632 .with_context(|| format!("Labeling {local_destname}"))?;
633 tracing::trace!("Label for {local_destname} is {label:?}");
634
635 root.ensure_dir_with(local_destname, &DirBuilder::new())
636 .with_context(|| format!("Opening {local_destname}"))?;
637 let dirfd = cap_std_ext::cap_primitives::fs::open(
638 &root.as_filelike_view(),
639 local_destname.as_std_path(),
640 OpenOptions::new().read(true),
641 )
642 .context("opendir")?;
643 let dirfd = dirfd.as_fd();
644 rustix::fs::fchmod(dirfd, mode).context("fchmod")?;
645 if let Some(label) = label {
646 set_security_selinux(dirfd, label.as_bytes())?;
647 }
648
649 Ok(())
650}
651
652pub(crate) fn atomic_replace_labeled<F>(
656 root: &Dir,
657 destname: impl AsRef<Utf8Path>,
658 mode: rustix::fs::Mode,
659 policy: Option<&ostree::SePolicy>,
660 f: F,
661) -> Result<()>
662where
663 F: FnOnce(&mut std::io::BufWriter<cap_std_ext::cap_tempfile::TempFile>) -> Result<()>,
664{
665 let destname = destname.as_ref();
666 let label = policy
667 .map(|policy| {
668 let abs_destname = Utf8Path::new("/").join(destname);
669 require_label(policy, &abs_destname, libc::S_IFREG | mode.as_raw_mode())
670 })
671 .transpose()?;
672
673 root.atomic_replace_with(destname, |w| {
674 let fd = w.get_mut();
676 let fd = fd.as_file_mut();
677 let fd = fd.as_fd();
678 rustix::fs::fchmod(fd, mode).context("fchmod")?;
680 if let Some(label) = label {
682 tracing::debug!("Setting label for {destname} to {label}");
683 set_security_selinux(fd, label.as_bytes())?;
684 } else {
685 tracing::debug!("No label for {destname}");
686 }
687 f(w)
689 })
690}
691
692#[cfg(test)]
693mod tests {
694 use super::*;
695 use gio::glib::Variant;
696
697 #[test]
698 fn test_selinux_xattr() {
699 let notfound: &[&[(&[u8], &[u8])]] = &[&[], &[(b"foo", b"bar")]];
700 for case in notfound {
701 assert!(!xattrs_have_selinux(&Variant::from(case)));
702 }
703 let found: &[(&[u8], &[u8])] = &[(b"foo", b"bar"), (SELINUX_XATTR, b"foo_t")];
704 assert!(xattrs_have_selinux(&Variant::from(found)));
705 }
706
707 #[test]
708 #[cfg(feature = "selinux")]
709 fn test_unlabeled_type() {
710 let Some(unlabeled_type) = UnlabeledType::query().unwrap() else {
712 return;
713 };
714 assert!(
715 unlabeled_type
716 .matches(b"system_u:object_r:unlabeled_t:s0\0")
717 .unwrap()
718 );
719 assert!(
720 !unlabeled_type
721 .matches(b"system_u:object_r:var_t:s0")
722 .unwrap()
723 );
724 }
725
726 #[test]
727 #[cfg(feature = "selinux")]
728 fn test_context_type() {
729 let cases: &[(&[u8], Option<&str>)] = &[
730 (b"system_u:object_r:unlabeled_t:s0", Some("unlabeled_t")),
731 (b"system_u:object_r:unlabeled_t:s0\0", Some("unlabeled_t")),
732 (
733 b"system_u:object_r:unlabeled_t:s0:c0.c1023",
734 Some("unlabeled_t"),
735 ),
736 (b"system_u:object_r:var_t:s0", Some("var_t")),
737 (b"system_u:object_r", None),
738 (b"system_u:object_r:var_t:s0\0\0", None),
739 (b"system_u:object_r:var_t\0:s0", None),
740 (b"system_u:object_r:unlabeled_t:\xff", None),
741 ];
742
743 for (context, expected_type) in cases {
744 assert_eq!(
745 context_type(context)
746 .ok()
747 .and_then(|context_type| context_type.into_string().ok())
748 .as_deref(),
749 *expected_type
750 );
751 }
752 }
753}