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!("Failed to enter install_t (running as {current}) - use BOOTC_SETENFORCE0_FALLBACK=1 to override");
171 };
172 Ok(g)
173}
174
175pub(crate) fn new_sepolicy_at(fd: impl AsFd) -> Result<Option<ostree::SePolicy>> {
177 let fd = fd.as_fd();
178 let cancellable = gio::Cancellable::NONE;
179 let sepolicy = ostree::SePolicy::new_at(fd.as_raw_fd(), cancellable)?;
180 let r = if sepolicy.csum().is_none() {
181 None
182 } else {
183 Some(sepolicy)
184 };
185 Ok(r)
186}
187
188#[context("Setting SELinux permissive mode")]
189#[allow(dead_code)]
190pub(crate) fn selinux_set_permissive(permissive: bool) -> Result<()> {
191 let enforce_path = &Utf8Path::new(SELINUXFS).join("enforce");
192 if !enforce_path.exists() {
193 return Ok(());
194 }
195 let mut f = std::fs::File::options().write(true).open(enforce_path)?;
196 f.write_all(if permissive { b"0" } else { b"1" })?;
197 tracing::debug!(
198 "Set SELinux mode: {}",
199 if permissive {
200 "permissive"
201 } else {
202 "enforcing"
203 }
204 );
205 Ok(())
206}
207
208pub(crate) fn xattrs_have_selinux(xattrs: &ostree::glib::Variant) -> bool {
210 let n = xattrs.n_children();
211 for i in 0..n {
212 let child = xattrs.child_value(i);
213 let key = child.child_value(0);
214 let key = key.data_as_bytes();
215 if key == SELINUX_XATTR {
216 return true;
217 }
218 }
219 false
220}
221
222pub(crate) fn require_label(
224 policy: &ostree::SePolicy,
225 destname: &Utf8Path,
226 mode: u32,
227) -> Result<ostree::glib::GString> {
228 policy
229 .label(destname.as_str(), mode, ostree::gio::Cancellable::NONE)?
230 .ok_or_else(|| {
231 anyhow::anyhow!(
232 "No label found in policy '{:?}' for {destname})",
233 policy.csum()
234 )
235 })
236}
237
238pub(crate) fn set_security_selinux(fd: std::os::fd::BorrowedFd, label: &[u8]) -> Result<()> {
240 rustix::fs::fsetxattr(
241 fd,
242 "security.selinux",
243 label,
244 rustix::fs::XattrFlags::empty(),
245 )
246 .context("fsetxattr(security.selinux)")
247}
248
249pub(crate) enum SELinuxLabelState {
252 Unlabeled,
253 Unsupported,
254 Labeled,
255}
256
257pub(crate) fn has_security_selinux(root: &Dir, path: &Utf8Path) -> Result<SELinuxLabelState> {
259 let mut buf = [0u8; 2048];
261 let fdpath = format!("/proc/self/fd/{}/{path}", root.as_raw_fd());
262 match rustix::fs::lgetxattr(fdpath, "security.selinux", &mut buf) {
263 Ok(_) => Ok(SELinuxLabelState::Labeled),
264 Err(rustix::io::Errno::OPNOTSUPP) => Ok(SELinuxLabelState::Unsupported),
265 Err(rustix::io::Errno::NODATA) => Ok(SELinuxLabelState::Unlabeled),
266 Err(e) => Err(e).with_context(|| format!("Failed to look up context for {path:?}")),
267 }
268}
269
270pub(crate) fn set_security_selinux_path(root: &Dir, path: &Utf8Path, label: &[u8]) -> Result<()> {
275 let fdpath = format!("/proc/self/fd/{}/", root.as_raw_fd());
276 let fdpath = &Path::new(&fdpath).join(path);
277 rustix::fs::lsetxattr(
278 fdpath,
279 "security.selinux",
280 label,
281 rustix::fs::XattrFlags::empty(),
282 )?;
283 Ok(())
284}
285
286pub(crate) fn ensure_labeled(
290 root: &Dir,
291 path: &Utf8Path,
292 metadata: &Metadata,
293 policy: &ostree::SePolicy,
294) -> Result<SELinuxLabelState> {
295 let r = has_security_selinux(root, path)?;
296 if matches!(r, SELinuxLabelState::Unlabeled) {
297 relabel(root, metadata, path, None, policy)?;
298 }
299 Ok(r)
300}
301
302pub(crate) fn relabel(
306 root: &Dir,
307 metadata: &Metadata,
308 path: &Utf8Path,
309 as_path: Option<&Utf8Path>,
310 policy: &ostree::SePolicy,
311) -> Result<()> {
312 assert!(!path.starts_with("/"));
313 let as_path = as_path
314 .map(Cow::Borrowed)
315 .unwrap_or_else(|| Utf8Path::new("/").join(path).into());
316 let label = require_label(policy, &as_path, metadata.mode())?;
317 tracing::trace!("Setting label for {path} to {label}");
318 set_security_selinux_path(root, &path, label.as_bytes())
319}
320
321pub(crate) fn relabel_recurse_inner(
322 root: &Dir,
323 path: &mut Utf8PathBuf,
324 mut as_path: Option<&mut Utf8PathBuf>,
325 policy: &ostree::SePolicy,
326) -> Result<()> {
327 let self_meta = root.dir_metadata()?;
329 relabel(
330 root,
331 &self_meta,
332 path,
333 as_path.as_ref().map(|p| p.as_path()),
334 policy,
335 )?;
336
337 for ent in root.read_dir(&path)? {
339 let ent = ent?;
340 let metadata = ent.metadata()?;
341 let name = ent.file_name();
342 let name = name
343 .to_str()
344 .ok_or_else(|| anyhow::anyhow!("Invalid non-UTF-8 filename: {name:?}"))?;
345 path.push(name);
347 if let Some(p) = as_path.as_mut() {
348 p.push(name);
349 }
350
351 if metadata.is_dir() {
352 let as_path = as_path.as_deref_mut();
353 relabel_recurse_inner(root, path, as_path, policy)?;
354 } else {
355 let as_path = as_path.as_ref().map(|p| p.as_path());
356 relabel(root, &metadata, &path, as_path, policy)?
357 }
358 let r = path.pop();
360 assert!(r);
361 if let Some(p) = as_path.as_mut() {
362 let r = p.pop();
363 assert!(r);
364 }
365 }
366
367 Ok(())
368}
369
370pub(crate) fn relabel_recurse(
372 root: &Dir,
373 path: impl AsRef<Utf8Path>,
374 as_path: Option<&Utf8Path>,
375 policy: &ostree::SePolicy,
376) -> Result<()> {
377 let mut path = path.as_ref().to_owned();
378 assert!(!path.starts_with("/"));
380 let mut as_path = as_path.map(|v| v.to_owned());
381 if let Some(as_path) = as_path.as_deref() {
383 assert!(as_path.starts_with("/"));
384 }
385 relabel_recurse_inner(root, &mut path, as_path.as_mut(), policy)
386}
387
388pub(crate) fn ensure_dir_labeled_recurse(
391 root: &Dir,
392 path: &mut Utf8PathBuf,
393 policy: &ostree::SePolicy,
394 skip: Option<(libc::dev_t, libc::ino64_t)>,
395) -> Result<()> {
396 let path_for_read = if path.as_str().is_empty() {
400 Utf8Path::new(".")
401 } else {
402 &*path
403 };
404
405 let mut n = 0u64;
406
407 let metadata = root.symlink_metadata(path_for_read)?;
408 match ensure_labeled(root, path, &metadata, policy)? {
409 SELinuxLabelState::Unlabeled => {
410 n += 1;
411 }
412 SELinuxLabelState::Unsupported => return Ok(()),
413 SELinuxLabelState::Labeled => {}
414 }
415
416 for ent in root.read_dir(path_for_read)? {
417 let ent = ent?;
418 let metadata = ent.metadata()?;
419 if let Some((skip_dev, skip_ino)) = skip.as_ref().copied() {
420 if (metadata.dev(), metadata.ino()) == (skip_dev, skip_ino) {
421 tracing::debug!("Skipping dev={skip_dev} inode={skip_ino}");
422 continue;
423 }
424 }
425 let name = ent.file_name();
426 let name = name
427 .to_str()
428 .ok_or_else(|| anyhow::anyhow!("Invalid non-UTF-8 filename: {name:?}"))?;
429 path.push(name);
430
431 if metadata.is_dir() {
432 ensure_dir_labeled_recurse(root, path, policy, skip)?;
433 } else {
434 match ensure_labeled(root, path, &metadata, policy)? {
435 SELinuxLabelState::Unlabeled => {
436 n += 1;
437 }
438 SELinuxLabelState::Unsupported => break,
439 SELinuxLabelState::Labeled => {}
440 }
441 }
442 path.pop();
443 }
444
445 if n > 0 {
446 tracing::debug!("Relabeled {n} objects in {path}");
447 }
448 Ok(())
449}
450
451pub(crate) fn ensure_dir_labeled(
453 root: &Dir,
454 destname: impl AsRef<Utf8Path>,
455 as_path: Option<&Utf8Path>,
456 mode: rustix::fs::Mode,
457 policy: Option<&ostree::SePolicy>,
458) -> Result<()> {
459 use std::borrow::Cow;
460
461 let destname = destname.as_ref();
462 let local_destname = if destname.as_str().is_empty() {
464 ".".into()
465 } else {
466 destname
467 };
468 tracing::debug!("Labeling {local_destname}");
469 let label = policy
470 .map(|policy| {
471 let as_path = as_path
472 .map(Cow::Borrowed)
473 .unwrap_or_else(|| Utf8Path::new("/").join(destname).into());
474 require_label(policy, &as_path, libc::S_IFDIR | mode.as_raw_mode())
475 })
476 .transpose()
477 .with_context(|| format!("Labeling {local_destname}"))?;
478 tracing::trace!("Label for {local_destname} is {label:?}");
479
480 root.ensure_dir_with(local_destname, &DirBuilder::new())
481 .with_context(|| format!("Opening {local_destname}"))?;
482 let dirfd = cap_std_ext::cap_primitives::fs::open(
483 &root.as_filelike_view(),
484 local_destname.as_std_path(),
485 OpenOptions::new().read(true),
486 )
487 .context("opendir")?;
488 let dirfd = dirfd.as_fd();
489 rustix::fs::fchmod(dirfd, mode).context("fchmod")?;
490 if let Some(label) = label {
491 set_security_selinux(dirfd, label.as_bytes())?;
492 }
493
494 Ok(())
495}
496
497pub(crate) fn atomic_replace_labeled<F>(
499 root: &Dir,
500 destname: impl AsRef<Utf8Path>,
501 mode: rustix::fs::Mode,
502 policy: Option<&ostree::SePolicy>,
503 f: F,
504) -> Result<()>
505where
506 F: FnOnce(&mut std::io::BufWriter<cap_std_ext::cap_tempfile::TempFile>) -> Result<()>,
507{
508 let destname = destname.as_ref();
509 let label = policy
510 .map(|policy| {
511 let abs_destname = Utf8Path::new("/").join(destname);
512 require_label(policy, &abs_destname, libc::S_IFREG | mode.as_raw_mode())
513 })
514 .transpose()?;
515
516 root.atomic_replace_with(destname, |w| {
517 let fd = w.get_mut();
519 let fd = fd.as_file_mut();
520 let fd = fd.as_fd();
521 rustix::fs::fchmod(fd, mode).context("fchmod")?;
523 if let Some(label) = label {
525 tracing::debug!("Setting label for {destname} to {label}");
526 set_security_selinux(fd, label.as_bytes())?;
527 } else {
528 tracing::debug!("No label for {destname}");
529 }
530 f(w)
532 })
533}
534
535#[cfg(test)]
536mod tests {
537 use super::*;
538 use gio::glib::Variant;
539
540 #[test]
541 fn test_selinux_xattr() {
542 let notfound: &[&[(&[u8], &[u8])]] = &[&[], &[(b"foo", b"bar")]];
543 for case in notfound {
544 assert!(!xattrs_have_selinux(&Variant::from(case)));
545 }
546 let found: &[(&[u8], &[u8])] = &[(b"foo", b"bar"), (SELINUX_XATTR, b"foo_t")];
547 assert!(xattrs_have_selinux(&Variant::from(found)));
548 }
549}