uucore: do not panic when argv is unavailable - #13882
Conversation
`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.
|
But why do we need to support as library? |
| /// 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"; |
Merging this PR will degrade performance by 2.95%
|
| 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)
Footnotes
-
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. ↩
|
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 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 the narrower question I'd put back is: given 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 On the red CI, in case it's in the way: none of it reaches this change. "Run GNU tests (native)" failed in |
|
I think there is nothing to do if it does not break |
Problem
UTIL_NAMEandEXECUTION_PHRASEindexARGVunconditionally:That assumes
std::env::args_os()always yields at leastargv[0]. It does for anormal 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, whereargs_os()returns an empty vector.The first utility that resolves its name then panics with
and because these are
LazyLocks, the panic also poisons the lock: every latercall fails with
LazyLock instance has previously been poisonedinstead of theoriginal 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) whenARGVis empty. Behaviour on a normal binary is unchanged — the fallback is onlyreachable 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 inhas 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 warningsandcargo fmt --checkare clean.ARGVis a process-globalLazyLockover the realargs_os(), so an empty argv cannot be simulated in-process without changing theproduction code path. Happy to add one if you see a way you'd accept.