This file collects per-utility POSIX conformance audits for the process-control
utilities crate. Each audit follows the playbook in audits.md and was
spec-vs-code verified: every Critical/Major claim below was confirmed by reading
the cited implementation lines and the cited POSIX.1-2024 slice
(~/tmp/posix.2024/sliced/xcu-shell-and-utilities/3-utilities/<util>.md).
Several agent-proposed findings were refuted on verification; those are
recorded inline (struck through) so the next auditor does not re-chase them.
Crate: process/ — env, fuser, kill, nice, nohup, renice,
timeout, xargs (8 binaries) plus the shared signal.rs module.
Date: 2026-06-17
All priority findings have since been remediated on the process-audit
branch across 8 themed phases, each independently committed with regression
tests (the Critical/Major fixes were behaviorally re-verified against the
rebuilt binaries) and clippy/fmt clean:
- Phase 1 —
env(#E1–#E5): exec failures map to 126/127 via a new sharedposixutils_process::exec::exec_error_exit; non-POSIX--ignore-env/--help/--versionsurface removed; deterministicBTreeMapdump;name=valuevalidated against the portable name set. - Phase 2 —
nice(#NC1–#NC5): a failednice()no longer prevents invoking the utility; errno-based failure check; 126/127 mapping; clap range removed;--nicevaldropped. - Phase 3 —
renice(#RN1–#RN4) +plib::priority: acceptsID...with per-ID continuation and non-zero exit on any failure; ID 0 accepted;-20..=20; theprioritylibrary no longer prints its own diagnostics. - Phase 4 —
nohup(#NH1–#NH6):nohup.outcreated mode 0600;$HOMEfallback; spec stderr-follows-stdout-fd routing; no-panic file-open failure;dirsdependency dropped. - Phase 5 —
timeout(#T1–#T6):setitimersub-second precision; child inherits timeout's SIGTTIN/SIGTTOU dispositions; full terminate-default forwarding set; WCOREDUMP re-raise; group SIGCONT;MONITORED_PIDsentinel. - Phase 6 —
xargs(#X1–#X5):-r+ run-once-on-empty; UTF-8 word splitting; quoted-newline error;-Ehonored in insert mode;is_affirmativefor-p. - Phase 7 —
fuser(#F1–#F7):" %1d"PID format;makedevdevice decode;scan_procserror propagation; bounds-checked/proc/mounts;f/Fuse-chars; stat-timeout thread tidied. - Phase 8 —
kill(#K1/#K2): per-signal%s%c-loutput;-lno longer mis-reads a following option as its exit_status operand. #C1(cross-cutting i18n): all 8 binaries migrated toplib::diag.
All 8 utilities promoted to README Stage 6 — Audited. Validation: full
workspace cargo build --release clean; cargo clippy --all-targets zero
warnings; cargo fmt --all -- --check clean; 132 process integration tests +
71 plib tests green. The refuted findings below (e.g. env -u, fuser
exit-on-no-match, xargs signal→125, kill ABRT/IOT) remain dispositioned
as recorded — not actionable.
Three themes recur across every utility in the crate. Fixing them once closes a column of Minor items below.
Every binary calls setlocale(LC_ALL, "") + textdomain("posixutils-rs") +
bind_textdomain_codeset in main, and the clap about/help strings are
wrapped in gettext(). But runtime diagnostic strings (the eprintln!
error/warning paths, and the &'static str error returns) are not wrapped in
gettext(). POSIX requires LC_MESSAGES to determine "the format and contents
of diagnostic messages written to standard error" for each utility. So the
locale machinery is initialized but has no effect on actual error text.
-
#C1Major — wrap runtime diagnostics ingettext(). ✓ fixed across Phases 1–8: all 8 binaries now initialize locale viaplib::diag::init_localeand route runtime diagnostics throughplib::diag::error/warningwith the human fragments wrapped ingettext()(replacing the per-cratesetlocale/textdomainboilerplate and raweprintln!error paths).
kill (list_signals/lookup_signum/signum_to_name) and timeout
(parse_signal) share process/signal.rs (148 lines). It holds a per-OS
SIGLIST table (32 entries Linux, 31 macOS). It is sound; the only blemish is
the Linux table lists the historical alias IOT (signo 6) in addition to
ABRT (signo 6) — harmless, see the kill section (#K-refuted).
POSIX mandates 126 (utility found, cannot invoke) / 127 (utility not
found) for the utility-launcher family (env, nice, nohup, timeout,
xargs). The crate is split: nohup (nohup.rs:133-142), timeout, and
xargs map these correctly; env and nice do not — they propagate the
exec error through main() -> Result<…>'s ?, so Rust's default harness prints
the error and exits 1 for every case (see #E1, #NC2).
Implementation: process/env.rs (116 lines)
Tests: none (no process/tests/env/)
Spec: POSIX.1-2024, Vol. 3 §3, env (slice lines 94028–94100)
Core behavior is correct: inherit env, -i clear, name=value merge, utility
exec with the modified environment, and the no-utility stdout dump. One genuine
Critical: exec failures never produce exit 126/127 (always 1). The clap layer
leaks a non-POSIX --ignore-env long option and intercepts --help/--version.
No tests exist.
-
#E1— exec failure never yields 126/127. ✓ fixed in Phase 1.env.rs:88-97,113.exec_utilreturnsio::Result<()>; the?atenv.rs:113propagates the error out ofmain() -> Result<…>, so Rust prints it and exits 1. POSIXenvEXIT STATUS mandates 126 (found, not invocable) and 127 (not found). Fix: matchErrorKind::NotFound→exit(127),PermissionDenied/other →exit(126), internal errors → 1-125.
-
#E2—--ignore-envlong option is non-POSIX. ✓ fixed in Phase 1.env.rs:22-29.#[arg(short, long)]synthesizes--ignore-env; POSIXenvSYNOPSIS (slice 94031) isenv [-i] [name=value]... [utility [argument...]]— only-i. Fix: droplong, keepshort. -
#E3—--help/--versionintercepted by clap. ✓ fixed in Phase 1.env.rs:18-20.env --help foois eaten by clap instead of being treated as a utility named--help. Fix:disable_help_flag/disable_version_flag, or document.
-
#E4— stdout dump order is nondeterministic. ✓ fixed in Phase 1 (BTreeMap).env.rs:80-85.print_enviterates aHashMap, soname=valueorder randomizes per run. POSIX does not mandate order, but determinism is expected; sort by key. -
#E5—name=valuesplit uses barecontains('='). ✓ fixed in Phase 1.env.rs:43. A leading operand whose name is not a valid env-var name (e.g.1abc=x) is still treated as an assignment. Fix: validate the pre-=token against[A-Za-z_][A-Za-z0-9_]*.
-
— re-examined: POSIX.1-2024-u namemissingenvSYNOPSIS (slice 94031) isenv [-i] …with no-u.-uis a GNU extension, not a POSIX gap. Not actionable.
| Area | Status | Notes (file:line) |
|---|---|---|
-i clear environment |
CONFORMS | env.rs:22-29,61-65,94 (.env_clear().envs()) |
name=value merge |
CONFORMS | env.rs:35-57,72-75 |
utility [argument…] exec |
CONFORMS | env.rs:88-96 (PATH-searched via Command::exec) |
| No-utility → dump env to stdout | CONFORMS | env.rs:109-110,80-85 |
-- end-of-options |
CONFORMS | clap default |
- operand (unspecified) |
N/A | spec 94038 leaves it unspecified |
| EXIT 0 / utility status | CONFORMS | exec replaces image |
| EXIT 126 / 127 | MISSING | #E1 |
| ASYNCHRONOUS EVENTS | CONFORMS | "Default"; no handlers needed |
| STDERR i18n | PARTIAL | #C1 |
setlocale/textdomain |
CONFORMS | env.rs:100-102 |
- no-arg dump;
-iempties;-i FOO=barexact;name=valueoverride -
env /nonexistent→ 127;env /etc/passwd(non-exec) → 126 (proves#E1) -
--delimiter;A=Bafter utility name treated as utility arg - exit-status passthrough
Implementation: process/fuser.rs (1538 lines; Linux + macOS backends)
Tests: process/tests/fuser/ (basic/tcp/udp/unix/with_user, ~377 lines)
Spec: POSIX.1-2024, Vol. 3 §3, fuser (XSI; slice lines 98770–98910)
The XSI base form (-c, -f, -u) and the stdout/stderr channel split are
implemented. The standard does not include -k/-M/-s (those are
extensions) and they are correctly absent. Real defects: the stdout PID format
does not match the spec's " %1d", the /proc/<pid>/maps device-number formula
uses obsolete 8-bit minor encoding, and a /proc scan error hard-exits past
the error path. The widely-assumed "exit 1 when nothing matches" is not a
POSIX requirement and is correctly not implemented.
-
#F1— stdout PID format diverges from" %1d". ✓ fixed in Phase 7.fuser.rs:1435-1438. Spec (slice 98794-98795): "On standard output, the process ID in the format:" %1d", <process ID>" — exactly one leading space. The code useslet width = if pid.to_string().len() > 4 { " " } else { " " }→ two spaces for ≤4-digit PIDs. Fix:print!(" {}", pid)unconditionally. -
#F2—/proc/<pid>/mapsdevice number uses 8-bit minor encoding. ✓ fixed in Phase 7 (libc::makedev).fuser.rs:752(tmp_maj * 256 + tmp_min). Modern Linux uses the splitmakedevencoding (minors > 255); this yields wrong device IDs on LVM / high minors → false negatives. Fix:libc::makedev(tmp_maj, tmp_min). -
#F3—scan_procserror hard-exits, bypassing the error path. ✓ fixed in Phase 7.fuser.rs:368-380callsstd::process::exit(1)directly instead of propagating theErrtomain. Exit is incidentally >0 (POSIX-OK) but the abrupt exit skips flush/cleanup. Fix: propagate with?.
-
#F4—read_proc_mountscan panic on a malformed line. ✓ fixed in Phase 7.fuser.rs:833indexesparts[1]without a bounds check. Kernel-generated/proc/mountsis normally well-formed, but a short line panics. Useparts.get(1)and skip. -
#F5— macOSpid.try_into().unwrap(). ✓ fixed in Phase 7.fuser.rs:1351..unwrap()on/proc-style data is a code smell; usepid as i32. -
#F6—Access::File/Access::Filewruse-chars dropped. ✓ fixed in Phase 7.fuser.rs:1421(_ => ()). POSIX mandates onlycandr;f/Fare "may", so this is a behavioral gap vs GNUfuser, not a spec violation. -
#F7— detached timeout thread leak. ✓ addressed in Phase 7 (self-reaps on stat completion; noise removed; full cancellation not portable).fuser.rs:1484. The NFS-stall guard thread is detached and keeps running on timeout. -
#C1— diagnostics hardcoded English.fuser.rs— ✓ fixed in Phase 7 (plib::diag).
-
exit status must be >0 when no process uses any file— re-examined: POSIXfuserEXIT STATUS (slice 98857-98860) is only "0 Successful completion / >0 An error occurred." There is no "1 if nothing matched" requirement; that is GNU/BSD behavior.process::exit(0)atfuser.rs:1537on a no-match success is POSIX-conformant. Not actionable.
| Area | Status | Notes (file:line) |
|---|---|---|
SYNOPSIS fuser [-cfu] file... |
CONFORMS | clap; file required fuser.rs:1376 |
-k/-M/-s absent |
CONFORMS | not in POSIX.1-2024 base form |
-c mount-point scan |
CONFORMS | fuser.rs:1129-1132 |
-f named-files |
CONFORMS | fuser.rs:1129 |
-u (username) + uid fallback |
CONFORMS | fuser.rs:1442-1456 |
| PIDs → stdout; rest → stderr | CONFORMS | print! vs eprint! split |
PID format " %1d" |
DIVERGES | #F1 |
| stderr write-order (98810: info first, then all PIDs) | PARTIAL | interleaved per-pid; correct only for same-file case |
| EXIT 0 / >0 | CONFORMS | (no-match=0 is OK, see refuted) |
| device-number decode | DIVERGES | #F2 |
| ASYNCHRONOUS EVENTS | CONFORMS | "Default" |
| i18n diagnostics | PARTIAL | #C1 |
Present: open-fd detect, -u username, TCP/UDP/unix (compare vs system fuser).
-
-cmount-point;-fblock-device; multiple operands - PID format (
" %1d") assertion (proves#F1) -
r(root-dir) use-char; high-minor device file (proves#F2)
Implementation: process/kill.rs (137 lines) + shared process/signal.rs
Tests: process/tests/kill/mod.rs (392 lines, ~25 tests)
Spec: POSIX.1-2024, Vol. 3 §3, kill (slice incl. lines 101500–101560)
The cleanest utility in the crate. All four invocation forms work, the full
mandatory signal-name set is present on both platforms, pid 0 / negative-pid /
signal-0 semantics are delegated correctly to libc::kill, per-pid failures
continue the loop, and the EXIT STATUS is right. The agent's headline
"kill -l 6 prints IOT not ABRT" finding was refuted. Only Minor polish
items remain (i18n, list-format, a -l -opt edge case).
-
#K1—kill -lemits one space-joined line. ✓ fixed in Phase 8 (explicit per-signal%s%c).signal.rs:13-15(names.join(" ")+ oneprintln!). POSIX formats each name"%s%c"with a space/newline separator, last = newline. The current output (all spaces, final newline) is within the letter of "separator shall be a<space>or<newline>", but a multi-line per-signal emit is the conventional form. Cosmetic. -
#K2—-lfollowed by an option mis-parses. ✓ fixed in Phase 8.kill.rs:36-51.kill -l -s TERM …treats-sas theexit_statusoperand →"-s".parse::<i32>()fails → "Invalid exit status". Fix: stop treating the next token asexit_statusif it begins with-. -
#C1— diagnostics hardcoded English (kill/signal). ✓ fixed in Phase 8 (plib::diag);signal.rs&'static strreturns ("Invalid PID","Unknown signal name").
-
— re-examined: the Linuxkill -l 6printsIOT; Linux table is missingABRTSIGLISTdoes contain("ABRT", 6)atsignal.rs:121, before("IOT", 6)atsignal.rs:122.signum_to_namereturns the first match, sokill -l 6→ABRT. TheIOTalias is a harmless extra entry. Not a defect. -
— re-examined: shell status for signal N is-l exit_status128+N boundary at exactly 128128+Nfor N≥1, sosignum > 128(signal.rs:21) matches the spec example. Conformant. -
negative-pid operand rejected— re-examined:"-165".parse::<i32>()succeeds (kill.rs:57,74); negative pids parse fine. Conformant.
| Area | Status | Notes (file:line) |
|---|---|---|
-s signal_name pid… |
CONFORMS | kill.rs:47; SIG-prefix/case-insensitive signal.rs:45-46,60-61 |
-l [exit_status] |
CONFORMS (fmt cosmetic #K1) |
kill.rs:36-51,108-110 |
-signal_name / -signal_number (XSI) |
CONFORMS | kill.rs:55-60 |
-- end-of-options |
CONFORMS | kill.rs:52-54 |
| Mandatory signal-name set (ABRT…USR2 + RT) | CONFORMS | signal.rs:80-148 (both OSes) |
signal 0 (existence check only) |
CONFORMS | signal.rs:57-59; libc::kill(pid,0) |
| pid 0 / negative pid (process group) | CONFORMS | delegated to libc::kill |
| Per-pid failure continues; EXIT >0 | CONFORMS | kill.rs:95-103 |
| EXIT 0 success | CONFORMS | kill.rs:93,129 |
| i18n diagnostics | PARTIAL | #C1 |
Strong (delivery, -l numeric/128+N, signal 0, --, multiple pids). Gaps:
-
-lexact%s%cformat; actual negative-pid (process-group) delivery -
-l -s …edge case (proves#K2)
Implementation: process/nice.rs (70 lines)
Tests: none
Spec: POSIX.1-2024, Vol. 3 §3, nice (slice lines 108426–108540)
Two genuine Criticals: a failed nice() aborts instead of still invoking the
utility (the spec's central guarantee), and exec failures never produce 126/127.
The nice() return-value check (res < 0) is also wrong — -1 is a legal new
nice value — and the increment is artificially clamped to [-30, 29] by clap.
-
#NC1— a failednice()must NOT prevent invoking the utility. ✓ fixed in Phase 2.nice.rs:60-65. Spec (slice 108435-108437): if the user lacks privilege the nice value "shall not [be] affect[ed]… but this shall not prevent the invocation of utility or affect the exit status." The code doesif res < 0 { eprintln!; return Err(...) }→ exits without exec. Fix: onEPERM, optionally warn, then fall through toexec_util. -
#NC2— exec failure never yields 126/127. ✓ fixed in Phase 2.nice.rs:44-51,67. Same?-propagation defect as#E1:Command::exec()error bubbles out ofmain, exiting 1. Spec (slice 108484-108486 + the 108504 rationale) mandates 127/126. Fix: matchNotFound→127, other exec err→126, internal→1-125.
-
#NC3— increment clamped to[-30, 29]by clap. ✓ fixed in Phase 2.nice.rs:27(.range(-30..30)). POSIX places no fixed range on the-nargument — the kernel'snice(3)clamps the result.nice -n 40 cmd(historical) andnice -n -20 cmdare rejected at parse time. Fix: drop.range(). -
#NC4—nice()return checked asres < 0. ✓ fixed in Phase 2.nice.rs:60-61.nice(3)returns the new nice value;-1is legal (root, increment to -1) yet trips the error path. Fix: clearerrno, call, then testerrno.
-
#NC5—--nicevallong option is non-POSIX. ✓ fixed in Phase 2.nice.rs:23-35.-nis correct; the derived--nicevallong form is an unspecified extension. -
#C1— diagnostic atnice.rs:63not gettext-wrapped. ✓ fixed in Phase 2 (plib::diag).
| Area | Status | Notes (file:line) |
|---|---|---|
-n increment, default 10 |
CONFORMS (range bug #NC3) |
nice.rs:23-27 |
utility [argument…] exec |
CONFORMS | nice.rs:44-51 |
| Failed nice still execs utility | DIVERGES | #NC1 |
| EXIT utility status | CONFORMS (success path) | exec replaces image |
| EXIT 126 / 127 | MISSING | #NC2 |
setlocale/textdomain |
CONFORMS | nice.rs:54-56 |
| i18n diagnostics | PARTIAL | #C1 |
| ASYNCHRONOUS EVENTS | CONFORMS | "Default" |
- default increment runs;
-n 5applied; unprivileged negative increment still execs (proves#NC1);nonexistent→127; non-exec file→126;-n 40accepted (proves#NC3)
Implementation: process/nohup.rs (145 lines)
Tests: none
Spec: POSIX.1-2024, Vol. 3 §3, nohup (slice lines 108886–108996)
SIGHUP is ignored before exec, the nohup.out (cwd → home fallback) path
exists, and the 126/127 exit mapping is correct. But the created file is not
mode 0600 (uses umask), the home fallback reads dirs::home_dir() instead of
$HOME, the stderr-redirect logic misses the spec's "redirect to the same open
file description as stdout" case, and a file-open failure panic!s (exit 101)
instead of the controlled error path.
-
#NH1—nohup.outnot created mode 0600. ✓ fixed in Phase 4.nohup.rs:26-40.OpenOptions::new().create(true).append(true)honors umask (→ 0644/0666). Spec (slice 108896) mandatesS_IRUSR | S_IWUSR(0600) on creation. Fix:OpenOptionsExt::mode(0o600). -
#NH2— home fallback usesdirs::home_dir(), not$HOME. ✓ fixed in Phase 4.nohup.rs:34. Spec (slice 108916) names theHOMEenvironment variable.dirs::home_dir()consultsgetpwuidand may ignore a set$HOME. Fix:std::env::var("HOME").
-
#NH3— stderr redirect misses the "follow stdout fd" case. ✓ fixed in Phase 4.nohup.rs:106-113. Spec (slice 108897-108900): "If standard error is a terminal and standard output is open but is not a terminal, all output … to its standard error shall be redirected to the same open file description as the standard output." The code only redirects stderr to a freshly-openednohup.out; when stdout is a non-terminal pipe/file it shoulddup2stdout's fd onto stderr instead. Fix: branch on stdout's terminal/open state per spec. -
#NH4— file-open failure panics (exit 101) instead of erroring. ✓ fixed in Phase 4.nohup.rs:81-82(.expect(...)). Spec APPLICATION USAGE expects an internal failure to exit 127 with a diagnostic, not a Rust panic backtrace. Fix: handle theErrandexit(127).
-
#NH5—signal(SIGHUP, SIG_IGN)return value unchecked. ✓ fixed in Phase 4.nohup.rs:57-60. ASIG_ERRreturn goes undetected. Check andexit(127). -
#NH6— "appending output" notice wording/format. ✓ fixed in Phase 4.nohup.rs:94-101. Functional and correctly gated onstdout().is_terminal(), but the message text is bespoke (not gettext-wrapped —#C1).
| Area | Status | Notes (file:line) |
|---|---|---|
nohup utility [argument…] |
CONFORMS | nohup.rs:70-77 (missing-operand → 127) |
| SIGHUP set SIG_IGN before exec | CONFORMS (unchecked #NH5) |
nohup.rs:57-60 |
| stdout terminal → append nohup.out | CONFORMS | nohup.rs:84-90 (O_APPEND) |
| nohup.out mode 0600 | MISSING | #NH1 |
HOME fallback via $HOME |
DIVERGES | #NH2 |
| stderr → same fd as stdout (stdout open non-tty) | DIVERGES | #NH3 |
| "appending" notice → stderr | CONFORMS (wording #NH6) |
nohup.rs:94-101 |
| EXIT 127 not found / 126 cannot-invoke | CONFORMS | nohup.rs:133-142 |
| EXIT utility status | CONFORMS | nohup.rs:118 |
| File-open failure handling | DIVERGES (panic) | #NH4 |
| i18n diagnostics | PARTIAL | #C1 |
- stdout-tty → nohup.out created mode 0600 in cwd (proves
#NH1) - cwd unwritable →
$HOME/nohup.outhonoring a set$HOME(proves#NH2) - stdout=pipe, stderr=tty → stderr follows stdout fd (proves
#NH3) - SIGHUP ignored in child; not-found→127; non-exec→126; status passthrough
Implementation: process/renice.rs (137 lines) + plib::priority
Tests: none
Spec: POSIX.1-2024, Vol. 3 §3, renice (slice lines 113200–113330)
The core algorithm is right: relative increment (getpriority + add + clamp +
setpriority), -g/-p/-u mutually exclusive, username→uid via getpwnam.
But it accepts only a single ID where POSIX requires ID..., so per-ID
error continuation is structurally impossible; numeric ID 0 is wrongly
rejected; and the -n range excludes +20.
-
#RN1— only oneIDaccepted; POSIX requiresID.... ✓ fixed in Phase 3.renice.rs:67(id: String). The SYNOPSIS isrenice [-g|-p|-u] -n increment ID...— one or more. A second operand makes clap error "unexpected argument". Fix:id: Vec<String>+ loop. -
#RN2— no per-ID error continuation. ✓ fixed in Phase 3.renice.rs:125-134. CONSEQUENCES OF ERRORS = "Default": a failure on one ID must not stop the others, and exit must be >0 if any failed. The?chain aborts on the first error (and only one ID exists anyway). Fix: loop, accumulate failures, exit non-zero at end.
-
#RN3— numeric ID0rejected. ✓ fixed in Phase 3.renice.rs:85(Ok(0) => Err("Invalid ID")).0is valid: PID/PGID 0 = caller's process/group, UID 0 = root. Fix: remove theOk(0)reject arm. -
#RN4—-nrange excludes+20. ✓ fixed in Phase 3.renice.rs:27(.range(-20..20), exclusive upper). Historical max increment is+20(spec EXAMPLES cite 19/20). Fix:.range(-20..=20).
-
#C1— diagnostics atrenice.rs:89,95not gettext-wrapped. ✓ fixed in Phase 3 (plib::diag).
-
— re-examined:-nis--nicevalnot-n#[arg(short)]on fieldnicevalderives-ncorrectly (renice.rs:23-34); the--nicevallong form is an unspecified extension only. Folded into#NC5-style note, not a defect. -
obsolescent positional— that form is no longer specified by POSIX.1-2024 (RATIONALE); N/A, not a gap.renice priority … ID…form missing
| Area | Status | Notes (file:line) |
|---|---|---|
-n increment (relative) |
CONFORMS (range #RN4) |
renice.rs:23-34,131 (prio + niceval) |
-g/-p/-u mutually exclusive |
CONFORMS | group="mode" renice.rs:38,47,58 |
-p default |
CONFORMS | renice.rs:113-121 |
-u name or numeric uid |
CONFORMS | renice.rs:83-101 (getpwnam) |
ID... (≥1) |
DIVERGES | #RN1 |
ID 0 accepted |
DIVERGES | #RN3 |
| clamp to PRIO_MIN/MAX | CONFORMS | renice.rs:131 |
| per-ID continue + EXIT >0 | MISSING | #RN2 |
| EXIT 0 success | CONFORMS | renice.rs:136 |
| i18n diagnostics | PARTIAL | #C1 |
- single PID; multiple PIDs in one call (proves
#RN1);-g;-u name;-u 0; partial-failure continuation (proves#RN2);-n 20accepted (proves#RN4); ID0accepted (proves#RN3)
Implementation: process/timeout.rs (489 lines) + shared signal.rs
Tests: process/tests/timeout/mod.rs (320 lines, broad)
Spec: POSIX.1-2024, Vol. 3 §3, timeout (slice lines 117540–117670)
The most complete utility here: duration grammar (suffix + fraction), the full
124/125/126/127 exit map, -k/-s/-p/-f, and signal forwarding all work
for the common case. Three real defects: alarm(2) truncates sub-second
durations to whole seconds; the child unconditionally resets SIGTTIN/SIGTTOU to
SIG_DFL (spec: inherit timeout's own disposition); and only 5 signals are
forwarded where the spec says any terminate-default signal must be.
-
#T1— fractional/sub-second duration truncated byalarm(). ✓ fixed in Phase 5.timeout.rs:95(libc::alarm(duration.as_secs() as c_uint)).as_secs()drops the fraction, sotimeout 0.5s …arms a 0-second alarm (never fires) andtimeout 1.9s …fires at 1s. The parser correctly accepts fractions (slice 117549-117556), but the timer discards them. Fix:setitimer(ITIMER_REAL)(ortimerfd) with microsecond precision. Also affects-k(timeout.rs:136). -
#T2— child resets SIGTTIN/SIGTTOU toSIG_DFLunconditionally. ✓ fixed in Phase 5.timeout.rs:384-385(inpre_exec). Spec (slice 117593-117598): the child's dispositions "shall be the same as the disposition that timeout inherited," except the-ssignal. If timeout was launched with SIGTTIN/SIGTTOU ignored (e.g. under another timeout/nohup), the child must inherit ignored, not default. Fix: save the original disposition before theSIG_IGNinstall attimeout.rs:363-364and restore that inpre_exec.
-
#T3— only 5 signals forwarded to the child. ✓ fixed in Phase 5.timeout.rs:212-246forwards SIGALRM/SIGINT/SIGQUIT/SIGHUP/SIGTERM (+-s). Spec (slice 117587-117591): forward the-ssignal or any signal whose default action is to terminate. SIGPIPE/SIGUSR1/SIGUSR2/SIGXCPU/SIGVTALRM/SIGPROF etc. are omitted — a SIGPIPE to timeout kills it silently, orphaning the child. Fix: register the full terminate-default set. -
#T4—WCOREDUMPbranch exits 125 instead of re-raising. ✓ fixed in Phase 5.timeout.rs:436-439. On a core-dumping child, timeout returns 125 rather than mirroring the child's signal death (with core suppressed). Fix: fall through to theraise(signal)path.
-
#T5— SIGCONT after stop sent only to child PID. ✓ fixed in Phase 5.timeout.rs:420-422— in non-foreground mode it should also reach the process group. -
#T6— race: signal beforeMONITORED_PIDis set. ✓ fixed in Phase 5.timeout.rs:131exits128+signalif a signal lands between handler setup (:361) and PID store (:405). Use a-1sentinel. -
#C1— diagnostics not gettext-wrapped. ✓ fixed in Phase 5 (plib::diag).
| Area | Status | Notes (file:line) |
|---|---|---|
[-fp] [-k time] [-s sig] duration utility … |
CONFORMS | timeout.rs:32-50 |
| duration suffix s/m/h/d + fraction parse | CONFORMS | timeout.rs:68-85 |
| duration timer honors fraction | DIVERGES | #T1 |
0 disables timeout |
CONFORMS | timeout.rs:94 |
-k second-stage SIGKILL |
CONFORMS (truncation #T1) |
timeout.rs:135-136 |
-s (case/prefix-insensitive, default TERM) |
CONFORMS | timeout.rs:41, signal.rs:45-46 |
-f/-p semantics |
CONFORMS | timeout.rs:351-358,440-458 |
| Forward terminate-default signals | PARTIAL | #T3 |
| Briefly ignore signal during group-kill | CONFORMS (narrow race) | timeout.rs:107,141-148 |
| Child SIGTTIN/SIGTTOU disposition inherited | DIVERGES | #T2 |
| EXIT 124/125/126/127, status passthrough | CONFORMS | timeout.rs:346,394,399,401,432,455 |
WCOREDUMP re-raise |
DIVERGES | #T4 |
| i18n diagnostics | PARTIAL | #C1 |
Broad (124/125/126/127, suffixes, -p/-f, kill). Gaps:
- sub-second duration precision (proves
#T1);-kfractional time - child inherits ignored SIGTTIN/SIGTTOU (proves
#T2) - forwarding SIGUSR1/SIGPIPE to the timeout process (proves
#T3)
Implementation: process/xargs.rs (701 lines)
Tests: process/tests/xargs/mod.rs (545 lines, broad)
Spec: POSIX.1-2024, Vol. 3 §3, xargs (slice lines 123150–123320)
The option surface (-ptx -E -I -L -n -s -0), the quote/backslash word-splitter,
and the 126/127 exit map are largely implemented and well-tested. Two genuine
defects: the POSIX.1-2024 -r option is missing and the empty-input case
runs the utility zero times where POSIX mandates exactly once; and the
byte-to-char cast in the word-splitter corrupts multibyte UTF-8 arguments.
-
#X1—-rmissing; empty input invokes 0× (POSIX: exactly 1×). ✓ fixed in Phase 6.xargs.rs(no-rfield) +xargs.rs:680(else if !state.args.is_empty()). Spec (slice 123180-123182): "If no arguments are supplied on standard input, the utility … shall be executed exactly once if the −r option is not specified," and-r(slice 123225) suppresses that. The impl has no-rand never runs the utility on empty input — the inverse of the default. Fix: add-r; when absent and input is empty, invoke once with the fixed args. (Testxargs_empty_inputcurrently encodes the wrong expectation.)
-
#X2—c8 as charcorrupts multibyte UTF-8. ✓ fixed in Phase 6.xargs.rs:367,452. Casting au8≥ 0x80 tocharyields the Latin-1 scalar, not the decoded multibyte codepoint, soé(0xC3 0xA9) becomes two wrong chars. POSIX requiresLC_CTYPE-aware splitting. Fix: decode buffers viastr::from_utf8/chars(). -
#X3— newline inside quotes silently accepted. ✓ fixed in Phase 6.xargs.rs:369-378. Spec allows only "non-double-quote non-<newline>" chars inside double-quotes (likewise single). A quoted newline falls through to the generic push instead of erroring. Fix: error onin_quote && ch == '\n'. -
#X4—-EEOF string ignored in-Iinsert mode. ✓ fixed in Phase 6.xargs.rs:616-620—postprocess(theeofstrcheck) runs only on the non-insert path. Fix: apply the EOF check in insert mode too.
-
#X5—-pprompt format. ✓ fixed in Phase 6.xargs.rs:133fuses command +?...in oneeprint!; the affirmative test is ASCIIy/Yrather than the localeyesexpr. (/dev/ttyread atxargs.rs:137is correct.) -
#C1— error strings not gettext-wrapped. ✓ fixed in Phase 6 (plib::diag).
-
signal-killed child must exit 125 and stop— re-examined: POSIXxargsEXIT STATUS (slice 123300-123306) defines only 0 / 1-125 / 126 / 127, with no 125-for-signal rule and no stop-on-signal mandate.exit_code_from_statuscomputes128+sig(xargs.rs:196) buthandle_exec_result!(xargs.rs:550-552) folds any non-zero non-255 code intoany_failed, andmainreturns 1 (xargs.rs:688) — inside the conformant 1-125 band. No out-of-range code leaks. Not a defect. -
exit-255 must print a diagnostic— POSIX has no 255/124 rule; returning 1 (xargs.rs:549) is conformant. Not actionable.
| Area | Status | Notes (file:line) |
|---|---|---|
-p -t -x |
CONFORMS | xargs.rs:92-108,162-164 |
-E eofstr / -0 |
CONFORMS (insert-mode #X4) |
xargs.rs:77-82,98-99,503-508 |
-I replstr (implies -x, per-line) |
CONFORMS | xargs.rs:84-90,238,512-541 |
-L number (cont. on trailing blank) |
CONFORMS | xargs.rs:47-55,396-418 |
-n number |
CONFORMS | xargs.rs:57-65,273-276 |
-s size (ARG_MAX-derived default) |
CONFORMS | xargs.rs:67-74,253 |
-r |
MISSING | #X1 |
| empty input → invoke once | DIVERGES (0×) | #X1 |
| default utility = echo | CONFORMS | xargs.rs:110 |
| word-split: blanks/quotes/backslash | CONFORMS (UTF-8 #X2, nl-in-quote #X3) |
xargs.rs:330-494 |
-p → /dev/tty |
CONFORMS (fmt #X5) |
xargs.rs:137 |
| EXIT 0 / 1-125 / 126 / 127 | CONFORMS | xargs.rs:549-558,688 |
| i18n diagnostics | PARTIAL | #C1 |
Broad (n/s/L/I/E/0/x, quoting, escapes, 126/127/255). Gaps:
-
-r+ empty input invoke-once (proves#X1; fixxargs_empty_input) - multibyte UTF-8 argument round-trip (proves
#X2); newline-in-quotes error -
-E+-Iinteraction (proves#X4)
| Util | Critical | Major | Minor | Refuted |
|---|---|---|---|---|
env |
1 (#E1) |
2 (#E2,#E3) |
2 | 1 (-u) |
fuser |
0 | 3 (#F1–#F3) |
4 + #C1 |
1 (exit-on-no-match) |
kill |
0 | 0 | 2 + #C1 |
3 |
nice |
2 (#NC1,#NC2) |
2 (#NC3,#NC4) |
1 + #C1 |
0 |
nohup |
2 (#NH1,#NH2) |
2 (#NH3,#NH4) |
2 + #C1 |
0 |
renice |
2 (#RN1,#RN2) |
2 (#RN3,#RN4) |
#C1 |
2 (N/A) |
timeout |
2 (#T1,#T2) |
2 (#T3,#T4) |
2 + #C1 |
0 |
xargs |
1 (#X1) |
3 (#X2–#X4) |
1 + #C1 |
2 |
Crate-wide: 10 Critical, 16 Major, plus the #C1 i18n theme touching all 8.
Zero test coverage for env, nice, nohup, renice.
- PR A — "exec-launcher exit-status 126/127" (
#E1,#NC2): shared pattern;env+niceshould map exec errors likenohup/timeout/xargsalready do (#C3). Addnonexistent→127 / non-exec→126 tests. - PR B — "nice honors the POSIX invoke-anyway guarantee" (
#NC1,#NC3,#NC4): drop the?-abort, fix theerrnocheck, remove the clap range. - PR C — "renice accepts
ID...and continues per-ID" (#RN1–#RN4):Vec<String>operand, loop with error accumulation, accept ID 0,-20..=20. - PR D — "nohup file semantics" (
#NH1–#NH4): 0600 mode,$HOME, stderr-follows-stdout, no-panic on open failure. - PR E — "timeout sub-second + signal fidelity" (
#T1–#T4):setitimer, inherit SIGTTIN/SIGTTOU, full forwarding set, WCOREDUMP re-raise. - PR F — "xargs
-r+ empty-input + UTF-8" (#X1–#X4): add-r, invoke once on empty input (fixxargs_empty_input), decode UTF-8, reject quoted nl. - PR G — "fuser output + /proc robustness" (
#F1–#F5):" %1d"format,makedev, propagate scan errors, bounds-check/proc/mounts. - PR H — "env/kill polish" (
#E2–#E5,#K1,#K2): clap surface,-lformat/edge. - PR I — "crate-wide i18n" (
#C1): route every runtime diagnostic throughgettext(); consider a sharedplib-style diag helper (mirrors thedev/crate'splib::diagconsolidation).