From 2362ace0cf8452a4d42989c73941315d1b5b0d9a Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Sun, 17 Mar 2024 14:17:15 -0400 Subject: [PATCH] Rework SELinux labeling more First, in the install code, acquire a proper policy object. Add helpers for writing files/directories that take a policy object and operate *solely* using fd-relative operations and don't fork off helper processes. This is a notable cleanup because we don't need to juggle absolute file paths *and* fds, which avoids a lot of confusion. Our usage of a wrapper for the cap-std-ext atomic write API for generating files ensures that if the file is present, it will always have the correct label without any race conditions. Change the one place we now call `chcon` as a helper process to be an explicit recursive selinux relabeling. In the future we should switch to using a direct API instead of forking off `/usr/bin/chcon` - then everything would be fd-relative. Signed-off-by: Colin Walters --- .github/workflows/ci.yml | 7 +- lib/src/cli.rs | 6 + lib/src/install.rs | 78 ++++++------ lib/src/install/baseline.rs | 14 ++- lib/src/install/osconfig.rs | 65 +++------- lib/src/lsm.rs | 231 ++++++++++++++++++++++++++++++++---- lib/src/privtests.rs | 41 +++++++ 7 files changed, 328 insertions(+), 114 deletions(-) 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? + } } }