diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 933060c8..d4240af2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -132,10 +132,12 @@ jobs: - name: Integration tests run: | set -xeuo pipefail + image=quay.io/centos-bootc/centos-bootc-dev:stream9 echo 'ssh-ed25519 ABC0123 testcase@example.com' > test_authorized_keys sudo podman run --rm -ti --privileged -v ./test_authorized_keys:/test_authorized_keys --env RUST_LOG=debug -v /:/target -v /var/lib/containers:/var/lib/containers -v ./usr/bin/bootc:/usr/bin/bootc --pid=host --security-opt label=disable \ - quay.io/centos-bootc/centos-bootc-dev:stream9 bootc install to-filesystem \ + ${image} bootc install to-filesystem \ --karg=foo=bar --disable-selinux --replace=alongside --root-ssh-authorized-keys=/test_authorized_keys /target + sudo podman run --rm -ti --privileged -v /:/target -v ./usr/bin/bootc:/usr/bin/bootc --pid=host --security-opt label=disable ${image} bootc internal-tests verify-selinux /target/ostree --warn ls -al /boot/loader/ sudo grep foo=bar /boot/loader/entries/*.conf grep authorized_keys /ostree/deploy/default/deploy/*/etc/tmpfiles.d/bootc-root-ssh.conf @@ -143,5 +145,4 @@ jobs: sudo chattr -i /ostree/deploy/default/deploy/* sudo rm /ostree/deploy/default -rf sudo podman run --rm -ti --privileged --env BOOTC_SKIP_SELINUX_HOST_CHECK=1 --env RUST_LOG=debug -v /:/target -v /var/lib/containers:/var/lib/containers -v ./usr/bin/bootc:/usr/bin/bootc --pid=host --security-opt label=disable \ - quay.io/centos-bootc/centos-bootc-dev:stream9 bootc install to-existing-root - sudo ls -ldZ / /ostree/deploy/default/deploy/* /ostree/deploy/default/deploy/*/etc + ${image} bootc install to-existing-root diff --git a/lib/src/cli.rs b/lib/src/cli.rs index 61223b6a..008e387b 100644 --- a/lib/src/cli.rs +++ b/lib/src/cli.rs @@ -150,6 +150,12 @@ pub(crate) enum TestingOpts { image: String, blockdev: Utf8PathBuf, }, + #[clap(name = "verify-selinux")] + VerifySELinux { + root: String, + #[clap(long)] + warn: bool, + }, } /// Deploy and transactionally in-place with bootable container images. diff --git a/lib/src/install.rs b/lib/src/install.rs index 59d7d19a..314dee82 100644 --- a/lib/src/install.rs +++ b/lib/src/install.rs @@ -10,7 +10,6 @@ pub(crate) mod baseline; pub(crate) mod config; pub(crate) mod osconfig; -use std::io::BufWriter; use std::io::Write; use std::os::fd::AsFd; use std::os::unix::process::CommandExt; @@ -303,17 +302,17 @@ pub(crate) struct State { } impl State { - // Wraps core lsm labeling functionality, conditionalizing based on source state - pub(crate) fn lsm_label( - &self, - target: &Utf8Path, - as_path: &Utf8Path, - recurse: bool, - ) -> Result<()> { - if !self.source.selinux { - return Ok(()); + #[context("Loading SELinux policy")] + pub(crate) fn load_policy(&self) -> Result> { + use std::os::fd::AsRawFd; + if !self.source.selinux || self.override_disable_selinux { + return Ok(None); } - crate::lsm::lsm_label(target, as_path, recurse) + // We always use the physical container root to bootstrap policy + let rootfs = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?; + let r = ostree::SePolicy::new_at(rootfs.as_raw_fd(), gio::Cancellable::NONE)?; + tracing::debug!("Loaded SELinux policy: {}", r.name()); + Ok(Some(r)) } } @@ -510,13 +509,17 @@ async fn initialize_ostree_root_from_self( state: &State, root_setup: &RootSetup, ) -> Result { + let sepolicy = state.load_policy()?; + let sepolicy = sepolicy.as_ref(); + + // Load a fd for the mounted target physical root let rootfs_dir = &root_setup.rootfs_fd; let rootfs = root_setup.rootfs.as_path(); let cancellable = gio::Cancellable::NONE; // Ensure that the physical root is labeled. // Another implementation: https://github.com/coreos/coreos-assembler/blob/3cd3307904593b3a131b81567b13a4d0b6fe7c90/src/create_disk.sh#L295 - state.lsm_label(rootfs, "/".into(), false)?; + crate::lsm::ensure_dir_labeled(rootfs_dir, "", 0o755.into(), sepolicy)?; // TODO: make configurable? let stateroot = STATEROOT_DEFAULT; @@ -529,7 +532,7 @@ async fn initialize_ostree_root_from_self( // And also label /boot AKA xbootldr, if it exists let bootdir = rootfs.join("boot"); if bootdir.try_exists()? { - state.lsm_label(&bootdir, "/boot".into(), false)?; + crate::lsm::ensure_dir_labeled(rootfs_dir, "boot", 0o755.into(), sepolicy)?; } // Default to avoiding grub2-mkconfig etc., but we need to use zipl on s390x. @@ -557,8 +560,11 @@ async fn initialize_ostree_root_from_self( .cwd(rootfs_dir)? .run()?; - // Ensure everything in the ostree repo is labeled - state.lsm_label(&rootfs.join("ostree"), "/usr".into(), true)?; + // Bootstrap the initial labeling of the /ostree directory as usr_t + if let Some(policy) = sepolicy { + let ostree_dir = rootfs_dir.open_dir("ostree")?; + crate::lsm::ensure_dir_labeled(&ostree_dir, "/usr", 0o755.into(), Some(policy))?; + } let sysroot = ostree::Sysroot::new(Some(&gio::File::for_path(rootfs))); sysroot.load(cancellable)?; @@ -620,8 +626,6 @@ async fn initialize_ostree_root_from_self( println!("Installed: {target_image}"); println!(" Digest: {digest}"); - // Write the entry for /boot to /etc/fstab. TODO: Encourage OSes to use the karg? - // Or better bind this with the grub data. sysroot.load(cancellable)?; let deployment = sysroot .deployments() @@ -633,28 +637,32 @@ async fn initialize_ostree_root_from_self( let root = rootfs_dir .open_dir(path.as_str()) .context("Opening deployment dir")?; - let root_path = &rootfs.join(&path.as_str()); - let mut f = { - let mut opts = cap_std::fs::OpenOptions::new(); - root.open_with("etc/fstab", opts.append(true).write(true).create(true)) - .context("Opening etc/fstab") - .map(BufWriter::new)? - }; - if let Some(boot) = root_setup.boot.as_ref() { - writeln!(f, "{}", boot.to_fstab())?; + + // And do another recursive relabeling pass over the entire physical root + // but avoid recursing into the deployment root (because that's a *distinct* + // logical root). + if let Some(policy) = sepolicy { + let deployment_root_meta = root.dir_metadata()?; + let deployment_root_devino = (deployment_root_meta.dev(), deployment_root_meta.ino()); + let mut pathbuf = Utf8PathBuf::from(""); + crate::lsm::ensure_dir_labeled_recurse( + rootfs_dir, + &mut pathbuf, + policy, + Some(deployment_root_devino), + )?; } - f.flush()?; - let fstab_path = root_path.join("etc/fstab"); - state.lsm_label(&fstab_path, "/etc/fstab".into(), false)?; + // Write the entry for /boot to /etc/fstab. TODO: Encourage OSes to use the karg? + // Or better bind this with the grub data. + if let Some(boot) = root_setup.boot.as_ref() { + crate::lsm::atomic_replace_labeled(&root, "etc/fstab", 0o644.into(), sepolicy, |w| { + writeln!(w, "{}", boot.to_fstab()).map_err(Into::into) + })?; + } if let Some(contents) = state.root_ssh_authorized_keys.as_deref() { - osconfig::inject_root_ssh_authorized_keys( - &root, - &root_path, - |target, path, recurse| state.lsm_label(target, path, recurse), - contents, - )?; + osconfig::inject_root_ssh_authorized_keys(&root, sepolicy, contents)?; } let uname = rustix::system::uname(); diff --git a/lib/src/install/baseline.rs b/lib/src/install/baseline.rs index 95af9eb5..e1103aff 100644 --- a/lib/src/install/baseline.rs +++ b/lib/src/install/baseline.rs @@ -195,6 +195,10 @@ pub(crate) fn install_create_rootfs( .transpose() .context("Parsing root size")?; + // Load the policy from the container root, which also must be our install root + let sepolicy = state.load_policy()?; + let sepolicy = sepolicy.as_ref(); + // Create a temporary directory to use for mount points. Note that we're // in a mount namespace, so these should not be visible on the host. let rootfs = mntdir.join("rootfs"); @@ -368,15 +372,15 @@ pub(crate) fn install_create_rootfs( .collect::>(); mount::mount(&rootdev, &rootfs)?; - state.lsm_label(&rootfs, "/".into(), false)?; + let target_rootfs = Dir::open_ambient_dir(&rootfs, cap_std::ambient_authority())?; + crate::lsm::ensure_dir_labeled(&target_rootfs, "", 0o755.into(), sepolicy)?; let rootfs_fd = Dir::open_ambient_dir(&rootfs, cap_std::ambient_authority())?; let bootfs = rootfs.join("boot"); - std::fs::create_dir(&bootfs).context("Creating /boot")?; - // The underlying directory on the root should be labeled - state.lsm_label(&bootfs, "/boot".into(), false)?; + // Create the underlying mount point directory, which should be labeled + crate::lsm::ensure_dir_labeled(&target_rootfs, "boot", 0o755.into(), sepolicy)?; mount::mount(bootdev, &bootfs)?; // And we want to label the root mount of /boot - state.lsm_label(&bootfs, "/boot".into(), false)?; + crate::lsm::ensure_dir_labeled(&target_rootfs, "boot", 0o755.into(), sepolicy)?; // Create the EFI system partition, if applicable if let Some(esp_partno) = esp_partno { diff --git a/lib/src/install/osconfig.rs b/lib/src/install/osconfig.rs index 019933c7..a1c7152c 100644 --- a/lib/src/install/osconfig.rs +++ b/lib/src/install/osconfig.rs @@ -1,78 +1,51 @@ +use std::io::Write; + use anyhow::Result; -use camino::Utf8Path; use cap_std::fs::Dir; -use cap_std_ext::{cap_std, dirext::CapStdExtDirExt}; +use cap_std_ext::cap_std; use fn_error_context::context; +use ostree_ext::ostree; const ETC_TMPFILES: &str = "etc/tmpfiles.d"; const ROOT_SSH_TMPFILE: &str = "bootc-root-ssh.conf"; #[context("Injecting root authorized_keys")] -pub(crate) fn inject_root_ssh_authorized_keys( +pub(crate) fn inject_root_ssh_authorized_keys( root: &Dir, - root_path: &Utf8Path, - lsm_label_fn: F, + sepolicy: Option<&ostree::SePolicy>, contents: &str, -) -> Result<()> -where - F: Fn(&Utf8Path, &Utf8Path, bool) -> Result<()>, -{ +) -> Result<()> { // While not documented right now, this one looks like it does not newline wrap let b64_encoded = ostree_ext::glib::base64_encode(contents.as_bytes()); // See the example in https://systemd.io/CREDENTIALS/ let tmpfiles_content = format!("f~ /root/.ssh/authorized_keys 600 root root - {b64_encoded}\n"); - let tmpfiles_dir = Utf8Path::new(ETC_TMPFILES); - root.create_dir_all(tmpfiles_dir)?; - let target = tmpfiles_dir.join(ROOT_SSH_TMPFILE); - root.atomic_write(&target, &tmpfiles_content)?; - - let as_path = Utf8Path::new(ETC_TMPFILES).join(ROOT_SSH_TMPFILE); - lsm_label_fn( - &root_path.join(&as_path), - &Utf8Path::new("/").join(&as_path), - false, + crate::lsm::ensure_dir_labeled(root, ETC_TMPFILES, 0o755.into(), sepolicy)?; + let tmpfiles_dir = root.open_dir(ETC_TMPFILES)?; + crate::lsm::atomic_replace_labeled( + &tmpfiles_dir, + ROOT_SSH_TMPFILE, + 0o644.into(), + sepolicy, + |w| w.write_all(tmpfiles_content.as_bytes()).map_err(Into::into), )?; - println!("Injected: {target}"); + println!("Injected: {ETC_TMPFILES}/{ROOT_SSH_TMPFILE}"); Ok(()) } #[test] fn test_inject_root_ssh() -> Result<()> { - use camino::Utf8PathBuf; - use std::cell::Cell; - - let fake_lsm_label_called = Cell::new(0); - let fake_lsm_label = |target: &Utf8Path, as_path: &Utf8Path, recurse: bool| -> Result<()> { - assert_eq!( - target, - format!("/root/path/etc/tmpfiles.d/{ROOT_SSH_TMPFILE}") - ); - assert_eq!(as_path, format!("/etc/tmpfiles.d/{ROOT_SSH_TMPFILE}")); - assert_eq!(recurse, false); - - fake_lsm_label_called.set(fake_lsm_label_called.get() + 1); - Ok(()) - }; - - let root_path = &Utf8PathBuf::from("/root/path"); let root = &cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?; - inject_root_ssh_authorized_keys( - root, - root_path, - fake_lsm_label, - "ssh-ed25519 ABCDE example@demo\n", - ) - .unwrap(); + // The code expects this to exist, reasonably so + root.create_dir("etc")?; + inject_root_ssh_authorized_keys(root, None, "ssh-ed25519 ABCDE example@demo\n").unwrap(); let content = root.read_to_string(format!("etc/tmpfiles.d/{ROOT_SSH_TMPFILE}"))?; assert_eq!( content, "f~ /root/.ssh/authorized_keys 600 root root - c3NoLWVkMjU1MTkgQUJDREUgZXhhbXBsZUBkZW1vCg==\n" ); - assert_eq!(fake_lsm_label_called, 1.into()); - Ok(()) } diff --git a/lib/src/lsm.rs b/lib/src/lsm.rs index 128b7fb6..11407bec 100644 --- a/lib/src/lsm.rs +++ b/lib/src/lsm.rs @@ -6,13 +6,17 @@ use std::process::Command; use anyhow::{Context, Result}; use camino::{Utf8Path, Utf8PathBuf}; +use cap_std::fs::{Dir, DirBuilder, OpenOptions}; +use cap_std::io_lifetimes::AsFilelike; +use cap_std_ext::cap_std; +use cap_std_ext::dirext::CapStdExtDirExt; use fn_error_context::context; #[cfg(feature = "install")] use gvariant::{aligned_bytes::TryAsAligned, Marker, Structure}; -#[cfg(feature = "install")] +use ostree_ext::gio; use ostree_ext::ostree; - -use crate::task::Task; +use rustix::fd::AsFd; +use std::os::fd::AsRawFd; /// The mount path for selinux #[cfg(feature = "install")] @@ -75,8 +79,11 @@ pub(crate) fn selinux_ensure_install() -> Result { tmpf.as_file_mut() .set_permissions(meta.permissions()) .context("Setting permissions of tempfile")?; + let container_root = Dir::open_ambient_dir("/", cap_std::ambient_authority())?; + let policy = ostree::SePolicy::new_at(container_root.as_raw_fd(), gio::Cancellable::NONE)?; + let label = require_label(&policy, "/usr/bin/ostree".into(), libc::S_IFREG | 0o755)?; + set_security_selinux(tmpf.as_fd(), label.as_bytes())?; let tmpf: Utf8PathBuf = tmpf.keep()?.1.try_into().unwrap(); - lsm_label(&tmpf, "/usr/bin/ostree".into(), false)?; tracing::debug!("Created {tmpf:?}"); let mut cmd = Command::new(&tmpf); @@ -142,28 +149,8 @@ pub(crate) fn selinux_set_permissive(permissive: bool) -> Result<()> { Ok(()) } -fn selinux_label_for_path(target: &str) -> Result { - // TODO: detect case where SELinux isn't enabled - let label = Task::new_quiet("matchpathcon") - .args(["-n", target]) - .read()?; - // TODO: trim in place instead of reallocating - Ok(label.trim().to_string()) -} - -// Write filesystem labels (currently just for SELinux) -#[context("Labeling {as_path}")] -pub(crate) fn lsm_label(target: &Utf8Path, as_path: &Utf8Path, recurse: bool) -> Result<()> { - let label = selinux_label_for_path(as_path.as_str())?; - tracing::debug!("Label for {as_path} (target={target}) is {label}"); - Task::new_quiet("chcon") - .arg("-h") - .args(recurse.then_some("-R")) - .args(["-h", label.as_str(), target.as_str()]) - .run() -} - #[cfg(feature = "install")] +/// Check if the ostree-formatted extended attributes include a security.selinux value. pub(crate) fn xattrs_have_selinux(xattrs: &ostree::glib::Variant) -> bool { let v = xattrs.data_as_bytes(); let v = v.try_as_aligned().unwrap(); @@ -176,3 +163,197 @@ pub(crate) fn xattrs_have_selinux(xattrs: &ostree::glib::Variant) -> bool { } false } + +/// Look up the label for a path in a policy, and error if one is not found. +pub(crate) fn require_label( + policy: &ostree::SePolicy, + destname: &Utf8Path, + mode: u32, +) -> Result { + policy + .label(destname.as_str(), mode, ostree::gio::Cancellable::NONE)? + .ok_or_else(|| { + anyhow::anyhow!( + "No label found in policy '{}' for {destname})", + policy.name() + ) + }) +} + +/// A thin wrapper for invoking fsetxattr(security.selinux) +pub(crate) fn set_security_selinux(fd: std::os::fd::BorrowedFd, label: &[u8]) -> Result<()> { + rustix::fs::fsetxattr( + fd, + "security.selinux", + label, + rustix::fs::XattrFlags::empty(), + ) + .context("fsetxattr(security.selinux)") +} + +pub(crate) fn has_security_selinux(root: &Dir, path: &Utf8Path) -> Result { + // TODO: avoid hardcoding a max size here + let mut buf = [0u8; 2048]; + let fdpath = format!("/proc/self/fd/{}/", root.as_raw_fd()); + let fdpath = &Path::new(&fdpath).join(path); + match rustix::fs::lgetxattr(fdpath, "security.selinux", &mut buf) { + Ok(_) => Ok(true), + Err(rustix::io::Errno::NODATA) => Ok(false), + Err(e) => Err(e).with_context(|| format!("Failed to look up context for {path:?}")), + } +} + +pub(crate) fn set_security_selinux_path(root: &Dir, path: &Utf8Path, label: &[u8]) -> Result<()> { + // TODO: avoid hardcoding a max size here + let fdpath = format!("/proc/self/fd/{}/", root.as_raw_fd()); + let fdpath = &Path::new(&fdpath).join(path); + rustix::fs::lsetxattr( + fdpath, + "security.selinux", + label, + rustix::fs::XattrFlags::empty(), + )?; + Ok(()) +} + +/// A wrapper for creating a directory, also optionally setting a SELinux label. +/// The provided `skip` parameter is a device/inode that we will ignore (and not traverse). +#[cfg(feature = "install")] +pub(crate) fn ensure_dir_labeled_recurse( + root: &Dir, + path: &mut Utf8PathBuf, + policy: &ostree::SePolicy, + skip: Option<(libc::dev_t, libc::ino64_t)>, +) -> Result<()> { + use cap_std_ext::cap_std::fs::MetadataExt; + + // Juggle the cap-std requirement for relative paths vs the libselinux + // requirement for absolute paths by special casing the empty string "" as "." + // just for the initial directory enumeration. + let path_for_readdir = if path.as_str().is_empty() { + Utf8Path::new(".") + } else { + &*path + }; + + let mut n = 0u64; + for ent in root.read_dir(path_for_readdir)? { + let ent = ent?; + let metadata = ent.metadata()?; + if let Some((skip_dev, skip_ino)) = skip.as_ref().copied() { + if (metadata.dev(), metadata.ino()) == (skip_dev, skip_ino) { + tracing::debug!("Skipping {path}"); + continue; + } + } + let name = ent.file_name(); + let name = name + .to_str() + .ok_or_else(|| anyhow::anyhow!("Invalid non-UTF-8 filename: {name:?}"))?; + path.push(name); + + if !has_security_selinux(root, path)? { + let abspath = Utf8Path::new("/").join(&path); + let label = require_label(policy, &abspath, metadata.mode())?; + tracing::trace!("Setting label for {path} to {label}"); + set_security_selinux_path(root, &path, label.as_bytes())?; + n += 1; + } + + if metadata.is_dir() { + ensure_dir_labeled_recurse(root, path, policy, skip)?; + } + path.pop(); + } + + if n > 0 { + tracing::debug!("Relabeled {n} objects in {path}"); + } + Ok(()) +} + +/// A wrapper for creating a directory, also optionally setting a SELinux label. +#[cfg(feature = "install")] +pub(crate) fn ensure_dir_labeled( + root: &Dir, + destname: impl AsRef, + mode: rustix::fs::Mode, + policy: Option<&ostree::SePolicy>, +) -> Result<()> { + let destname = destname.as_ref(); + // Special case the empty string + let local_destname = if destname.as_str().is_empty() { + ".".into() + } else { + destname + }; + tracing::debug!("Labeling {local_destname}"); + let label = policy + .map(|policy| { + // Also special case the absolute root + let abs_destname = if destname.as_str().is_empty() { + "/".into() + } else { + Utf8Path::new("/").join(destname) + }; + require_label(policy, &abs_destname, libc::S_IFDIR | mode.as_raw_mode()) + }) + .transpose() + .with_context(|| format!("Labeling {local_destname}"))?; + tracing::trace!("Label for {local_destname} is {label:?}"); + + root.ensure_dir_with(local_destname, &DirBuilder::new()) + .with_context(|| format!("Opening {local_destname}"))?; + let dirfd = cap_std_ext::cap_primitives::fs::open( + &root.as_filelike_view(), + local_destname.as_std_path(), + OpenOptions::new().read(true), + ) + .context("opendir")?; + let dirfd = dirfd.as_fd(); + rustix::fs::fchmod(dirfd, mode).context("fchmod")?; + if let Some(label) = label { + set_security_selinux(dirfd, label.as_bytes())?; + } + + Ok(()) +} + +/// A wrapper for atomically writing a file, also optionally setting a SELinux label. +#[cfg(feature = "install")] +pub(crate) fn atomic_replace_labeled( + root: &Dir, + destname: impl AsRef, + mode: rustix::fs::Mode, + policy: Option<&ostree::SePolicy>, + f: F, +) -> Result<()> +where + F: FnOnce(&mut std::io::BufWriter) -> Result<()>, +{ + let destname = destname.as_ref(); + let label = policy + .map(|policy| { + let abs_destname = Utf8Path::new("/").join(destname); + require_label(policy, &abs_destname, libc::S_IFREG | mode.as_raw_mode()) + }) + .transpose()?; + + root.atomic_replace_with(destname, |w| { + // Peel through the bufwriter to get the fd + let fd = w.get_mut(); + let fd = fd.as_file_mut(); + let fd = fd.as_fd(); + // Apply the target mode bits + rustix::fs::fchmod(fd, mode).context("fchmod")?; + // If we have a label, apply it + if let Some(label) = label { + tracing::debug!("Setting label for {destname} to {label}"); + set_security_selinux(fd, label.as_bytes())?; + } else { + tracing::debug!("No label for {destname}"); + } + // Finally call the underlying writer function + f(w) + }) +} diff --git a/lib/src/privtests.rs b/lib/src/privtests.rs index a3a6192c..132807a5 100644 --- a/lib/src/privtests.rs +++ b/lib/src/privtests.rs @@ -1,7 +1,11 @@ +use std::os::fd::AsRawFd; +use std::path::{Path, PathBuf}; use std::process::Command; use anyhow::{Context, Result}; use camino::Utf8Path; +use cap_std_ext::cap_std; +use cap_std_ext::cap_std::fs::Dir; use fn_error_context::context; use rustix::fd::AsFd; use xshell::{cmd, Shell}; @@ -162,6 +166,35 @@ fn test_install_filesystem(image: &str, blockdev: &Utf8Path) -> Result<()> { Ok(()) } +fn verify_selinux_label_exists(root: &Dir, path: &Path, warn: bool) -> Result<()> { + let mut buf = [0u8; 1024]; + let fdpath = format!("/proc/self/fd/{}/", root.as_raw_fd()); + let fdpath = &Path::new(&fdpath).join(path); + match rustix::fs::lgetxattr(fdpath, "security.selinux", &mut buf) { + Ok(_) => Ok(()), + Err(rustix::io::Errno::NODATA) if warn => { + eprintln!("No SELinux label found for: {path:?}"); + Ok(()) + } + Err(e) => Err(e).with_context(|| format!("Failed to look up context for {path:?}")), + } +} + +fn verify_selinux_recurse(root: &Dir, path: &mut PathBuf, warn: bool) -> Result<()> { + for ent in root.read_dir(&path)? { + let ent = ent?; + let name = ent.file_name(); + path.push(name); + verify_selinux_label_exists(root, &path, warn)?; + let file_type = ent.file_type()?; + if file_type.is_dir() { + verify_selinux_recurse(root, path, warn)?; + } + path.pop(); + } + Ok(()) +} + pub(crate) async fn run(opts: TestingOpts) -> Result<()> { match opts { TestingOpts::RunPrivilegedIntegration {} => { @@ -179,5 +212,13 @@ pub(crate) async fn run(opts: TestingOpts) -> Result<()> { crate::cli::ensure_self_unshared_mount_namespace().await?; tokio::task::spawn_blocking(move || test_install_filesystem(&image, &blockdev)).await? } + // This one is currently executed mainly from Github Actions + TestingOpts::VerifySELinux { root, warn } => { + let rootfs = cap_std::fs::Dir::open_ambient_dir(root, cap_std::ambient_authority()) + .context("Opening dir")?; + let mut path = PathBuf::from("."); + tokio::task::spawn_blocking(move || verify_selinux_recurse(&rootfs, &mut path, warn)) + .await? + } } }