Skip to content

Commit 102c94f

Browse files
ChrisDrydensylvestre
authored andcommitted
cp: fix SELinux context handling for cp-a-selinux GNU test
1 parent 9922135 commit 102c94f

9 files changed

Lines changed: 196 additions & 26 deletions

File tree

src/uu/cp/locales/en-US.ftl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ cp-error-selinux-not-enabled = SELinux was not enabled during the compile time!
8585
cp-error-selinux-set-context = failed to set the security context of { $path }: { $error }
8686
cp-error-selinux-get-context = failed to get security context of { $path }
8787
cp-error-selinux-error = SELinux error: { $error }
88+
cp-error-selinux-context-conflict = cannot combine --context (-Z) with --preserve=context
8889
cp-error-cannot-create-fifo = cannot create fifo { $path }: File exists
8990
cp-error-invalid-attribute = invalid attribute { $value }
9091
cp-error-failed-to-create-whole-tree = failed to create whole tree

src/uu/cp/locales/fr-FR.ftl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ cp-error-selinux-not-enabled = SELinux n'était pas activé lors de la compilati
8585
cp-error-selinux-set-context = échec de la définition du contexte de sécurité de { $path } : { $error }
8686
cp-error-selinux-get-context = échec de l'obtention du contexte de sécurité de { $path }
8787
cp-error-selinux-error = Erreur SELinux : { $error }
88+
cp-error-selinux-context-conflict = impossible de combiner --context (-Z) avec --preserve=context
8889
cp-error-cannot-create-fifo = impossible de créer le fifo { $path } : Le fichier existe
8990
cp-error-invalid-attribute = attribut invalide { $value }
9091
cp-error-failed-to-create-whole-tree = échec de la création de l'arborescence complète

src/uu/cp/src/copydir.rs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ use uucore::translate;
2626
use uucore::uio_error;
2727
use walkdir::{DirEntry, WalkDir};
2828

29+
#[cfg(all(feature = "selinux", target_os = "linux"))]
30+
use crate::set_selinux_context;
2931
use crate::{
3032
CopyMode, CopyResult, CpError, Options, aligned_ancestors, context_for, copy_attributes,
3133
copy_file,
@@ -492,6 +494,7 @@ pub(crate) fn copy_directory(
492494
&entry.local_to_target,
493495
&options.attributes,
494496
false,
497+
options.set_selinux_context,
495498
)?;
496499
continue;
497500
}
@@ -534,6 +537,7 @@ pub(crate) fn copy_directory(
534537
&entry.local_to_target,
535538
&options.attributes,
536539
false,
540+
options.set_selinux_context,
537541
)?;
538542
}
539543
}
@@ -550,7 +554,18 @@ pub(crate) fn copy_directory(
550554
// Fix permissions for all directories we created
551555
// This ensures that even sibling directories get their permissions fixed
552556
for dir in dirs_needing_permissions {
553-
copy_attributes(&dir.source, &dir.dest, &options.attributes, dir.was_created)?;
557+
copy_attributes(
558+
&dir.source,
559+
&dir.dest,
560+
&options.attributes,
561+
dir.was_created,
562+
options.set_selinux_context,
563+
)?;
564+
565+
#[cfg(all(feature = "selinux", target_os = "linux"))]
566+
if options.set_selinux_context {
567+
set_selinux_context(&dir.dest, options.context.as_ref())?;
568+
}
554569
}
555570

556571
// Also fix permissions for parent directories,
@@ -559,7 +574,12 @@ pub(crate) fn copy_directory(
559574
let dest = target.join(root.file_name().unwrap());
560575
for (x, y) in aligned_ancestors(root, dest.as_path()) {
561576
if let Ok(src) = canonicalize(x, MissingHandling::Normal, ResolveMode::Physical) {
562-
copy_attributes(&src, y, &options.attributes, false)?;
577+
copy_attributes(&src, y, &options.attributes, false, options.set_selinux_context)?;
578+
579+
#[cfg(all(feature = "selinux", target_os = "linux"))]
580+
if options.set_selinux_context {
581+
set_selinux_context(y, options.context.as_ref())?;
582+
}
563583
}
564584
}
565585
}

src/uu/cp/src/cp.rs

Lines changed: 92 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1159,6 +1159,21 @@ impl Options {
11591159
None
11601160
};
11611161

1162+
// -Z/--context conflicts with explicit --preserve=context but overrides implicit (from -a)
1163+
if set_selinux_context || context.is_some() {
1164+
match attributes.context {
1165+
Preserve::Yes { required: true } => {
1166+
return Err(CpError::Error(translate!(
1167+
"cp-error-selinux-context-conflict"
1168+
)));
1169+
}
1170+
Preserve::Yes { required: false } => {
1171+
attributes.context = Preserve::No { explicit: false };
1172+
}
1173+
Preserve::No { .. } => {}
1174+
}
1175+
}
1176+
11621177
let options = Self {
11631178
attributes_only: matches.get_flag(options::ATTRIBUTES_ONLY),
11641179
copy_contents: matches.get_flag(options::COPY_CONTENTS),
@@ -1550,7 +1565,7 @@ fn copy_source(
15501565
if options.parents {
15511566
for (x, y) in aligned_ancestors(source, dest.as_path()) {
15521567
if let Ok(src) = canonicalize(x, MissingHandling::Normal, ResolveMode::Physical) {
1553-
copy_attributes(&src, y, &options.attributes, false)?;
1568+
copy_attributes(&src, y, &options.attributes, false, options.set_selinux_context)?;
15541569
}
15551570
}
15561571
}
@@ -1671,12 +1686,27 @@ fn handle_preserve<F: Fn() -> CopyResult<()>>(p: Preserve, f: F) -> CopyResult<(
16711686
Ok(())
16721687
}
16731688

1689+
#[cfg(all(feature = "selinux", target_os = "linux"))]
1690+
pub(crate) fn set_selinux_context(path: &Path, context: Option<&String>) -> CopyResult<()> {
1691+
if !uucore::selinux::is_selinux_enabled() {
1692+
return Ok(());
1693+
}
1694+
1695+
match uucore::selinux::set_selinux_security_context(path, context) {
1696+
Ok(()) => Ok(()),
1697+
Err(uucore::selinux::SeLinuxError::OperationNotSupported) => Ok(()),
1698+
Err(e) => Err(CpError::Error(
1699+
translate!("cp-error-selinux-error", "error" => e),
1700+
)),
1701+
}
1702+
}
1703+
16741704
/// Copies extended attributes (xattrs) from `source` to `dest`, ensuring that `dest` is temporarily
16751705
/// user-writable if needed and restoring its original permissions afterward. This avoids "Operation
16761706
/// not permitted" errors on read-only files. Returns an error if permission or metadata operations fail,
16771707
/// or if xattr copying fails.
16781708
#[cfg(all(unix, not(target_os = "android")))]
1679-
fn copy_extended_attrs(source: &Path, dest: &Path) -> CopyResult<()> {
1709+
fn copy_extended_attrs(source: &Path, dest: &Path, skip_selinux: bool) -> CopyResult<()> {
16801710
let metadata = fs::symlink_metadata(dest)?;
16811711

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

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

16971733
// Restore read-only if we changed it.
16981734
if was_readonly {
@@ -1712,12 +1748,31 @@ fn copy_extended_attrs(source: &Path, dest: &Path) -> CopyResult<()> {
17121748
Ok(())
17131749
}
17141750

1751+
/// Copy extended attributes but skip security.selinux
1752+
#[cfg(all(unix, not(target_os = "android")))]
1753+
fn copy_xattrs_skip_selinux(source: &Path, dest: &Path) -> std::io::Result<()> {
1754+
for attr_name in xattr::list(source)? {
1755+
// Skip security.selinux when -Z is used to set default context
1756+
if attr_name.to_string_lossy() == "security.selinux" {
1757+
continue;
1758+
}
1759+
if let Some(value) = xattr::get(source, &attr_name)? {
1760+
xattr::set(dest, &attr_name, &value)?;
1761+
}
1762+
}
1763+
Ok(())
1764+
}
1765+
17151766
/// Copy the specified attributes from one path to another.
1767+
/// If `skip_selinux_xattr` is true, the security.selinux xattr will not be copied
1768+
/// (used when -Z is specified to set the default context instead).
1769+
#[allow(unused_variables)]
17161770
pub(crate) fn copy_attributes(
17171771
source: &Path,
17181772
dest: &Path,
17191773
attributes: &Attributes,
17201774
dest_is_freshly_created_dir: bool,
1775+
skip_selinux_xattr: bool,
17211776
) -> CopyResult<()> {
17221777
let context = &*format!("{} -> {}", source.quote(), dest.quote());
17231778
let source_metadata =
@@ -1822,9 +1877,10 @@ pub(crate) fn copy_attributes(
18221877
handle_preserve(attributes.xattr, || -> CopyResult<()> {
18231878
#[cfg(all(unix, not(target_os = "android")))]
18241879
{
1825-
copy_extended_attrs(source, dest)?;
1880+
copy_extended_attrs(source, dest, skip_selinux_xattr)?;
18261881
}
18271882
#[cfg(not(all(unix, not(target_os = "android"))))]
1883+
#[allow(unused_variables)]
18281884
{
18291885
// The documentation for GNU cp states:
18301886
//
@@ -2576,33 +2632,44 @@ fn copy_file(
25762632
fs::set_permissions(dest, dest_permissions).ok();
25772633
}
25782634

2579-
if options.dereference(source_in_command_line) {
2635+
let copy_attributes_result = if options.dereference(source_in_command_line) {
25802636
// Try to canonicalize, but if it fails (e.g., due to inaccessible parent directories),
25812637
// fall back to the original source path
25822638
let src_for_attrs = canonicalize(source, MissingHandling::Normal, ResolveMode::Physical)
25832639
.ok()
25842640
.filter(|p| p.exists())
25852641
.unwrap_or_else(|| source.to_path_buf());
2586-
copy_attributes(&src_for_attrs, dest, &options.attributes, false)?;
2642+
copy_attributes(
2643+
&src_for_attrs,
2644+
dest,
2645+
&options.attributes,
2646+
false,
2647+
options.set_selinux_context,
2648+
)
25872649
} else if source_is_stream && !source.exists() {
25882650
// Some stream files may not exist after we have copied it,
25892651
// like anonymous pipes. Thus, we can't really copy its
25902652
// attributes. However, this is already handled in the stream
25912653
// copy function (see `copy_stream` under platform/linux.rs).
2654+
Ok(())
25922655
} else {
2593-
copy_attributes(source, dest, &options.attributes, false)?;
2594-
}
2656+
copy_attributes(
2657+
source,
2658+
dest,
2659+
&options.attributes,
2660+
false,
2661+
options.set_selinux_context,
2662+
)
2663+
};
25952664

2596-
#[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))]
2597-
if options.set_selinux_context && uucore::selinux::is_selinux_enabled() {
2598-
// Set the given selinux permissions on the copied file.
2599-
if let Err(e) =
2600-
uucore::selinux::set_selinux_security_context(dest, options.context.as_ref())
2601-
{
2602-
return Err(CpError::Error(
2603-
translate!("cp-error-selinux-error", "error" => e),
2604-
));
2605-
}
2665+
// GNU cp truncates the destination when a required attribute cannot be preserved
2666+
copy_attributes_result.inspect_err(|_| {
2667+
fs::File::create(dest).map(|f| f.set_len(0)).ok();
2668+
})?;
2669+
2670+
#[cfg(all(feature = "selinux", target_os = "linux"))]
2671+
if options.set_selinux_context {
2672+
set_selinux_context(dest, options.context.as_ref())?;
26062673
}
26072674

26082675
// Skip tracking copied files when using --link mode since hard link
@@ -2772,7 +2839,13 @@ fn copy_link(
27722839
delete_path(dest, options)?;
27732840
}
27742841
symlink_file(&link, dest, symlinked_files)?;
2775-
copy_attributes(source, dest, &options.attributes, false)
2842+
copy_attributes(
2843+
source,
2844+
dest,
2845+
&options.attributes,
2846+
false,
2847+
options.set_selinux_context,
2848+
)
27762849
}
27772850

27782851
/// Generate an error message if `target` is not the correct `target_type`

src/uucore/locales/en-US.ftl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ selinux-error-file-open-failure = failed to open the file: { $error }
4545
selinux-error-context-retrieval-failure = failed to retrieve the security context: { $error }
4646
selinux-error-context-set-failure = failed to set default file creation context to '{ $context }': { $error }
4747
selinux-error-context-conversion-failure = failed to set default file creation context to '{ $context }': { $error }
48+
selinux-error-operation-not-supported = operation not supported
4849
4950
# SMACK error messages
5051
smack-error-not-enabled = SMACK is not enabled on this system

src/uucore/locales/fr-FR.ftl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ selinux-error-file-open-failure = échec de l'ouverture du fichier : { $error }
4545
selinux-error-context-retrieval-failure = échec de la récupération du contexte de sécurité : { $error }
4646
selinux-error-context-set-failure = échec de la définition du contexte de création de fichier par défaut à '{ $context }' : { $error }
4747
selinux-error-context-conversion-failure = échec de la définition du contexte de création de fichier par défaut à '{ $context }' : { $error }
48+
selinux-error-operation-not-supported = opération non prise en charge
4849
4950
# Messages d'erreur de traversée sécurisée
5051
safe-traversal-error-path-contains-null = le chemin contient un octet null

src/uucore/src/lib/features/selinux.rs

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ pub enum SeLinuxError {
3030

3131
#[error("{}", translate!("selinux-error-context-conversion-failure", "context" => .0.clone(), "error" => .1.clone()))]
3232
ContextConversionFailure(String, String),
33+
34+
#[error("{}", translate!("selinux-error-operation-not-supported"))]
35+
OperationNotSupported,
3336
}
3437

3538
impl UError for SeLinuxError {
@@ -40,6 +43,7 @@ impl UError for SeLinuxError {
4043
Self::ContextRetrievalFailure(_) => 3,
4144
Self::ContextSetFailure(_, _) => 4,
4245
Self::ContextConversionFailure(_, _) => 5,
46+
Self::OperationNotSupported => 6,
4347
}
4448
}
4549
}
@@ -154,13 +158,23 @@ pub fn set_selinux_security_context(
154158
false,
155159
)
156160
.set_for_path(path, false, false)
157-
.map_err(|e| {
158-
SeLinuxError::ContextSetFailure(ctx_str.to_owned(), selinux_error_description(&e))
161+
.map_err(|e| match &e {
162+
selinux::errors::Error::IO1Path { source, .. }
163+
if source.raw_os_error() == Some(libc::ENOTSUP) =>
164+
{
165+
SeLinuxError::OperationNotSupported
166+
}
167+
_ => SeLinuxError::ContextSetFailure(ctx_str.to_owned(), selinux_error_description(&e)),
159168
})
160169
} else {
161170
// If no context provided, set the default SELinux context for the path
162-
SecurityContext::set_default_for_path(path).map_err(|e| {
163-
SeLinuxError::ContextSetFailure(String::new(), selinux_error_description(&e))
171+
SecurityContext::set_default_for_path(path).map_err(|e| match &e {
172+
selinux::errors::Error::IO1Path { source, .. }
173+
if source.raw_os_error() == Some(libc::ENOTSUP) =>
174+
{
175+
SeLinuxError::OperationNotSupported
176+
}
177+
_ => SeLinuxError::ContextSetFailure(String::new(), selinux_error_description(&e)),
164178
})
165179
}
166180
}
@@ -532,6 +546,9 @@ mod tests {
532546
"File open failure occurred despite file being created: {e}"
533547
);
534548
}
549+
Err(e @ SeLinuxError::OperationNotSupported) => {
550+
println!("{e}");
551+
}
535552
}
536553
}
537554

tests/by-util/test_cp.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7671,3 +7671,59 @@ fn test_cp_gnu_preserve_mode() {
76717671

76727672
assert_eq!(d1_mode, d3_mode);
76737673
}
7674+
7675+
#[test]
7676+
#[cfg(feature = "feat_selinux")]
7677+
fn test_cp_a_z_overrides_context() {
7678+
use std::path::Path;
7679+
use uucore::selinux::{get_selinux_security_context, set_selinux_security_context};
7680+
7681+
let (at, mut ucmd) = at_and_ucmd!();
7682+
at.touch("src");
7683+
7684+
let ctx = "unconfined_u:object_r:user_tmp_t:s0".to_string();
7685+
if set_selinux_security_context(Path::new(&at.plus_as_string("src")), Some(&ctx)).is_err() {
7686+
return;
7687+
}
7688+
7689+
let src_ctx =
7690+
get_selinux_security_context(Path::new(&at.plus_as_string("src")), false).unwrap();
7691+
ucmd.args(&["-aZ", "src", "dst"]).succeeds();
7692+
let dst_ctx =
7693+
get_selinux_security_context(Path::new(&at.plus_as_string("dst")), false).unwrap();
7694+
7695+
assert_ne!(src_ctx, dst_ctx, "-aZ should override context from -a");
7696+
}
7697+
7698+
#[test]
7699+
#[cfg(feature = "feat_selinux")]
7700+
fn test_cp_a_preserves_context() {
7701+
use std::path::Path;
7702+
use uucore::selinux::{get_selinux_security_context, set_selinux_security_context};
7703+
7704+
let (at, mut ucmd) = at_and_ucmd!();
7705+
at.touch("src");
7706+
7707+
let ctx = "unconfined_u:object_r:user_tmp_t:s0".to_string();
7708+
if set_selinux_security_context(Path::new(&at.plus_as_string("src")), Some(&ctx)).is_err() {
7709+
return;
7710+
}
7711+
7712+
let src_ctx =
7713+
get_selinux_security_context(Path::new(&at.plus_as_string("src")), false).unwrap();
7714+
ucmd.args(&["-a", "src", "dst"]).succeeds();
7715+
let dst_ctx =
7716+
get_selinux_security_context(Path::new(&at.plus_as_string("dst")), false).unwrap();
7717+
7718+
assert_eq!(src_ctx, dst_ctx, "-a should preserve SELinux context");
7719+
}
7720+
7721+
#[test]
7722+
#[cfg(feature = "feat_selinux")]
7723+
fn test_cp_preserve_context_with_z_fails() {
7724+
let (at, mut ucmd) = at_and_ucmd!();
7725+
at.touch("src");
7726+
ucmd.args(&["--preserve=context", "-Z", "src", "dst"])
7727+
.fails()
7728+
.stderr_contains("cannot combine");
7729+
}

0 commit comments

Comments
 (0)