Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/uu/cp/locales/en-US.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ cp-error-reflink-not-supported = --reflink is only supported on linux and macOS
cp-error-sparse-not-supported = --sparse is only supported on linux
cp-error-not-a-directory = { $path } is not a directory
cp-error-selinux-not-enabled = SELinux was not enabled during the compile time!
cp-error-selinux-set-context = failed to set the security context of { $path }: { $error }
cp-error-selinux-set-context = failed to set the security context of { $path }
cp-error-selinux-get-context = failed to get security context of { $path }
cp-error-selinux-error = SELinux error: { $error }
cp-error-selinux-context-conflict = cannot combine --context (-Z) with --preserve=context
Expand Down
2 changes: 1 addition & 1 deletion src/uu/cp/locales/fr-FR.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ cp-error-reflink-not-supported = --reflink n'est pris en charge que sur linux et
cp-error-sparse-not-supported = --sparse n'est pris en charge que sur linux
cp-error-not-a-directory = { $path } n'est pas un répertoire
cp-error-selinux-not-enabled = SELinux n'était pas activé lors de la compilation !
cp-error-selinux-set-context = échec de la définition du contexte de sécurité de { $path } : { $error }
cp-error-selinux-set-context = échec de la définition du contexte de sécurité de { $path }
cp-error-selinux-get-context = échec de l'obtention du contexte de sécurité de { $path }
cp-error-selinux-error = Erreur SELinux : { $error }
cp-error-selinux-context-conflict = impossible de combiner --context (-Z) avec --preserve=context
Expand Down
18 changes: 15 additions & 3 deletions src/uu/cp/src/cp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1934,9 +1934,21 @@ pub(crate) fn copy_attributes(
CpError::Error(translate!("cp-error-selinux-get-context", "path" => source.quote()))
})?;
if let Some(context) = context {
context.set_for_path(dest, false, false).map_err(|e|CpError::Error(
translate!("cp-error-selinux-set-context", "path" => dest.quote(), "error" => e),
))?;
context.set_for_path(dest, false, false).map_err(|e| {
// Keep the errno: the ENOTSUP of a mount with a fixed context is
// what -a and --preserve=all have to stay quiet about.
let source = match e {
selinux::errors::Error::IO { source, .. }
| selinux::errors::Error::IO1Name { source, .. }
| selinux::errors::Error::IO1Path { source, .. }
| selinux::errors::Error::IO1Process { source, .. } => source,
e => io::Error::other(e),
};
CpError::IoErrContext(
source,
translate!("cp-error-selinux-set-context", "path" => dest.quote()),
)
})?;
}
Ok(())
})?;
Expand Down
59 changes: 58 additions & 1 deletion src/uucore/src/lib/features/selinux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// spell-checker:ignore defaultcon setfscreatecon
//! Set of functions to manage SELinux security contexts

use std::borrow::Cow;
use std::error::Error;
use std::marker::PhantomData;
use std::path::Path;
Expand Down Expand Up @@ -290,7 +291,8 @@ pub fn set_selinux_security_context(
})
} else {
// If no context provided, set the default SELinux context for the path
SecurityContext::set_default_for_path(path).map_err(|e| match &e {
let path = absolute_for_policy_lookup(path);
SecurityContext::set_default_for_path(&path).map_err(|e| match &e {
selinux::errors::Error::IO1Path { source, .. }
if source.raw_os_error() == Some(libc::ENOTSUP) =>
{
Expand All @@ -301,6 +303,35 @@ pub fn set_selinux_security_context(
}
}

/// Returns `path` as an absolute name, for the default context lookup.
///
/// The policy's `file_contexts` only ever holds absolute names, so a relative
/// one matches nothing -- and libselinux reports the miss as success, leaving
/// the object with the context it already carried.
fn absolute_for_policy_lookup(path: &Path) -> Cow<'_, Path> {
if path.is_absolute() {
return Cow::Borrowed(path);
}
// Resolve the directory only, so that `.` and `..` do not reach the policy
// patterns; the last component is labelled itself, symbolic link or not.
let resolved = match (path.parent(), path.file_name()) {
(Some(directory), Some(name)) => {
let directory = if directory.as_os_str().is_empty() {
Path::new(".")
} else {
directory
};
std::fs::canonicalize(directory).map(|directory| directory.join(name))
}
// No last component to keep: the name ends in `.` or `..`, which names
// a directory and never a symbolic link, so resolve the whole of it.
// A lexical absolute name would keep those components and match no
// pattern.
_ => std::fs::canonicalize(path),
};
resolved.map_or(Cow::Borrowed(path), Cow::Owned)
}

/// Gets the SELinux security context for the given filesystem path.
///
/// Retrieves the security context of the specified filesystem path if SELinux is enabled
Expand Down Expand Up @@ -532,6 +563,32 @@ mod tests {
use super::*;
use tempfile::NamedTempFile;

#[test]
fn test_absolute_for_policy_lookup() {
let directory = tempfile::tempdir().expect("Failed to create tempdir");
let directory = std::fs::canonicalize(directory.path()).expect("Failed to canonicalize");
std::fs::create_dir(directory.join("branch")).expect("Failed to create dir");
let previous = std::env::current_dir().expect("Failed to read the working directory");
std::env::set_current_dir(&directory).expect("Failed to change the working directory");

// An absolute name is handed to the policy untouched.
let untouched = Path::new("/opt/quokka");
assert_eq!(absolute_for_policy_lookup(untouched), untouched);

// A relative one becomes absolute, and `.`/`..` never reach the policy.
for (given, expected) in [
("quokka", directory.join("quokka")),
("./quokka", directory.join("quokka")),
("branch/../quokka", directory.join("quokka")),
("branch/..", directory.clone()),
(".", directory.clone()),
] {
assert_eq!(absolute_for_policy_lookup(Path::new(given)), expected);
}

std::env::set_current_dir(previous).expect("Failed to restore the working directory");
}

#[test]
fn test_selinux_context_setting() {
let tmpfile = NamedTempFile::new().expect("Failed to create tempfile");
Expand Down
44 changes: 44 additions & 0 deletions tests/by-util/test_cp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7320,6 +7320,50 @@ fn test_cp_selinux() {
}
}

#[test]
#[cfg(all(
feature = "feat_selinux",
any(target_os = "linux", target_os = "android")
))]
fn test_cp_selinux_default_context_relative_dest() {
// -Z labels the destination with the context the policy has for its path,
// and the policy only lists absolute paths: a relative destination used to
// match nothing and silently keep the context it already carried.
use std::path::Path;
use uucore::selinux::set_selinux_security_context;

let ts = TestScenario::new(util_name!());
let at = &ts.fixtures;
at.touch(TEST_HELLO_WORLD_SOURCE);

// A type the policy will not hand out for a path under the test directory,
// so that the comparison below still sees a difference when -Z leaves the
// relative destination alone. Usable even when mcstransd is not running.
let ctx = "root:object_r:etc_t:s0".to_string();
for dest in ["relative", "absolute"] {
at.touch(dest);
if set_selinux_security_context(Path::new(&at.plus_as_string(dest)), Some(&ctx)).is_err() {
return;
}
}

let absolute = at.plus_as_string("absolute");
ts.ucmd()
.args(&["-Z", TEST_HELLO_WORLD_SOURCE, &absolute])
.succeeds();
ts.ucmd()
.args(&["-Z", TEST_HELLO_WORLD_SOURCE, "relative"])
.succeeds();

// Compare the type only, the one field -Z is about.
let selinux_type = |context: &str| context.split(':').nth(2).unwrap_or("").to_string();
assert_eq!(
selinux_type(&get_getfattr_output(&at.plus_as_string("relative"))),
selinux_type(&get_getfattr_output(&absolute)),
"-Z gave a relative and an absolute destination different contexts"
);
}

#[test]
#[cfg(all(
feature = "feat_selinux",
Expand Down
42 changes: 32 additions & 10 deletions util/build-gnu.sh
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ done
# This ensures the correct path is used even if the repository was moved or rebuilt in a different location
sed -i "s/^[[:blank:]]*PATH=.*/ PATH='${UU_BUILD_DIR//\//\\/}\$(PATH_SEPARATOR)'\"\$\$PATH\" \\\/" tests/local.mk

# Use GNU nproc for *BSD and macOS
NPROC="$(command -v nproc||command -v gnproc)"

if test -f gnu-built; then
echo "GNU build already found. Skip"
echo "'rm -f $(pwd)/{gnu-built,src/getlimits}' to force the build"
Expand All @@ -132,12 +135,11 @@ else
# Use a better diff
sed -i 's|diff -c|diff -u|g' tests/Coreutils.pm

# Skip make if possible
# Use GNU nproc for *BSD and macOS
NPROC="$(command -v nproc||command -v gnproc)"
test "${SELINUX_ENABLED}" = 1 && touch src/getlimits # SELinux tests does not use it
test -f src/getlimits || make -j "$("${NPROC}")"
cp -f src/getlimits "${UU_BUILD_DIR}"
# Skip make if possible. The SELinux job does not build the GNU tree at all;
# the block after this "if" builds the one program its tests need.
if [ "${SELINUX_ENABLED}" != 1 ]; then
test -x src/getlimits || make -j "$("${NPROC}")"
fi

# Handle generated factor tests
t_first=00
Expand All @@ -161,15 +163,35 @@ else
touch gnu-built
fi

# Keep getlimits available on PATH for GNU shell and Perl tests even when
# reusing an existing GNU build directory.
test -f src/getlimits && cp -f src/getlimits "${UU_BUILD_DIR}"

# Keep Makefile.in newer than the local.mk files we just modified,
# and Makefile newer than Makefile.in, so make won't re-run
# automake or config.status and undo our edits.
touch Makefile.in Makefile

# The GNU shell and Perl tests call getlimits_, so getlimits has to be a real
# program on PATH, also when reusing an existing GNU build directory. An earlier
# version of this script left an empty, non-executable stub behind under
# SELINUX_ENABLED, hence the test for an executable rather than for a file.
# Build only that program, plus the generated sources that the "all" target
# would otherwise pull in, so the SELinux job still skips the rest of the tree.
# This has to come after the touch above: stripping the factor tests leaves
# tests/local.mk with a trailing backslash that automake rejects, so a make that
# still sees it as newer than Makefile.in dies in the remake rule.
if ! test -x src/getlimits; then
rm -f src/getlimits
printf 'built-sources: $(BUILT_SOURCES)\n' |
make -f Makefile -f - -j "$("${NPROC}")" built-sources || true
make -j "$("${NPROC}")" src/getlimits || true
fi
if test -x src/getlimits; then
# Remove the destination first: cp keeps the permissions of an existing
# file, so a leftover stub there would stay non-executable.
rm -f "${UU_BUILD_DIR}/getlimits"
cp -f src/getlimits "${UU_BUILD_DIR}"
else
echo "WARNING: could not build getlimits; tests calling getlimits_ will fail" >&2
fi

# Patch the Makefile PATH to point to uutils build dir instead of GNU src/
sed -i "s/^[[:blank:]]*PATH=.*/ PATH='${UU_BUILD_DIR//\//\\/}\$(PATH_SEPARATOR)'\"\$\$PATH\" \\\/" Makefile
# Prevent make check from rebuilding the GNU binaries over the uutils ones
Expand Down
Loading