Add SMACK support to id, mkdir, mkfifo, mknod utilities (#9910)

* Add SMACK support to id, mkdir, mkfifo, mknod utilities

* Address review comments: refactor SMACK helpers and rename SELinux flags

* Use official Arch Linux download URL for SMACK CI

* smack: refactor to single set_smack_label_and_cleanup function
This commit is contained in:
Sylvestre Ledru
2026-01-07 17:03:28 +01:00
committed by GitHub
14 changed files with 197 additions and 69 deletions
+7 -1
View File
@@ -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
+1
View File
@@ -29,3 +29,4 @@ path = "src/main.rs"
[features]
feat_selinux = ["selinux"]
smack = ["uucore/smack"]
+1 -1
View File
@@ -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 }
+1 -1
View File
@@ -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 }
+52 -28
View File
@@ -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<Ids>,
// 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")))]
+1
View File
@@ -24,6 +24,7 @@ fluent = { workspace = true }
[features]
selinux = ["uucore/selinux"]
smack = ["uucore/smack"]
[[bin]]
name = "mkdir"
+14 -7
View File
@@ -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::<String>(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(())
}
+1
View File
@@ -25,6 +25,7 @@ fluent = { workspace = true }
[features]
selinux = ["uucore/selinux"]
smack = ["uucore/smack"]
[[bin]]
name = "mkfifo"
+16 -4
View File
@@ -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::<String>(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::<String>(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),
+1
View File
@@ -26,6 +26,7 @@ fluent = { workspace = true }
[features]
selinux = ["uucore/selinux"]
smack = ["uucore/smack"]
[[bin]]
name = "mknod"
+22 -9
View File
@@ -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::<String>("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::<String>(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),
+5
View File
@@ -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
+46 -2
View File
@@ -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<String, SmackError> {
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<String, SmackError> {
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<Path>,
context: Option<&String>,
cleanup: impl FnOnce(&Path) -> io::Result<()>,
) -> Result<(), Box<dyn UError>> {
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())
})
}
+29 -16
View File
@@ -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")