Skip to content
Open
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
1 change: 1 addition & 0 deletions .vscode/cspell.dictionaries/jargon.wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -219,3 +219,4 @@ VMULL
vmull
SETFL
tmpfs
ENOTSUP
1 change: 1 addition & 0 deletions src/uu/cp/locales/en-US.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ 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-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
cp-error-cannot-create-fifo = cannot create fifo { $path }: File exists
cp-error-invalid-attribute = invalid attribute { $value }
cp-error-failed-to-create-whole-tree = failed to create whole tree
Expand Down
1 change: 1 addition & 0 deletions src/uu/cp/locales/fr-FR.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ cp-error-selinux-not-enabled = SELinux n'était pas activé lors de la compilati
cp-error-selinux-set-context = échec de la définition du contexte de sécurité de { $path } : { $error }
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
cp-error-cannot-create-fifo = impossible de créer le fifo { $path } : Le fichier existe
cp-error-invalid-attribute = attribut invalide { $value }
cp-error-failed-to-create-whole-tree = échec de la création de l'arborescence complète
Expand Down
23 changes: 21 additions & 2 deletions src/uu/cp/src/copydir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ use uucore::translate;
use uucore::uio_error;
use walkdir::{DirEntry, WalkDir};

#[cfg(all(feature = "selinux", target_os = "linux"))]
use crate::set_selinux_context;
use crate::{
CopyMode, CopyResult, CpError, Options, aligned_ancestors, context_for, copy_attributes,
copy_file,
Expand Down Expand Up @@ -475,6 +477,7 @@ pub(crate) fn copy_directory(
&entry.source_absolute,
&entry.local_to_target,
&options.attributes,
options.set_selinux_context,
)?;
continue;
}
Expand Down Expand Up @@ -513,6 +516,7 @@ pub(crate) fn copy_directory(
&entry.source_absolute,
&entry.local_to_target,
&options.attributes,
options.set_selinux_context,
)?;
}
}
Expand All @@ -529,7 +533,17 @@ pub(crate) fn copy_directory(
// Fix permissions for all directories we created
// This ensures that even sibling directories get their permissions fixed
for (source_path, dest_path) in dirs_needing_permissions {
copy_attributes(&source_path, &dest_path, &options.attributes)?;
copy_attributes(
&source_path,
&dest_path,
&options.attributes,
options.set_selinux_context,
)?;

#[cfg(all(feature = "selinux", target_os = "linux"))]
if options.set_selinux_context {
set_selinux_context(&dest_path, options.context.as_ref())?;
}
}

// Also fix permissions for parent directories,
Expand All @@ -538,7 +552,12 @@ pub(crate) fn copy_directory(
let dest = target.join(root.file_name().unwrap());
for (x, y) in aligned_ancestors(root, dest.as_path()) {
if let Ok(src) = canonicalize(x, MissingHandling::Normal, ResolveMode::Physical) {
copy_attributes(&src, y, &options.attributes)?;
copy_attributes(&src, y, &options.attributes, options.set_selinux_context)?;

#[cfg(all(feature = "selinux", target_os = "linux"))]
if options.set_selinux_context {
set_selinux_context(y, options.context.as_ref())?;
}
}
}
}
Expand Down
111 changes: 88 additions & 23 deletions src/uu/cp/src/cp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1159,6 +1159,21 @@ impl Options {
None
};

// -Z/--context conflicts with explicit --preserve=context but overrides implicit (from -a)
if set_selinux_context || context.is_some() {
match attributes.context {
Preserve::Yes { required: true } => {
return Err(CpError::Error(translate!(
"cp-error-selinux-context-conflict"
)));
}
Preserve::Yes { required: false } => {
attributes.context = Preserve::No { explicit: false };
}
Preserve::No { .. } => {}
}
}

let options = Self {
attributes_only: matches.get_flag(options::ATTRIBUTES_ONLY),
copy_contents: matches.get_flag(options::COPY_CONTENTS),
Expand Down Expand Up @@ -1532,7 +1547,7 @@ fn copy_source(
if options.parents {
for (x, y) in aligned_ancestors(source, dest.as_path()) {
if let Ok(src) = canonicalize(x, MissingHandling::Normal, ResolveMode::Physical) {
copy_attributes(&src, y, &options.attributes)?;
copy_attributes(&src, y, &options.attributes, options.set_selinux_context)?;
}
}
}
Expand Down Expand Up @@ -1627,30 +1642,40 @@ impl OverwriteMode {
}
}

/// Handles errors for attributes preservation. If the attribute is not required, and
/// errored, tries to show error (see `show_error_if_needed` for additional behavior details).
/// If it's required, then the error is thrown.
fn handle_preserve<F: Fn() -> CopyResult<()>>(p: &Preserve, f: F) -> CopyResult<()> {
match p {
Preserve::No { .. } => {}
Preserve::Yes { required } => {
let result = f();
if *required {
result?;
} else if let Err(error) = result {
show_error_if_needed(&error);
}
}
}
Ok(())
}

#[cfg(all(feature = "selinux", target_os = "linux"))]
pub(crate) fn set_selinux_context(path: &Path, context: Option<&String>) -> CopyResult<()> {
if !uucore::selinux::is_selinux_enabled() {
return Ok(());
}

match uucore::selinux::set_selinux_security_context(path, context) {
Ok(()) => Ok(()),
Err(uucore::selinux::SeLinuxError::OperationNotSupported) => Ok(()),
Err(e) => Err(CpError::Error(
translate!("cp-error-selinux-error", "error" => e),
)),
}
}

/// Copies extended attributes (xattrs) from `source` to `dest`, ensuring that `dest` is temporarily
/// user-writable if needed and restoring its original permissions afterward. This avoids "Operation
/// not permitted" errors on read-only files. Returns an error if permission or metadata operations fail,
/// or if xattr copying fails.
#[cfg(all(unix, not(target_os = "android")))]
fn copy_extended_attrs(source: &Path, dest: &Path) -> CopyResult<()> {
fn copy_extended_attrs(source: &Path, dest: &Path, skip_selinux: bool) -> CopyResult<()> {
let metadata = fs::symlink_metadata(dest)?;

// Check if the destination file is currently read-only for the user.
Expand All @@ -1666,7 +1691,13 @@ fn copy_extended_attrs(source: &Path, dest: &Path) -> CopyResult<()> {

// Perform the xattr copy and capture any potential error,
// so we can restore permissions before returning.
let copy_xattrs_result = copy_xattrs(source, dest);
let copy_xattrs_result = if skip_selinux {
// When -Z is used, skip copying security.selinux xattr so that
// the default context can be set instead of preserving from source
copy_xattrs_skip_selinux(source, dest)
} else {
copy_xattrs(source, dest)
};

// Restore read-only if we changed it.
if was_readonly {
Expand All @@ -1681,11 +1712,30 @@ fn copy_extended_attrs(source: &Path, dest: &Path) -> CopyResult<()> {
Ok(())
}

/// Copy extended attributes but skip security.selinux
#[cfg(all(unix, not(target_os = "android")))]
fn copy_xattrs_skip_selinux(source: &Path, dest: &Path) -> std::io::Result<()> {
for attr_name in xattr::list(source)? {
// Skip security.selinux when -Z is used to set default context
if attr_name.to_string_lossy() == "security.selinux" {
continue;
}
if let Some(value) = xattr::get(source, &attr_name)? {
xattr::set(dest, &attr_name, &value)?;
}
}
Ok(())
}

/// Copy the specified attributes from one path to another.
/// If `skip_selinux_xattr` is true, the security.selinux xattr will not be copied
/// (used when -Z is specified to set the default context instead).
#[allow(unused_variables)]
pub(crate) fn copy_attributes(
source: &Path,
dest: &Path,
attributes: &Attributes,
skip_selinux_xattr: bool,
) -> CopyResult<()> {
let context = &*format!("{} -> {}", source.quote(), dest.quote());
let source_metadata =
Expand Down Expand Up @@ -1781,9 +1831,10 @@ pub(crate) fn copy_attributes(
handle_preserve(&attributes.xattr, || -> CopyResult<()> {
#[cfg(all(unix, not(target_os = "android")))]
{
copy_extended_attrs(source, dest)?;
copy_extended_attrs(source, dest, skip_selinux_xattr)?;
}
#[cfg(not(all(unix, not(target_os = "android"))))]
#[allow(unused_variables)]
{
// The documentation for GNU cp states:
//
Expand Down Expand Up @@ -2538,33 +2589,42 @@ fn copy_file(
fs::set_permissions(dest, dest_permissions).ok();
}

if options.dereference(source_in_command_line) {
let copy_attributes_result = if options.dereference(source_in_command_line) {
// Try to canonicalize, but if it fails (e.g., due to inaccessible parent directories),
// fall back to the original source path
let src_for_attrs = canonicalize(source, MissingHandling::Normal, ResolveMode::Physical)
.ok()
.filter(|p| p.exists())
.unwrap_or_else(|| source.to_path_buf());
copy_attributes(&src_for_attrs, dest, &options.attributes)?;
copy_attributes(
&src_for_attrs,
dest,
&options.attributes,
options.set_selinux_context,
)
} else if source_is_stream && !source.exists() {
// Some stream files may not exist after we have copied it,
// like anonymous pipes. Thus, we can't really copy its
// attributes. However, this is already handled in the stream
// copy function (see `copy_stream` under platform/linux.rs).
Ok(())
} else {
copy_attributes(source, dest, &options.attributes)?;
}
copy_attributes(
source,
dest,
&options.attributes,
options.set_selinux_context,
)
};

// GNU cp truncates the destination when a required attribute cannot be preserved
copy_attributes_result.inspect_err(|_| {
fs::File::create(dest).map(|f| f.set_len(0)).ok();
})?;

#[cfg(all(feature = "selinux", target_os = "linux"))]
if options.set_selinux_context && uucore::selinux::is_selinux_enabled() {
// Set the given selinux permissions on the copied file.
if let Err(e) =
uucore::selinux::set_selinux_security_context(dest, options.context.as_ref())
{
return Err(CpError::Error(
translate!("cp-error-selinux-error", "error" => e),
));
}
if options.set_selinux_context {
set_selinux_context(dest, options.context.as_ref())?;
}

// Skip tracking copied files when using --link mode since hard link
Expand Down Expand Up @@ -2734,7 +2794,12 @@ fn copy_link(
delete_path(dest, options)?;
}
symlink_file(&link, dest, symlinked_files)?;
copy_attributes(source, dest, &options.attributes)
copy_attributes(
source,
dest,
&options.attributes,
options.set_selinux_context,
)
}

/// Generate an error message if `target` is not the correct `target_type`
Expand Down
1 change: 1 addition & 0 deletions src/uucore/locales/en-US.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ selinux-error-file-open-failure = failed to open the file: { $error }
selinux-error-context-retrieval-failure = failed to retrieve the security context: { $error }
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 }
selinux-error-operation-not-supported = operation not supported

# SMACK error messages
smack-error-not-enabled = SMACK is not enabled on this system
Expand Down
1 change: 1 addition & 0 deletions src/uucore/locales/fr-FR.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ selinux-error-file-open-failure = échec de l'ouverture du fichier : { $error }
selinux-error-context-retrieval-failure = échec de la récupération du contexte de sécurité : { $error }
selinux-error-context-set-failure = échec de la définition du contexte de création de fichier par défaut à '{ $context }' : { $error }
selinux-error-context-conversion-failure = échec de la définition du contexte de création de fichier par défaut à '{ $context }' : { $error }
selinux-error-operation-not-supported = opération non prise en charge

# Messages d'erreur de traversée sécurisée
safe-traversal-error-path-contains-null = le chemin contient un octet null
Expand Down
26 changes: 22 additions & 4 deletions src/uucore/src/lib/features/selinux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ pub enum SeLinuxError {

#[error("{}", translate!("selinux-error-context-conversion-failure", "context" => .0.clone(), "error" => .1.clone()))]
ContextConversionFailure(String, String),

#[error("{}", translate!("selinux-error-operation-not-supported"))]
OperationNotSupported,
}

impl UError for SeLinuxError {
Expand All @@ -40,6 +43,7 @@ impl UError for SeLinuxError {
Self::ContextRetrievalFailure(_) => 3,
Self::ContextSetFailure(_, _) => 4,
Self::ContextConversionFailure(_, _) => 5,
Self::OperationNotSupported => 6,
}
}
}
Expand Down Expand Up @@ -154,13 +158,23 @@ pub fn set_selinux_security_context(
false,
)
.set_for_path(path, false, false)
.map_err(|e| {
SeLinuxError::ContextSetFailure(ctx_str.to_owned(), selinux_error_description(&e))
.map_err(|e| match &e {
selinux::errors::Error::IO1Path { source, .. }
if source.raw_os_error() == Some(libc::ENOTSUP) =>
{
SeLinuxError::OperationNotSupported
}
_ => SeLinuxError::ContextSetFailure(ctx_str.to_owned(), selinux_error_description(&e)),
})
} else {
// If no context provided, set the default SELinux context for the path
SecurityContext::set_default_for_path(path).map_err(|e| {
SeLinuxError::ContextSetFailure(String::new(), selinux_error_description(&e))
SecurityContext::set_default_for_path(path).map_err(|e| match &e {
selinux::errors::Error::IO1Path { source, .. }
if source.raw_os_error() == Some(libc::ENOTSUP) =>
{
SeLinuxError::OperationNotSupported
}
_ => SeLinuxError::ContextSetFailure(String::new(), selinux_error_description(&e)),
})
}
}
Expand Down Expand Up @@ -205,6 +219,7 @@ pub fn set_selinux_security_context(
/// Err(SeLinuxError::ContextRetrievalFailure(e)) => println!("Failed to retrieve the security context: {e}"),
/// Err(SeLinuxError::ContextConversionFailure(ctx, e)) => println!("Failed to convert context '{ctx}': {e}"),
/// Err(SeLinuxError::ContextSetFailure(ctx, e)) => println!("Failed to set context '{ctx}': {e}"),
/// Err(SeLinuxError::OperationNotSupported) => println!("Operation not supported"),
/// }
/// ```
pub fn get_selinux_security_context(
Expand Down Expand Up @@ -532,6 +547,9 @@ mod tests {
"File open failure occurred despite file being created: {e}"
);
}
Err(e @ SeLinuxError::OperationNotSupported) => {
println!("{e}");
}
}
}

Expand Down
Loading
Loading