Skip to content

uucore: do not panic when argv is unavailable - #13882

Open
takakix2 wants to merge 1 commit into
uutils:mainfrom
takakix2:uucore-argv-guard
Open

uucore: do not panic when argv is unavailable#13882
takakix2 wants to merge 1 commit into
uutils:mainfrom
takakix2:uucore-argv-guard

Conversation

@takakix2

Copy link
Copy Markdown

Problem

UTIL_NAME and EXECUTION_PHRASE index ARGV unconditionally:

let is_man = usize::from(ARGV[base_index].eq("manpage"));
...
ARGV[0].to_string_lossy().into_owned()

That assumes std::env::args_os() always yields at least argv[0]. It does 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 LazyLocks, 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 and the message
points away from the cause.

Fix

Use ARGV.get() / ARGV.first() and fall back to a neutral name (uutils) when
ARGV is empty. Behaviour on a normal binary is unchanged — the fallback is only
reachable when there is no argv to read at all.

How it was found

By embedding uutils as an in-process command provider in a shell that also runs
inside an Android app. The shell is loaded as a .so, so the process it lives in
has no argv of its own; every bundled utility failed there, and the poisoning made
the first failure hide behind a second, unrelated-looking message.

We have been carrying this as a local patch and would rather not — it is a small
guard and it helps any embedder, not just us.

Testing

cargo clippy -p uucore -- -D warnings and cargo fmt --check are clean.

⚠️ 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. Happy to add one if you see a way you'd accept.

`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.
@oech3

oech3 commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

But why do we need to support as library?

Comment thread src/uucore/src/lib/lib.rs
/// 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";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this name?

@codspeed-hq

codspeed-hq Bot commented Aug 12, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 2.95%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 2 improved benchmarks
❌ 2 regressed benchmarks
✅ 91 untouched benchmarks
⏩ 298 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Simulation df_with_path 573.7 µs 704.2 µs -18.53%
Simulation numfmt_to_si_precision[10000] 92.6 ms 95.8 ms -3.27%
Simulation du_summarize_balanced_tree[(5, 4, 10)] 16.8 ms 15.8 ms +6.52%
Simulation du_max_depth_balanced_tree[(6, 4, 10)] 65.2 ms 61.7 ms +5.69%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing takakix2:uucore-argv-guard (e840168) with main (822aa83)

Open in CodSpeed

Footnotes

  1. 298 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@takakix2

Copy link
Copy Markdown
Author

uucore is already published on crates.io as a library, and util_name() / execution_phrase() are pub, so this isn't asking for a new commitment — it's asking two indexing sites not to panic.

To be precise about what I am not claiming: I checked whether a plain binary can reach this, and on current Linux it cannot. Since "exec: Force single empty string when argv is empty" the kernel substitutes [""] for an empty argv, so execve(path, [], envp) yields argc == 1. Measured against unpatched uucore 0.8.0 on 7.0.0:

argc = 1  argv = [""]
1st util_name() -> Ok("")
2nd util_name() -> Ok("")

No panic. So this is genuinely an embedded-host case, and I would rather say so than overstate it.

Where it does happen: uutils compiled into a .so that an Android app dlopens (a Tauri library loaded into the ART process), where std::env::args_os() is empty. The first util_name() then panics with index out of bounds: the len is 0 but the index is 0 inside a LazyLock, which poisons it — every later call reports LazyLock instance has previously been poisoned, so the original failure hides behind a second, unrelated-looking message. That second half is what cost us the most time. (iOS is fine; that process has a real argv[0].)

So the narrower question I'd put back is: given ARGV can be empty, is ARGV[0] the behaviour you want there? The PR leaves normal binaries byte-identical — the fallback is only reachable when there is no argv at all — and adds no API, feature, or dependency, in one file.

If your position is "we don't support that, but we still shouldn't poison a lock", I'm happy to replace the fallback with an explicit expect() that names the cause. That fixes the misleading-second-error half and keeps the failure loud instead of silently renaming the utility. Either shape works for me — tell me which you'd merge and I'll push it.

On the red CI, in case it's in the way: none of it reaches this change. "Run GNU tests (native)" failed in Install dependenciescurl http://launchpadlibrarian.net/.../automake_1.18.1-3_all.deb returned a 341-byte error page after stalling, so dpkg -i rejected it as "not a Debian format archive", and every later step including "Build binaries" was skipped. "Run GNU tests (SELinux)" failed at "Start Fedora VM with SELinux", and CICD.yml is currently failing on main too. The CodSpeed report itself flags "different runtime environments"; a .get() on a once-initialised LazyLock should not move df_with_path by 18%. I can't re-run these from a fork — happy to push a no-op commit if you'd rather see them green.

@oech3

oech3 commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

I think there is nothing to do if it does not break coreutils itself (expect() is unnecessary too).
uutils supports embedding utility to your program, but uucore does not.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants