From e84016822116feef3a2460e3789a1d5931453b92 Mon Sep 17 00:00:00 2001 From: takaki2 Date: Wed, 12 Aug 2026 12:47:22 +0900 Subject: [PATCH] uucore: do not panic when argv is unavailable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `UTIL_NAME` and `EXECUTION_PHRASE` index `ARGV` unconditionally, assuming `std::env::args_os()` always yields at least `argv[0]`. That holds for a normal binary, but not when a utility is called as a library from a host that is not a normal process entry point — for example from inside a shared object loaded with `dlopen`, where `args_os()` returns an empty vector. The first utility that resolves its name then panics with index out of bounds: the len is 0 but the index is 0 and, because these are `LazyLock`s, the panic also poisons the lock: every later call fails with "LazyLock instance has previously been poisoned" instead of the original error, so the failure outlives the call that caused it. Use `ARGV.get()` / `ARGV.first()` and fall back to a neutral name when `ARGV` is empty. Behaviour on a normal binary is unchanged: the fallback is only reachable when there is no argv to read. This was found by embedding uutils as an in-process command provider in a shell that runs inside an Android app (the shell is loaded as a `.so`, so it has no argv of its own). Every bundled utility failed there, and the poisoning made the message point away from the cause. Not covered by a unit test: `ARGV` is a process-global `LazyLock` over the real `args_os()`, so an empty argv cannot be simulated in-process without changing the production code path. --- src/uucore/src/lib/lib.rs | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index 048d6a0ca01..1563afeabde 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -5,7 +5,7 @@ //! library ~ (core/bundler file) // #![deny(missing_docs)] //TODO: enable this // -// spell-checker:ignore sigaction SIGBUS SIGSEGV extendedbigdecimal myutil logind +// spell-checker:ignore sigaction SIGBUS SIGSEGV extendedbigdecimal myutil logind dlopen // * feature-gated external crates (re-shared as public internal modules) #[cfg(feature = "libc")] @@ -357,17 +357,37 @@ static ARGV: LazyLock> = LazyLock::new(|| wild::args_os().collect( #[cfg(not(windows))] static ARGV: LazyLock> = LazyLock::new(|| std::env::args_os().collect()); +/// Name reported when `argv` is unavailable. +/// +/// `std::env::args_os()` is empty when the code does not run from a normal +/// process entry point — for example when a utility is called as a library from +/// inside a shared object loaded with `dlopen`. There is no `argv[0]` to derive +/// a name from in that case. +const ARGV_UNAVAILABLE_NAME: &str = "uutils"; + static UTIL_NAME: LazyLock = LazyLock::new(|| { let base_index = usize::from(get_utility_is_second_arg()); - let is_man = usize::from(ARGV[base_index].eq("manpage")); + + // `ARGV` can legitimately be empty (see `ARGV_UNAVAILABLE_NAME`). Indexing it + // panics with "index out of bounds: the len is 0 but the index is 0", and + // because this is a `LazyLock`, the panic also poisons the lock: every later + // call then fails with "LazyLock instance has previously been poisoned" + // instead of the original error. Fall back to a neutral name instead. + let Some(base_arg) = ARGV.get(base_index) else { + return ARGV_UNAVAILABLE_NAME.to_string(); + }; + let is_man = usize::from(base_arg.eq("manpage")); let argv_index = base_index + is_man; + let Some(arg) = ARGV.get(argv_index) else { + return ARGV_UNAVAILABLE_NAME.to_string(); + }; // Strip directory path to show only utility name // (e.g., "mkdir" instead of "./target/debug/mkdir") // in version output, error messages, and other user-facing output - std::path::Path::new(&ARGV[argv_index]) + std::path::Path::new(arg) .file_name() - .unwrap_or(&ARGV[argv_index]) + .unwrap_or(arg) .to_string_lossy() .into_owned() }); @@ -385,7 +405,11 @@ static EXECUTION_PHRASE: LazyLock = LazyLock::new(|| { .collect::>() .join(" ") } else { - ARGV[0].to_string_lossy().into_owned() + // Same reasoning as in `UTIL_NAME`: `ARGV` may be empty. + ARGV.first().map_or_else( + || ARGV_UNAVAILABLE_NAME.to_string(), + |arg| arg.to_string_lossy().into_owned(), + ) } });