From 344ba7f101e13697135a0e763885bc5d168ee267 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 18 Oct 2026 22:13:00 +0200 Subject: [PATCH 1/2] coreutils: return ExitCode from main instead of calling exit --- src/bin/coreutils.rs | 35 +++++++++++++++++++---------------- src/common/validation.rs | 14 +++++++------- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/src/bin/coreutils.rs b/src/bin/coreutils.rs index 46fcbc50ad8..390efd6a58b 100644 --- a/src/bin/coreutils.rs +++ b/src/bin/coreutils.rs @@ -9,6 +9,7 @@ use itertools::Itertools as _; use std::cmp; use std::ffi::OsString; use std::io::{self, Write}; +use std::process::ExitCode; use uucore::{Args, error::strip_errno}; const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -49,15 +50,15 @@ Currently defined functions: } #[allow(clippy::cognitive_complexity)] -fn main() { +fn main() -> ExitCode { let utils = util_map(); let mut args = uucore::args_os(); let binary = validation::binary_path(&mut args); - let binary_as_util = validation::name(&binary).unwrap_or_else(|| { + let Some(binary_as_util) = validation::name(&binary) else { usage(&utils, ""); - exit(0); - }); + return ExitCode::SUCCESS; + }; // binary name ends with util name? let is_coreutils = binary_as_util.ends_with("utils"); @@ -73,13 +74,13 @@ fn main() { uucore::set_utility_is_second_arg(); args.next() } else { - validation::not_found(&OsString::from(binary_as_util)); + return validation::not_found(&OsString::from(binary_as_util)); }; // 0th argument equals util name? if let Some(util_os) = util_name { let Some(util) = util_os.to_str() else { - validation::not_found(&util_os) + return validation::not_found(&util_os); }; match util { @@ -87,7 +88,7 @@ fn main() { // we should fail with additional args https://github.com/uutils/coreutils/issues/11383#issuecomment-4082564058 if args.next().is_some() { let _ = writeln!(io::stderr(), "coreutils: invalid argument"); - exit(1); + return ExitCode::FAILURE; } let mut out = io::stdout().lock(); for util in utils.keys() { @@ -95,19 +96,19 @@ fn main() { && e.kind() != io::ErrorKind::BrokenPipe { let _ = writeln!(io::stderr(), "coreutils: {}", strip_errno(&e)); - exit(1); + return ExitCode::FAILURE; } } - exit(0); + return ExitCode::SUCCESS; } "--version" | "-V" => { if let Err(e) = writeln!(io::stdout(), "coreutils {VERSION} (multi-call binary)") && e.kind() != io::ErrorKind::BrokenPipe { let _ = writeln!(io::stderr(), "coreutils: {}", strip_errno(&e)); - exit(1); + return ExitCode::FAILURE; } - exit(0); + return ExitCode::SUCCESS; } // Not a special command: fallthrough to calling a util _ => {} @@ -120,17 +121,19 @@ fn main() { // Could be something like: // #[cfg(not(feature = "only_english"))] validation::setup_localization_or_exit(util); - exit(uumain(vec![util_os].into_iter().chain(args))); + let code = uumain(vec![util_os].into_iter().chain(args)); + // ExitCode only holds a u8; larger codes (Windows) go through exit. + return u8::try_from(code).map_or_else(|_| exit(code), ExitCode::from); } // GNU coreutils --help string shows help for coreutils if util == "-h" || (util.starts_with("--h") && "--help".starts_with(util)) { usage(&utils, binary_as_util); - exit(0); + return ExitCode::SUCCESS; } else if util.starts_with('-') { // Argument looks like an option but wasn't recognized - validation::unrecognized_option(binary_as_util, &util_os); + return validation::unrecognized_option(binary_as_util, &util_os); } - validation::not_found(&util_os); + return validation::not_found(&util_os); } // GNU just fails, but busybox tests needs usage // todo: patch the test suite instead @@ -139,5 +142,5 @@ fn main() { } else { let _ = writeln!(io::stderr(), "coreutils: missing argument"); } - exit(1); + ExitCode::FAILURE } diff --git a/src/common/validation.rs b/src/common/validation.rs index 4b84ca705a4..5d004d9d87c 100644 --- a/src/common/validation.rs +++ b/src/common/validation.rs @@ -8,7 +8,7 @@ use std::ffi::{OsStr, OsString}; use std::io::{Write, stderr}; use std::path::{Path, PathBuf}; -use std::process; +use std::process::{self, ExitCode}; use uucore::Args; use uucore::display::Quotable; @@ -49,24 +49,24 @@ pub fn get_all_utilities( .collect() } -/// Prints a "utility not found" error and exits -pub fn not_found(util: &OsStr) -> ! { +/// Prints a "utility not found" error +pub fn not_found(util: &OsStr) -> ExitCode { let _ = writeln!( stderr(), "coreutils: unknown program '{}'", util.maybe_quote() ); - exit(1); + ExitCode::FAILURE } -/// Prints an "unrecognized option" error and exits -pub fn unrecognized_option(binary_name: &str, option: &OsStr) -> ! { +/// Prints an "unrecognized option" error +pub fn unrecognized_option(binary_name: &str, option: &OsStr) -> ExitCode { let _ = writeln!( stderr(), "{binary_name}: unrecognized option '{}'", option.to_string_lossy() ); - exit(1); + ExitCode::FAILURE } /// Sets up localization for a utility with proper error handling From f473e1a18101c22b8df68a51ad606dfce035cfb1 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 19 Oct 2026 22:41:00 +0200 Subject: [PATCH 2/2] util: add valgrind suppressions for process-lifetime allocations closes: #15522 --- util/valgrind.supp | 138 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 util/valgrind.supp diff --git a/util/valgrind.supp b/util/valgrind.supp new file mode 100644 index 00000000000..154eb1b6cfa --- /dev/null +++ b/util/valgrind.supp @@ -0,0 +1,138 @@ +# spell-checker:ignore supp jiff rayon crossbeam Memcheck stdin +# +# Valgrind suppressions for uutils coreutils. +# +# Everything below is allocated once, kept in a static (or a thread that lives +# until exit) and released by the OS when the process ends. Rust never drops +# statics, so valgrind reports these blocks as "still reachable" (or "possibly +# lost" when only an interior pointer is left). None of them grows with the +# input, so they are not leaks. +# +# Usage: +# valgrind --leak-check=full --show-reachable=yes \ +# --suppressions=util/valgrind.supp coreutils cat file + +# The parsed .ftl translations. Bundles need `&'static FluentResource`, so the +# resources are leaked on purpose and cached for the rest of the run. +{ + uucore-locale-resources + Memcheck:Leak + match-leak-kinds: reachable + ... + fun:*uucore*mods*locale* +} +# Same allocations as above. Debug builds have deep parser stacks and the +# default --num-callers cuts them before the uucore frame, so match the parser. +{ + fluent-resources + Memcheck:Leak + match-leak-kinds: reachable + ... + fun:*fluent_bundle*resource*FluentResource*try_new* +} +{ + fluent-parser + Memcheck:Leak + match-leak-kinds: reachable + ... + fun:*fluent_syntax*parser* +} +# The command line, copied once into a LazyLock static by uucore::args_os(). +{ + uucore-argv + Memcheck:Leak + match-leak-kinds: reachable + ... + fun:*uucore*ARGV* +} +# Derived from ARGV and cached for error messages ("coreutils cat: ..."). +{ + uucore-execution-phrase + Memcheck:Leak + match-leak-kinds: reachable + ... + fun:*uucore*EXECUTION_PHRASE* +} +{ + uucore-util-name + Memcheck:Leak + match-leak-kinds: reachable + ... + fun:*uucore*UTIL_NAME* +} +# jiff loads the system time zone database and the local zone into a global +# cache on first use (date, ls -l, pr, touch...). The cache holds Arcs, so +# valgrind only sees interior pointers and reports "possibly lost". +{ + jiff-timezone-database + Memcheck:Leak + match-leak-kinds: possible,reachable + ... + fun:*jiff*tz* +} +# sort spawns rayon's global thread pool. Its workers are still parked when +# main returns, so their stacks and registry show up as "possibly lost". +{ + rayon-global-thread-pool + Memcheck:Leak + match-leak-kinds: possible,reachable + ... + fun:*rayon_core*registry* +} +# The epoch-based garbage collector used by the rayon workers is a global. +{ + crossbeam-epoch-collector + Memcheck:Leak + match-leak-kinds: possible,reachable + ... + fun:*crossbeam_epoch* +} +# Thread handle created when a channel is first used inside a worker thread. +{ + std-thread-handle-in-worker + Memcheck:Leak + match-leak-kinds: possible,reachable + ... + fun:*std*thread*current*init_current* +} +# regex keeps a per-thread id for its matcher cache pool in a static. +{ + regex-thread-pool-id + Memcheck:Leak + match-leak-kinds: reachable + ... + fun:*regex_automata* +} +# Utilities cache locale-dependent data (date format, decimal separator...) in +# OnceLock/LazyLock statics. Only "reachable" blocks are matched here, so a +# real leak inside such an initializer would still be reported. +{ + once-lock-statics + Memcheck:Leak + match-leak-kinds: reachable + ... + fun:*OnceLock*initialize* +} +{ + lazy-lock-statics + Memcheck:Leak + match-leak-kinds: reachable + ... + fun:*LazyLock*force* +} +# std's global stdin buffer; any Rust program reading stdin has it. +{ + std-stdin-buffer + Memcheck:Leak + match-leak-kinds: reachable + ... + fun:*std*io*stdio* +} +# std records the main thread's stack bounds for its stack overflow handler. +{ + std-main-thread-info + Memcheck:Leak + match-leak-kinds: reachable + ... + fun:*std*sys*pal*unix*stack_overflow* +}