diff --git a/Cargo.toml b/Cargo.toml index 6a5796a46c6..6b771590380 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,7 +68,13 @@ feat_selinux = [ # "feat_smack" == enable support for SMACK Security Context (by using `--features feat_smack`) # NOTE: # * Running a uutils compiled with `feat_smack` requires a SMACK enabled Kernel at run time. -feat_smack = ["ls/smack"] +feat_smack = [ + "id/smack", + "ls/smack", + "mkdir/smack", + "mkfifo/smack", + "mknod/smack", +] ## ## feature sets ## (common/core and Tier1) feature sets diff --git a/src/uu/id/Cargo.toml b/src/uu/id/Cargo.toml index 9b947d956b6..de1752df1b3 100644 --- a/src/uu/id/Cargo.toml +++ b/src/uu/id/Cargo.toml @@ -29,3 +29,4 @@ path = "src/main.rs" [features] feat_selinux = ["selinux"] +smack = ["uucore/smack"] diff --git a/src/uu/id/locales/en-US.ftl b/src/uu/id/locales/en-US.ftl index 49264b30ed6..b9a93de0125 100644 --- a/src/uu/id/locales/en-US.ftl +++ b/src/uu/id/locales/en-US.ftl @@ -18,7 +18,7 @@ id-error-names-real-ids-require-flags = printing only names or real IDs requires id-error-zero-not-permitted-default = option --zero not permitted in default format id-error-cannot-print-context-with-user = cannot print security context when user specified id-error-cannot-get-context = can't get process context -id-error-context-selinux-only = --context (-Z) works only on an SELinux-enabled kernel +id-error-context-security-only = --context (-Z) works only on an SELinux/SMACK-enabled kernel id-error-no-such-user = { $user }: no such user id-error-cannot-find-group-name = cannot find name for group ID { $gid } id-error-cannot-find-user-name = cannot find name for user ID { $uid } diff --git a/src/uu/id/locales/fr-FR.ftl b/src/uu/id/locales/fr-FR.ftl index 2e799ae37bc..0cf8cd7588b 100644 --- a/src/uu/id/locales/fr-FR.ftl +++ b/src/uu/id/locales/fr-FR.ftl @@ -18,7 +18,7 @@ id-error-names-real-ids-require-flags = l'affichage des noms uniquement ou des I id-error-zero-not-permitted-default = l'option --zero n'est pas autorisée dans le format par défaut id-error-cannot-print-context-with-user = impossible d'afficher le contexte de sécurité quand un utilisateur est spécifié id-error-cannot-get-context = impossible d'obtenir le contexte du processus -id-error-context-selinux-only = --context (-Z) ne fonctionne que sur un noyau avec SELinux activé +id-error-context-security-only = --context (-Z) ne fonctionne que sur un noyau avec SELinux/SMACK activé id-error-no-such-user = { $user } : utilisateur inexistant id-error-cannot-find-group-name = impossible de trouver le nom pour l'ID de groupe { $gid } id-error-cannot-find-user-name = impossible de trouver le nom pour l'ID utilisateur { $uid } diff --git a/src/uu/id/src/id.rs b/src/uu/id/src/id.rs index 9ff314f626e..e6ab3a696cd 100644 --- a/src/uu/id/src/id.rs +++ b/src/uu/id/src/id.rs @@ -62,9 +62,9 @@ macro_rules! cstr2cow { } fn get_context_help_text() -> String { - #[cfg(not(feature = "selinux"))] + #[cfg(not(any(feature = "selinux", feature = "smack")))] return translate!("id-context-help-disabled"); - #[cfg(feature = "selinux")] + #[cfg(any(feature = "selinux", feature = "smack"))] return translate!("id-context-help-enabled"); } @@ -98,7 +98,10 @@ struct State { rflag: bool, // --real zflag: bool, // --zero cflag: bool, // --context + #[cfg(feature = "selinux")] selinux_supported: bool, + #[cfg(feature = "smack")] + smack_supported: bool, ids: Option, // The behavior for calling GNU's `id` and calling GNU's `id $USER` is similar but different. // * The SELinux context is only displayed without a specified user. @@ -136,16 +139,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { zflag: matches.get_flag(options::OPT_ZERO), cflag: matches.get_flag(options::OPT_CONTEXT), - selinux_supported: { - #[cfg(feature = "selinux")] - { - uucore::selinux::is_selinux_enabled() - } - #[cfg(not(feature = "selinux"))] - { - false - } - }, + #[cfg(feature = "selinux")] + selinux_supported: uucore::selinux::is_selinux_enabled(), + #[cfg(feature = "smack")] + smack_supported: uucore::smack::is_smack_enabled(), user_specified: !users.is_empty(), ids: None, }; @@ -179,26 +176,42 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let line_ending = LineEnding::from_zero_flag(state.zflag); if state.cflag { - return if state.selinux_supported { - // print SElinux context and exit - #[cfg(all(any(target_os = "linux", target_os = "android"), feature = "selinux"))] + // SELinux context + #[cfg(feature = "selinux")] + if state.selinux_supported { if let Ok(context) = selinux::SecurityContext::current(false) { let bytes = context.as_bytes(); print!("{}{line_ending}", String::from_utf8_lossy(bytes)); - } else { - // print error because `cflag` was explicitly requested - return Err(USimpleError::new( - 1, - translate!("id-error-cannot-get-context"), - )); + return Ok(()); } - Ok(()) - } else { - Err(USimpleError::new( + return Err(USimpleError::new( 1, - translate!("id-error-context-selinux-only"), - )) - }; + translate!("id-error-cannot-get-context"), + )); + } + + // SMACK label + #[cfg(feature = "smack")] + if state.smack_supported { + match uucore::smack::get_smack_label_for_self() { + Ok(label) => { + print!("{label}{line_ending}"); + return Ok(()); + } + Err(_) => { + return Err(USimpleError::new( + 1, + translate!("id-error-cannot-get-context"), + )); + } + } + } + + // Neither SELinux nor SMACK supported + return Err(USimpleError::new( + 1, + translate!("id-error-context-security-only"), + )); } for i in 0..=users.len() { @@ -666,7 +679,7 @@ fn id_print(state: &State, groups: &[u32]) { .join(",") ); - #[cfg(all(any(target_os = "linux", target_os = "android"), feature = "selinux"))] + #[cfg(feature = "selinux")] if state.selinux_supported && !state.user_specified && std::env::var_os("POSIXLY_CORRECT").is_none() @@ -677,6 +690,17 @@ fn id_print(state: &State, groups: &[u32]) { print!(" context={}", String::from_utf8_lossy(bytes)); } } + + #[cfg(feature = "smack")] + if state.smack_supported + && !state.user_specified + && std::env::var_os("POSIXLY_CORRECT").is_none() + { + // print SMACK label (does not depend on "-Z") + if let Ok(label) = uucore::smack::get_smack_label_for_self() { + print!(" context={label}"); + } + } } #[cfg(not(any(target_os = "linux", target_os = "android", target_os = "openbsd")))] diff --git a/src/uu/mkdir/Cargo.toml b/src/uu/mkdir/Cargo.toml index 7d81094cb06..b2723e1cfbc 100644 --- a/src/uu/mkdir/Cargo.toml +++ b/src/uu/mkdir/Cargo.toml @@ -24,6 +24,7 @@ fluent = { workspace = true } [features] selinux = ["uucore/selinux"] +smack = ["uucore/smack"] [[bin]] name = "mkdir" diff --git a/src/uu/mkdir/src/mkdir.rs b/src/uu/mkdir/src/mkdir.rs index 76262f4bdbc..88a196ebb1d 100644 --- a/src/uu/mkdir/src/mkdir.rs +++ b/src/uu/mkdir/src/mkdir.rs @@ -27,7 +27,7 @@ mod options { pub const PARENTS: &str = "parents"; pub const VERBOSE: &str = "verbose"; pub const DIRS: &str = "dirs"; - pub const SELINUX: &str = "z"; + pub const SECURITY_CONTEXT: &str = "z"; pub const CONTEXT: &str = "context"; } @@ -42,8 +42,8 @@ pub struct Config<'a> { /// Print message for each created directory. pub verbose: bool, - /// Set `SELinux` security context. - pub set_selinux_context: bool, + /// Set security context (SELinux/SMACK). + pub set_security_context: bool, /// Specific `SELinux` context. pub context: Option<&'a String>, @@ -79,7 +79,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let recursive = matches.get_flag(options::PARENTS); // Extract the SELinux related flags and options - let set_selinux_context = matches.get_flag(options::SELINUX); + let set_security_context = matches.get_flag(options::SECURITY_CONTEXT); let context = matches.get_one::(options::CONTEXT); match get_mode(&matches) { @@ -88,7 +88,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { recursive, mode, verbose, - set_selinux_context: set_selinux_context || context.is_some(), + set_security_context: set_security_context || context.is_some(), context, }; exec(dirs, &config) @@ -129,7 +129,7 @@ pub fn uu_app() -> Command { .action(ArgAction::SetTrue), ) .arg( - Arg::new(options::SELINUX) + Arg::new(options::SECURITY_CONTEXT) .short('Z') .help(translate!("mkdir-help-selinux")) .action(ArgAction::SetTrue), @@ -292,7 +292,7 @@ fn create_single_dir(path: &Path, is_parent: bool, config: &Config) -> UResult<( // Apply SELinux context if requested #[cfg(feature = "selinux")] - if config.set_selinux_context && uucore::selinux::is_selinux_enabled() { + if config.set_security_context && uucore::selinux::is_selinux_enabled() { if let Err(e) = uucore::selinux::set_selinux_security_context(path, config.context) { let _ = std::fs::remove_dir(path); @@ -300,6 +300,13 @@ fn create_single_dir(path: &Path, is_parent: bool, config: &Config) -> UResult<( } } + // Apply SMACK context if requested + #[cfg(feature = "smack")] + if config.set_security_context { + uucore::smack::set_smack_label_and_cleanup(path, config.context, |p| { + std::fs::remove_dir(p) + })?; + } Ok(()) } diff --git a/src/uu/mkfifo/Cargo.toml b/src/uu/mkfifo/Cargo.toml index 5edbfa6bde1..ca0cc4dcbec 100644 --- a/src/uu/mkfifo/Cargo.toml +++ b/src/uu/mkfifo/Cargo.toml @@ -25,6 +25,7 @@ fluent = { workspace = true } [features] selinux = ["uucore/selinux"] +smack = ["uucore/smack"] [[bin]] name = "mkfifo" diff --git a/src/uu/mkfifo/src/mkfifo.rs b/src/uu/mkfifo/src/mkfifo.rs index c55593dcbca..225540873cf 100644 --- a/src/uu/mkfifo/src/mkfifo.rs +++ b/src/uu/mkfifo/src/mkfifo.rs @@ -16,7 +16,7 @@ use uucore::{format_usage, show}; mod options { pub static MODE: &str = "mode"; - pub static SELINUX: &str = "Z"; + pub static SECURITY_CONTEXT: &str = "Z"; pub static CONTEXT: &str = "context"; pub static FIFO: &str = "fifo"; } @@ -62,10 +62,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { #[cfg(feature = "selinux")] { // Extract the SELinux related flags and options - let set_selinux_context = matches.get_flag(options::SELINUX); + let set_security_context = matches.get_flag(options::SECURITY_CONTEXT); let context = matches.get_one::(options::CONTEXT); - if set_selinux_context || context.is_some() { + if set_security_context || context.is_some() { use std::path::Path; if let Err(e) = uucore::selinux::set_selinux_security_context(Path::new(&f), context) @@ -75,6 +75,18 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } } } + + // Apply SMACK context if requested + #[cfg(feature = "smack")] + { + let set_security_context = matches.get_flag(options::SECURITY_CONTEXT); + let context = matches.get_one::(options::CONTEXT); + if set_security_context || context.is_some() { + uucore::smack::set_smack_label_and_cleanup(&f, context, |p| { + std::fs::remove_file(p) + })?; + } + } } Ok(()) @@ -95,7 +107,7 @@ pub fn uu_app() -> Command { .value_name("MODE"), ) .arg( - Arg::new(options::SELINUX) + Arg::new(options::SECURITY_CONTEXT) .short('Z') .help(translate!("mkfifo-help-selinux")) .action(ArgAction::SetTrue), diff --git a/src/uu/mknod/Cargo.toml b/src/uu/mknod/Cargo.toml index 32b983134e3..a32aa3e9aa8 100644 --- a/src/uu/mknod/Cargo.toml +++ b/src/uu/mknod/Cargo.toml @@ -26,6 +26,7 @@ fluent = { workspace = true } [features] selinux = ["uucore/selinux"] +smack = ["uucore/smack"] [[bin]] name = "mknod" diff --git a/src/uu/mknod/src/mknod.rs b/src/uu/mknod/src/mknod.rs index 8a4cf82d01e..56474e4b665 100644 --- a/src/uu/mknod/src/mknod.rs +++ b/src/uu/mknod/src/mknod.rs @@ -23,7 +23,7 @@ mod options { pub const TYPE: &str = "type"; pub const MAJOR: &str = "major"; pub const MINOR: &str = "minor"; - pub const SELINUX: &str = "z"; + pub const SECURITY_CONTEXT: &str = "z"; pub const CONTEXT: &str = "context"; } @@ -54,10 +54,10 @@ pub struct Config<'a> { pub dev: dev_t, - /// Set `SELinux` security context. - pub set_selinux_context: bool, + /// Set security context (SELinux/SMACK). + pub set_security_context: bool, - /// Specific `SELinux` context. + /// Specific security context (SELinux/SMACK). pub context: Option<&'a String>, } @@ -88,7 +88,7 @@ fn mknod(file_name: &str, config: Config) -> i32 { // Apply SELinux context if requested #[cfg(feature = "selinux")] - if config.set_selinux_context { + if config.set_security_context { if let Err(e) = uucore::selinux::set_selinux_security_context( std::path::Path::new(file_name), config.context, @@ -100,6 +100,19 @@ fn mknod(file_name: &str, config: Config) -> i32 { } } + // Apply SMACK context if requested + #[cfg(feature = "smack")] + if config.set_security_context { + if let Err(e) = + uucore::smack::set_smack_label_and_cleanup(file_name, config.context, |p| { + std::fs::remove_file(p) + }) + { + eprintln!("{}: {}", uucore::util_name(), e); + return 1; + } + } + errno } } @@ -124,8 +137,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .get_one::("name") .expect("Missing argument 'NAME'"); - // Extract the SELinux related flags and options - let set_selinux_context = matches.get_flag(options::SELINUX); + // Extract the security context related flags and options + let set_security_context = matches.get_flag(options::SECURITY_CONTEXT); let context = matches.get_one::(options::CONTEXT); let dev = match ( @@ -153,7 +166,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { mode, use_umask, dev, - set_selinux_context: set_selinux_context || context.is_some(), + set_security_context: set_security_context || context.is_some(), context, }; @@ -204,7 +217,7 @@ pub fn uu_app() -> Command { .value_parser(value_parser!(u64)), ) .arg( - Arg::new(options::SELINUX) + Arg::new(options::SECURITY_CONTEXT) .short('Z') .help(translate!("mknod-help-selinux")) .action(ArgAction::SetTrue), diff --git a/src/uucore/locales/en-US.ftl b/src/uucore/locales/en-US.ftl index fa77f5270b3..36cd9d942a9 100644 --- a/src/uucore/locales/en-US.ftl +++ b/src/uucore/locales/en-US.ftl @@ -46,6 +46,11 @@ selinux-error-context-retrieval-failure = failed to retrieve the security contex selinux-error-context-set-failure = failed to set default file creation context to '{ $context }': { $error } selinux-error-context-conversion-failure = failed to set default file creation context to '{ $context }': { $error } +# SMACK error messages +smack-error-not-enabled = SMACK is not enabled on this system +smack-error-label-retrieval-failure = failed to get security context: { $error } +smack-error-label-set-failure = failed to set default file creation context to '{ $context }': { $error } +smack-error-no-label-set = no security context set # Safe traversal error messages safe-traversal-error-path-contains-null = path contains null byte diff --git a/src/uucore/src/lib/features/smack.rs b/src/uucore/src/lib/features/smack.rs index 2a0250da5dd..d901bde00be 100644 --- a/src/uucore/src/lib/features/smack.rs +++ b/src/uucore/src/lib/features/smack.rs @@ -6,13 +6,14 @@ // spell-checker:ignore smackfs //! SMACK (Simplified Mandatory Access Control Kernel) support -use std::io; +use std::fs; +use std::io::{self, Read, Write}; use std::path::Path; use std::sync::OnceLock; use thiserror::Error; -use crate::error::{UError, strip_errno}; +use crate::error::{UError, USimpleError, strip_errno}; use crate::translate; #[derive(Debug, Error)] @@ -50,6 +51,32 @@ pub fn is_smack_enabled() -> bool { *SMACK_ENABLED.get_or_init(|| Path::new("/sys/fs/smackfs").exists()) } +/// Gets the SMACK label for the current process. +pub fn get_smack_label_for_self() -> Result { + if !is_smack_enabled() { + return Err(SmackError::SmackNotEnabled); + } + + let mut label = String::new(); + fs::File::open("/proc/self/attr/current") + .map_err(SmackError::LabelRetrievalFailure)? + .read_to_string(&mut label) + .map_err(SmackError::LabelRetrievalFailure)?; + + Ok(label.trim().to_string()) +} + +/// Sets the SMACK label for the current process. +pub fn set_smack_label_for_self(label: &str) -> Result<(), SmackError> { + if !is_smack_enabled() { + return Err(SmackError::SmackNotEnabled); + } + + fs::File::create("/proc/self/attr/current") + .and_then(|mut f| f.write_all(label.as_bytes())) + .map_err(|e| SmackError::LabelSetFailure(label.to_string(), e)) +} + /// Gets the SMACK label for a filesystem path via xattr. pub fn get_smack_label_for_path(path: &Path) -> Result { if !is_smack_enabled() { @@ -75,3 +102,20 @@ pub fn set_smack_label_for_path(path: &Path, label: &str) -> Result<(), SmackErr xattr::set(path, "security.SMACK64", label.as_bytes()) .map_err(|e| SmackError::LabelSetFailure(label.to_string(), e)) } + +/// Sets SMACK label for a new path, calling cleanup on failure. +pub fn set_smack_label_and_cleanup( + path: impl AsRef, + context: Option<&String>, + cleanup: impl FnOnce(&Path) -> io::Result<()>, +) -> Result<(), Box> { + let Some(ctx) = context else { return Ok(()) }; + if !is_smack_enabled() { + return Ok(()); + } + let path = path.as_ref(); + set_smack_label_for_path(path, ctx).map_err(|e| { + let _ = cleanup(path); + USimpleError::new(1, e.to_string()) + }) +} diff --git a/util/run-gnu-tests-smack-ci.sh b/util/run-gnu-tests-smack-ci.sh index 37a4631a594..5dc47eb8738 100755 --- a/util/run-gnu-tests-smack-ci.sh +++ b/util/run-gnu-tests-smack-ci.sh @@ -1,7 +1,7 @@ #!/bin/bash # Run GNU SMACK tests in QEMU with SMACK-enabled kernel # Usage: run-gnu-tests-smack-ci.sh [GNU_DIR] [OUTPUT_DIR] -# spell-checker:ignore rootfs zstd unzstd cpio newc nographic smackfs devtmpfs tmpfs poweroff libm libgcc libpthread libdl librt sysfs rwxat +# spell-checker:ignore rootfs zstd unzstd cpio newc nographic smackfs devtmpfs tmpfs poweroff libm libgcc libpthread libdl librt sysfs rwxat setuidgid set -e SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" @@ -17,10 +17,7 @@ mkdir -p "$SMACK_DIR"/{rootfs/{bin,lib64,proc,sys,dev,tmp,etc,gnu},kernel} # Download Arch Linux kernel (has SMACK built-in) if [ ! -f /tmp/arch-vmlinuz ]; then echo "Downloading Arch Linux kernel..." - MIRROR="https://geo.mirror.pkgbuild.com/core/os/x86_64" - KERNEL_PKG=$(curl -sL "$MIRROR/" | grep -oP 'linux-[0-9][^"]*-x86_64\.pkg\.tar\.zst' | grep -v headers | sort -V | tail -1) - [ -z "$KERNEL_PKG" ] && { echo "Error: Could not find kernel package"; exit 1; } - curl -sL -o /tmp/arch-kernel.pkg.tar.zst "$MIRROR/$KERNEL_PKG" + curl -sL -o /tmp/arch-kernel.pkg.tar.zst "https://archlinux.org/packages/core/x86_64/linux/download/" zstd -d /tmp/arch-kernel.pkg.tar.zst -o /tmp/arch-kernel.pkg.tar 2>/dev/null || unzstd /tmp/arch-kernel.pkg.tar.zst -o /tmp/arch-kernel.pkg.tar VMLINUZ_PATH=$(tar -tf /tmp/arch-kernel.pkg.tar | grep 'vmlinuz$' | head -1) tar -xf /tmp/arch-kernel.pkg.tar -C /tmp "$VMLINUZ_PATH" @@ -65,17 +62,22 @@ ln -sf /proc/mounts /etc/mtab mkdir -p /tmp && mount -t tmpfs tmpfs /tmp chmod 1777 /tmp export PATH="/bin:$PATH" srcdir="/gnu" LD_LIBRARY_PATH="/lib64" -cd /gnu/tests -sh "$TEST_SCRIPT" +if [ -n "$RUN_AS_USER" ]; then + # Run in /tmp so non-root user can create temp directories + cd /tmp + setuidgid "$RUN_AS_USER" sh "/gnu/tests/$TEST_SCRIPT" +else + cd /gnu/tests + sh "$TEST_SCRIPT" +fi echo "EXIT:$?" poweroff -f INIT chmod +x "$SMACK_DIR/rootfs/init" -# Build utilities with SMACK support (only ls has SMACK support for now) -# TODO: When other utilities have SMACK support, build: ls id mkdir mknod mkfifo +# Build utilities with SMACK support echo "Building utilities with SMACK support..." -cargo build --release --manifest-path="$REPO_DIR/Cargo.toml" --package uu_ls --bin ls --features uu_ls/smack +cargo build --release --manifest-path="$REPO_DIR/Cargo.toml" --package uu_id --features uu_id/smack --package uu_ls --features uu_ls/smack --package uu_mkdir --features uu_mkdir/smack --package uu_mkfifo --features uu_mkfifo/smack --package uu_mknod --features uu_mknod/smack # Find SMACK tests SMACK_TESTS=$(grep -l 'require_smack_' -r "$GNU_DIR/tests/" 2>/dev/null || true) @@ -95,19 +97,30 @@ for TEST_PATH in $SMACK_TESTS; do echo "Running: $TEST_REL" + # Determine if test needs non-root user + RUN_AS_USER="" + if echo "$TEST_REL" | grep -q "no-root"; then + RUN_AS_USER="nobody" + fi + # Create working copy WORK="/tmp/smack-test-$$" rm -rf "$WORK" "$WORK.gz" cp -a "$SMACK_DIR/rootfs" "$WORK" - # Copy built utilities (only ls has SMACK support for now) - # TODO: When other utilities have SMACK support, use: - # for U in ls id mkdir mknod mkfifo; do cp "$REPO_DIR/target/release/$U" "$WORK/bin/$U"; done - rm -f "$WORK/bin/ls" - cp "$REPO_DIR/target/release/ls" "$WORK/bin/ls" + # Copy built utilities with SMACK support + for U in id ls mkdir mkfifo mknod; do + rm -f "$WORK/bin/$U" + cp "$REPO_DIR/target/release/$U" "$WORK/bin/$U" + done - # Set test script path + # Set test script path and user sed -i "s|\$TEST_SCRIPT|$TEST_REL|g" "$WORK/init" + if [ -n "$RUN_AS_USER" ]; then + sed -i "s|\$RUN_AS_USER|$RUN_AS_USER|g" "$WORK/init" + else + sed -i "s|\$RUN_AS_USER||g" "$WORK/init" + fi # Build initramfs and run (cd "$WORK" && find . | cpio -o -H newc 2>/dev/null | gzip > "$WORK.gz")