What happens. When the program exists and is executable but exec still fails, the child does not become an Error::Spawn. It dies of a Rust runtime abort, and the abort's message is what the screen shows:
$ printf '#!/nonexistent/interpreter\necho hi\n' > badinterp; chmod +x badinterp
$ termlens inspect ./badinterp
size: 80x24 cursor: 1,0
fatal runtime error: assertion failed: output.write(&bytes).is_ok(), aborting
…
--- exited: killed by signal: Aborted ---
Exit 0 from inspect, because it did run; the library sees the same thing through Terminal::spawn, which returns Ok and a child that has already aborted. The shape that reaches it in practice is a #! line naming an absolute interpreter path the machine lacks, such as #!/usr/local/bin/python3. A #!/usr/bin/env python3 script is not affected: env exists, so the exec succeeds, and env reports the missing interpreter itself (env: 'python3': No such file or directory, exit 127 — checked). For comparison, a file without the execute bit is caught before the fork and reported cleanly (failed to spawn ./noexec: … because it is not executable), and a script with no shebang runs (the system falls back to sh).
Cause. std::process::Command reports an exec failure from the forked child through a close-on-exec pipe; the child writes the errno there and _exits. portable-pty 0.9's pre_exec hook calls close_random_fds() (portable-pty-0.9.0/src/unix.rs:276), which closes every descriptor above 2 — that pipe included. When exec then fails, std's write to the pipe fails and its rtassert! aborts the child. The pipe is close-on-exec already, so closing it buys nothing on the success path.
The relative current_dir case fixed in #561 reached this same abort, which is how it was found; that one is fixed at its source. This issue is the general path, which the fix there does not cover.
Options.
- Upstream: portable-pty's
close_random_fds could skip descriptors already marked FD_CLOEXEC (they close at exec anyway), which keeps std's error pipe alive. This is the real fix and benefits every portable-pty user.
- Here, as a stopgap: before spawning, read a
#! line and refuse with Error::Spawn if its interpreter path does not exist. This covers the shape above and nothing else.
- Here, after the fact: recognise a child killed by
SIGABRT within moments of spawning whose screen begins fatal runtime error: assertion failed: output.write(&bytes) and report it as a spawn failure. Fragile; listed for completeness.
Found while verifying #561; not part of the 0.11.5 release.
What happens. When the program exists and is executable but
execstill fails, the child does not become anError::Spawn. It dies of a Rust runtime abort, and the abort's message is what the screen shows:Exit 0 from
inspect, because it did run; the library sees the same thing throughTerminal::spawn, which returnsOkand a child that has already aborted. The shape that reaches it in practice is a#!line naming an absolute interpreter path the machine lacks, such as#!/usr/local/bin/python3. A#!/usr/bin/env python3script is not affected:envexists, so the exec succeeds, andenvreports the missing interpreter itself (env: 'python3': No such file or directory, exit 127 — checked). For comparison, a file without the execute bit is caught before the fork and reported cleanly (failed to spawn ./noexec: … because it is not executable), and a script with no shebang runs (the system falls back tosh).Cause.
std::process::Commandreports anexecfailure from the forked child through a close-on-exec pipe; the child writes the errno there and_exits. portable-pty 0.9'spre_exechook callsclose_random_fds()(portable-pty-0.9.0/src/unix.rs:276), which closes every descriptor above 2 — that pipe included. Whenexecthen fails, std's write to the pipe fails and itsrtassert!aborts the child. The pipe is close-on-exec already, so closing it buys nothing on the success path.The relative
current_dircase fixed in #561 reached this same abort, which is how it was found; that one is fixed at its source. This issue is the general path, which the fix there does not cover.Options.
close_random_fdscould skip descriptors already markedFD_CLOEXEC(they close atexecanyway), which keeps std's error pipe alive. This is the real fix and benefits every portable-pty user.#!line and refuse withError::Spawnif its interpreter path does not exist. This covers the shape above and nothing else.SIGABRTwithin moments of spawning whose screen beginsfatal runtime error: assertion failed: output.write(&bytes)and report it as a spawn failure. Fragile; listed for completeness.Found while verifying #561; not part of the 0.11.5 release.