Merge branch 'cp-preserve-strip-setuid-on-chown-fail' of github.com:Darius-Constantin/coreutils into cp-preserve-strip-setuid-on-chown-fail

This commit is contained in:
Darius-Constantin 2025-12-22 18:48:16 +02:00
commit 25feb0475a
17 changed files with 231 additions and 164 deletions

View file

@ -300,22 +300,22 @@ jobs:
run: make nextest PROFILE=ci CARGOFLAGS="--hide-progress-bar"
env:
RUST_BACKTRACE: "1"
- name: "`make install PROFILE=release-fast COMPLETIONS=n MANPAGES=n LOCALES=n`"
- name: "`make install PROG_PREFIX=uu- PROFILE=release-fast COMPLETIONS=n MANPAGES=n LOCALES=n`"
shell: bash
run: |
set -x
DESTDIR=/tmp/ make PROFILE=release-fast COMPLETIONS=n MANPAGES=n LOCALES=n install
DESTDIR=/tmp/ make install PROG_PREFIX=uu- PROFILE=release-fast COMPLETIONS=n MANPAGES=n LOCALES=n
# Check that utils are built with given profile
./target/release-fast/true
# Check that the utils are present
test -f /tmp/usr/local/bin/tty
# Check that the progs have prefix
test -f /tmp/usr/local/bin/uu-tty
test -f /tmp/usr/local/libexec/uu-coreutils/libstdbuf.*
# Check that the manpage is not present
! test -f /tmp/usr/local/share/man/man1/whoami.1
! test -f /tmp/usr/local/share/man/man1/uu-whoami.1
# Check that the completion is not present
! test -f /tmp/usr/local/share/zsh/site-functions/_install
! test -f /tmp/usr/local/share/bash-completion/completions/head.bash
! test -f /tmp/usr/local/share/fish/vendor_completions.d/cat.fish
! test -f /tmp/usr/local/share/zsh/site-functions/_uu-install
! test -f /tmp/usr/local/share/bash-completion/completions/uu-head.bash
! test -f /tmp/usr/local/share/fish/vendor_completions.d/uu-cat.fish
env:
RUST_BACKTRACE: "1"
- name: "`make install`"

View file

@ -79,8 +79,7 @@ jobs:
matrix:
test-target:
- { name: fuzz_test, should_pass: true }
# https://github.com/uutils/coreutils/issues/5311
- { name: fuzz_date, should_pass: false }
- { name: fuzz_date, should_pass: true }
- { name: fuzz_expr, should_pass: true }
- { name: fuzz_printf, should_pass: true }
- { name: fuzz_echo, should_pass: true }

1
Cargo.lock generated
View file

@ -3126,7 +3126,6 @@ dependencies = [
"clap",
"codspeed-divan-compat",
"fluent",
"hex",
"tempfile",
"uucore",
]

View file

@ -27,20 +27,20 @@ CARGO ?= cargo
CARGOFLAGS ?=
RUSTC_ARCH ?= # should be empty except for cross-build, not --target $(shell rustc --print host-tuple)
#prefix prepended to all binaries and library dir
PROG_PREFIX ?=
# Install directories
PREFIX ?= /usr/local
DESTDIR ?=
BINDIR ?= $(PREFIX)/bin
DATAROOTDIR ?= $(PREFIX)/share
LIBSTDBUF_DIR ?= $(PREFIX)/libexec/coreutils
LIBSTDBUF_DIR ?= $(PREFIX)/libexec/$(PROG_PREFIX)coreutils
# Export variable so that it is used during the build
export LIBSTDBUF_DIR
INSTALLDIR_BIN=$(DESTDIR)$(BINDIR)
#prefix to apply to coreutils binary and all tool binaries
PROG_PREFIX ?=
# This won't support any directory with spaces in its name, but you can just
# make a symlink without spaces that points to the directory.
BASEDIR ?= $(shell pwd)

1
fuzz/Cargo.lock generated
View file

@ -1575,7 +1575,6 @@ version = "0.5.0"
dependencies = [
"clap",
"fluent",
"hex",
"uucore",
]

View file

@ -3,12 +3,38 @@ use libfuzzer_sys::fuzz_target;
use std::ffi::OsString;
use uu_date::uumain;
use uufuzz::generate_and_run_uumain;
fuzz_target!(|data: &[u8]| {
let delim: u8 = 0; // Null byte
let args = data
let fuzz_args: Vec<OsString> = data
.split(|b| *b == delim)
.filter_map(|e| std::str::from_utf8(e).ok())
.map(OsString::from);
uumain(args);
.map(OsString::from)
.collect();
// Skip test cases that would cause the program to read from stdin
// These would hang the fuzzer waiting for input
for i in 0..fuzz_args.len() {
if let Some(arg) = fuzz_args.get(i) {
let arg_str = arg.to_string_lossy();
// Skip if -f- or --file=- (reads dates from stdin)
if (arg_str == "-f"
&& fuzz_args
.get(i + 1)
.map(|a| a.to_string_lossy() == "-")
.unwrap_or(false))
|| arg_str == "-f-"
|| arg_str == "--file=-"
{
return;
}
}
}
// Add program name as first argument (required for proper argument parsing)
let mut args = vec![OsString::from("date")];
args.extend(fuzz_args);
let _ = generate_and_run_uumain(&args, uumain, None);
});

View file

@ -25,7 +25,6 @@ uucore = { workspace = true, features = [
"sum",
"hardware",
] }
hex = { workspace = true }
fluent = { workspace = true }
[dev-dependencies]

View file

@ -22,7 +22,7 @@ use uucore::checksum::{
use uucore::error::UResult;
use uucore::hardware::{HasHardwareFeatures as _, SimdPolicy};
use uucore::line_ending::LineEnding;
use uucore::{format_usage, translate};
use uucore::{format_usage, show_error, translate};
/// Print CPU hardware capability detection information to stderr
/// This matches GNU cksum's --debug behavior
@ -31,9 +31,9 @@ fn print_cpu_debug_info() {
fn print_feature(name: &str, available: bool) {
if available {
eprintln!("cksum: using {name} hardware support");
show_error!("using {name} hardware support");
} else {
eprintln!("cksum: {name} support not detected");
show_error!("{name} support not detected");
}
}
@ -140,6 +140,20 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let check = matches.get_flag(options::CHECK);
let check_flag = |flag| match (check, matches.get_flag(flag)) {
(_, false) => Ok(false),
(true, true) => Ok(true),
(false, true) => Err(ChecksumError::CheckOnlyFlag(flag.into())),
};
// Each of the following flags are only expected in --check mode.
// If we encounter them otherwise, end with an error.
let ignore_missing = check_flag(options::IGNORE_MISSING)?;
let warn = check_flag(options::WARN)?;
let quiet = check_flag(options::QUIET)?;
let strict = check_flag(options::STRICT)?;
let status = check_flag(options::STATUS)?;
let algo_cli = matches
.get_one::<String>(options::ALGORITHM)
.map(AlgoKind::from_cksum)
@ -166,11 +180,6 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let text_flag = matches.get_flag(options::TEXT);
let binary_flag = matches.get_flag(options::BINARY);
let strict = matches.get_flag(options::STRICT);
let status = matches.get_flag(options::STATUS);
let warn = matches.get_flag(options::WARN);
let ignore_missing = matches.get_flag(options::IGNORE_MISSING);
let quiet = matches.get_flag(options::QUIET);
let tag = matches.get_flag(options::TAG);
if tag || binary_flag || text_flag {
@ -191,6 +200,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
// Not --check
// Print hardware debug info if requested
if matches.get_flag(options::DEBUG) {
print_cpu_debug_info();
}
// Set the default algorithm to CRC when not '--check'ing.
let algo_kind = algo_cli.unwrap_or(AlgoKind::Crc);
@ -199,22 +213,15 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let algo = SizedAlgoKind::from_unsized(algo_kind, length)?;
let line_ending = LineEnding::from_zero_flag(matches.get_flag(options::ZERO));
let output_format = figure_out_output_format(
algo,
tag,
binary,
matches.get_flag(options::RAW),
matches.get_flag(options::BASE64),
);
// Print hardware debug info if requested
if matches.get_flag(options::DEBUG) {
print_cpu_debug_info();
}
let opts = ChecksumComputeOptions {
algo_kind: algo,
output_format,
output_format: figure_out_output_format(
algo,
tag,
binary,
matches.get_flag(options::RAW),
matches.get_flag(options::BASE64),
),
line_ending,
};

View file

@ -104,3 +104,4 @@ date-error-date-overflow = date overflow '{$date}'
date-error-setting-date-not-supported-macos = setting the date is not supported by macOS
date-error-setting-date-not-supported-redox = setting the date is not supported by Redox
date-error-cannot-set-date = cannot set date
date-error-extra-operand = extra operand '{$operand}'

View file

@ -99,3 +99,4 @@ date-error-date-overflow = débordement de date '{$date}'
date-error-setting-date-not-supported-macos = la définition de la date n'est pas prise en charge par macOS
date-error-setting-date-not-supported-redox = la définition de la date n'est pas prise en charge par Redox
date-error-cannot-set-date = impossible de définir la date
date-error-extra-operand = opérande supplémentaire '{$operand}'

View file

@ -171,6 +171,17 @@ fn parse_military_timezone_with_offset(s: &str) -> Option<i32> {
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?;
// Check for extra operands (multiple positional arguments)
if let Some(formats) = matches.get_many::<String>(OPT_FORMAT) {
let format_args: Vec<&String> = formats.collect();
if format_args.len() > 1 {
return Err(USimpleError::new(
1,
translate!("date-error-extra-operand", "operand" => format_args[1]),
));
}
}
let format = if let Some(form) = matches.get_one::<String>(OPT_FORMAT) {
if !form.starts_with('+') {
return Err(USimpleError::new(
@ -515,7 +526,7 @@ pub fn uu_app() -> Command {
.help(translate!("date-help-universal"))
.action(ArgAction::SetTrue),
)
.arg(Arg::new(OPT_FORMAT))
.arg(Arg::new(OPT_FORMAT).num_args(0..).trailing_var_arg(true))
}
/// Return the appropriate format string for the given settings.

View file

@ -164,16 +164,27 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> {
binary_flag_default
};
let check = matches.get_flag("check");
let status = matches.get_flag("status");
let quiet = matches.get_flag("quiet");
let strict = matches.get_flag("strict");
let warn = matches.get_flag("warn");
let ignore_missing = matches.get_flag("ignore-missing");
if ignore_missing && !check {
// --ignore-missing needs -c
return Err(ChecksumError::IgnoreNotCheck.into());
}
let check_flag = |flag| match (check, matches.get_flag(flag)) {
(_, false) => Ok(false),
(true, true) => Ok(true),
(false, true) => Err(ChecksumError::CheckOnlyFlag(flag.into())),
};
// Each of the following flags are only expected in --check mode.
// If we encounter them otherwise, end with an error.
let ignore_missing = check_flag("ignore-missing")?;
let warn = check_flag("warn")?;
let quiet = check_flag("quiet")?;
let strict = check_flag("strict")?;
let status = check_flag("status")?;
let files = matches.get_many::<OsString>(options::FILE).map_or_else(
// No files given, read from stdin.
|| Box::new(iter::once(OsStr::new("-"))) as Box<dyn Iterator<Item = &OsStr>>,
// At least one file given, read from them.
|files| Box::new(files.map(OsStr::new)) as Box<dyn Iterator<Item = &OsStr>>,
);
if check {
// on Windows, allow --binary/--text to be used with --check
@ -188,13 +199,6 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> {
}
}
// Execute the checksum validation based on the presence of files or the use of stdin
// Determine the source of input: a list of files or stdin.
let input = matches.get_many::<OsString>(options::FILE).map_or_else(
|| iter::once(OsStr::new("-")).collect::<Vec<_>>(),
|files| files.map(OsStr::new).collect::<Vec<_>>(),
);
let verbose = ChecksumVerbose::new(status, quiet, warn);
let opts = ChecksumValidateOptions {
@ -204,16 +208,11 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> {
};
// Execute the checksum validation
return perform_checksum_validation(input.iter().copied(), Some(algo_kind), length, opts);
} else if quiet {
return Err(ChecksumError::QuietNotCheck.into());
} else if strict {
return Err(ChecksumError::StrictNotCheck.into());
return perform_checksum_validation(files, Some(algo_kind), length, opts);
}
let line_ending = LineEnding::from_zero_flag(matches.get_flag("zero"));
let algo = SizedAlgoKind::from_unsized(algo_kind, length)?;
let line_ending = LineEnding::from_zero_flag(matches.get_flag("zero"));
let opts = ChecksumComputeOptions {
algo_kind: algo,
@ -227,13 +226,6 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> {
line_ending,
};
let files = matches.get_many::<OsString>(options::FILE).map_or_else(
// No files given, read from stdin.
|| Box::new(iter::once(OsStr::new("-"))) as Box<dyn Iterator<Item = &OsStr>>,
// At least one file given, read from them.
|files| Box::new(files.map(OsStr::new)) as Box<dyn Iterator<Item = &OsStr>>,
);
// Show the hashsum of the input
perform_checksum_computation(opts, files)
}

View file

@ -3,10 +3,14 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use clap::{Arg, ArgAction, Command};
use std::env;
use uucore::translate;
use uucore::{error::UResult, format_usage};
use std::io::Write;
use clap::{Arg, ArgAction, Command};
use uucore::error::UResult;
use uucore::line_ending::LineEnding;
use uucore::{format_usage, os_str_as_bytes, translate};
static OPT_NULL: &str = "null";
@ -21,15 +25,16 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
.map(|v| v.map(ToString::to_string).collect())
.unwrap_or_default();
let separator = if matches.get_flag(OPT_NULL) {
"\x00"
} else {
"\n"
};
let separator = LineEnding::from_zero_flag(matches.get_flag(OPT_NULL));
if variables.is_empty() {
for (env_var, value) in env::vars() {
print!("{env_var}={value}{separator}");
for (env_var, value) in env::vars_os() {
let env_bytes = os_str_as_bytes(&env_var)?;
let val_bytes = os_str_as_bytes(&value)?;
std::io::stdout().lock().write_all(env_bytes)?;
print!("=");
std::io::stdout().lock().write_all(val_bytes)?;
print!("{separator}");
}
return Ok(());
}
@ -41,8 +46,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
error_found = true;
continue;
}
if let Ok(var) = env::var(env_var) {
print!("{var}{separator}");
if let Some(var) = env::var_os(env_var) {
let val_bytes = os_str_as_bytes(&var)?;
std::io::stdout().lock().write_all(val_bytes)?;
print!("{separator}");
} else {
error_found = true;
}

View file

@ -373,12 +373,9 @@ impl SizedAlgoKind {
pub enum ChecksumError {
#[error("the --raw option is not supported with multiple files")]
RawMultipleFiles,
#[error("the --ignore-missing option is meaningful only when verifying checksums")]
IgnoreNotCheck,
#[error("the --strict option is meaningful only when verifying checksums")]
StrictNotCheck,
#[error("the --quiet option is meaningful only when verifying checksums")]
QuietNotCheck,
#[error("the --{0} option is meaningful only when verifying checksums")]
CheckOnlyFlag(String),
// --length sanitization errors
#[error("--length required for {}", .0.quote())]

View file

@ -11,7 +11,7 @@
//! instead of parsing error strings, providing a more robust solution.
//!
use crate::error::UResult;
use crate::error::{UResult, USimpleError};
use crate::locale::translate;
use clap::error::{ContextKind, ErrorKind};
@ -108,43 +108,37 @@ impl<'a> ErrorFormatter<'a> {
where
F: FnOnce(),
{
let code = self.print_error(err, exit_code);
callback();
std::process::exit(code);
}
/// Print error and return exit code (no exit call)
pub fn print_error(&self, err: &Error, exit_code: i32) -> i32 {
match err.kind() {
ErrorKind::DisplayHelp | ErrorKind::DisplayVersion => self.handle_display_errors(err),
ErrorKind::UnknownArgument => {
self.handle_unknown_argument_with_callback(err, exit_code, callback)
}
ErrorKind::UnknownArgument => self.handle_unknown_argument(err, exit_code),
ErrorKind::InvalidValue | ErrorKind::ValueValidation => {
self.handle_invalid_value_with_callback(err, exit_code, callback)
}
ErrorKind::MissingRequiredArgument => {
self.handle_missing_required_with_callback(err, exit_code, callback)
self.handle_invalid_value(err, exit_code)
}
ErrorKind::MissingRequiredArgument => self.handle_missing_required(err, exit_code),
ErrorKind::TooFewValues | ErrorKind::TooManyValues | ErrorKind::WrongNumberOfValues => {
// These need full clap formatting
eprint!("{}", err.render());
callback();
std::process::exit(exit_code);
exit_code
}
_ => self.handle_generic_error_with_callback(err, exit_code, callback),
_ => self.handle_generic_error(err, exit_code),
}
}
/// Handle help and version display
fn handle_display_errors(&self, err: &Error) -> ! {
fn handle_display_errors(&self, err: &Error) -> i32 {
print!("{}", err.render());
std::process::exit(0);
0
}
/// Handle unknown argument errors with callback
fn handle_unknown_argument_with_callback<F>(
&self,
err: &Error,
exit_code: i32,
callback: F,
) -> !
where
F: FnOnce(),
{
/// Handle unknown argument errors
fn handle_unknown_argument(&self, err: &Error, exit_code: i32) -> i32 {
if let Some(invalid_arg) = err.get(ContextKind::InvalidArg) {
let arg_str = invalid_arg.to_string();
let error_word = translate!("common-error");
@ -179,21 +173,13 @@ impl<'a> ErrorFormatter<'a> {
self.print_usage_and_help();
} else {
self.print_simple_error_with_callback(
&translate!("clap-error-unexpected-argument-simple"),
exit_code,
|| {},
);
self.print_simple_error_msg(&translate!("clap-error-unexpected-argument-simple"));
}
callback();
std::process::exit(exit_code);
exit_code
}
/// Handle invalid value errors with callback
fn handle_invalid_value_with_callback<F>(&self, err: &Error, exit_code: i32, callback: F) -> !
where
F: FnOnce(),
{
/// Handle invalid value errors
fn handle_invalid_value(&self, err: &Error, exit_code: i32) -> i32 {
let invalid_arg = err.get(ContextKind::InvalidArg);
let invalid_value = err.get(ContextKind::InvalidValue);
@ -245,32 +231,22 @@ impl<'a> ErrorFormatter<'a> {
eprintln!();
eprintln!("{}", translate!("common-help-suggestion"));
} else {
self.print_simple_error(&err.render().to_string(), exit_code);
self.print_simple_error_msg(&err.render().to_string());
}
// InvalidValue errors traditionally use exit code 1 for backward compatibility
// But if a utility explicitly requests a high exit code (>= 125), respect it
// This allows utilities like runcon (125) to override the default while preserving
// the standard behavior for utilities using normal error codes (1, 2, etc.)
let actual_exit_code = if matches!(err.kind(), ErrorKind::InvalidValue) && exit_code < 125 {
if matches!(err.kind(), ErrorKind::InvalidValue) && exit_code < 125 {
1 // Force exit code 1 for InvalidValue unless using special exit codes
} else {
exit_code // Respect the requested exit code for special cases
};
callback();
std::process::exit(actual_exit_code);
}
}
/// Handle missing required argument errors with callback
fn handle_missing_required_with_callback<F>(
&self,
err: &Error,
exit_code: i32,
callback: F,
) -> !
where
F: FnOnce(),
{
/// Handle missing required argument errors
fn handle_missing_required(&self, err: &Error, exit_code: i32) -> i32 {
let rendered_str = err.render().to_string();
let lines: Vec<&str> = rendered_str.lines().collect();
@ -313,15 +289,11 @@ impl<'a> ErrorFormatter<'a> {
}
_ => eprint!("{}", err.render()),
}
callback();
std::process::exit(exit_code);
exit_code
}
/// Handle generic errors with callback
fn handle_generic_error_with_callback<F>(&self, err: &Error, exit_code: i32, callback: F) -> !
where
F: FnOnce(),
{
/// Handle generic errors
fn handle_generic_error(&self, err: &Error, exit_code: i32) -> i32 {
let rendered_str = err.render().to_string();
if let Some(main_error_line) = rendered_str.lines().next() {
self.print_localized_error_line(main_error_line);
@ -330,27 +302,16 @@ impl<'a> ErrorFormatter<'a> {
} else {
eprint!("{}", err.render());
}
callback();
std::process::exit(exit_code);
exit_code
}
/// Print a simple error message
fn print_simple_error(&self, message: &str, exit_code: i32) -> ! {
self.print_simple_error_with_callback(message, exit_code, || {})
}
/// Print a simple error message with callback
fn print_simple_error_with_callback<F>(&self, message: &str, exit_code: i32, callback: F) -> !
where
F: FnOnce(),
{
/// Print a simple error message (no exit)
fn print_simple_error_msg(&self, message: &str) {
let error_word = translate!("common-error");
eprintln!(
"{}: {message}",
self.color_mgr.colorize(&error_word, Color::Red)
);
callback();
std::process::exit(exit_code);
}
/// Print error line with localized "error:" prefix
@ -478,7 +439,9 @@ where
if e.exit_code() == 0 {
e.into() // Preserve help/version
} else {
handle_clap_error_with_exit_code(e, exit_code)
let formatter = ErrorFormatter::new(crate::util_name());
let code = formatter.print_error(&e, exit_code);
USimpleError::new(code, "")
}
})
}

View file

@ -17,6 +17,45 @@ fn test_invalid_arg() {
new_ucmd!().arg("--definitely-invalid").fails_with_code(1);
}
#[test]
fn test_empty_arguments() {
new_ucmd!().arg("").fails_with_code(1);
new_ucmd!().args(&["", ""]).fails_with_code(1);
new_ucmd!().args(&["", "", ""]).fails_with_code(1);
}
#[test]
fn test_extra_operands() {
new_ucmd!()
.args(&["test", "extra"])
.fails_with_code(1)
.stderr_contains("extra operand 'extra'");
}
#[test]
fn test_invalid_long_option() {
new_ucmd!()
.arg("--fB")
.fails_with_code(1)
.stderr_contains("unexpected argument '--fB'");
}
#[test]
fn test_invalid_short_option() {
new_ucmd!()
.arg("-w")
.fails_with_code(1)
.stderr_contains("unexpected argument '-w'");
}
#[test]
fn test_single_dash_as_date() {
new_ucmd!()
.arg("-")
.fails_with_code(1)
.stderr_contains("invalid date");
}
#[test]
fn test_date_email() {
for param in ["--rfc-email", "--rfc-e", "-R", "--rfc-2822", "--rfc-822"] {

View file

@ -90,3 +90,30 @@ fn test_null_separator() {
.stdout_is("FOO\x00VALUE\x00");
}
}
#[test]
#[cfg(unix)]
#[cfg(not(any(target_os = "freebsd", target_os = "android", target_os = "openbsd")))]
fn test_non_utf8_value() {
use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;
// Environment variable values can contain non-UTF-8 bytes on Unix.
// printenv should output them correctly, matching GNU behavior.
// Reproduces: LD_PRELOAD=$'/tmp/lib.so\xff' printenv LD_PRELOAD
let value_with_invalid_utf8 = OsStr::from_bytes(b"/tmp/lib.so\xff");
let result = new_ucmd!()
.env("LD_PRELOAD", value_with_invalid_utf8)
.arg("LD_PRELOAD")
.run();
// Use byte-based assertions to avoid UTF-8 conversion issues
// when the test framework tries to format error messages
assert!(
result.succeeded(),
"Command failed with exit code: {:?}, stderr: {:?}",
result.code(),
String::from_utf8_lossy(result.stderr())
);
result.stdout_is_bytes(b"/tmp/lib.so\xff\n");
}