Skip to content

An exec failure after the fork is a runtime abort, not a spawn error (a missing #! interpreter shows killed by signal: Aborted) #563

Description

@vyncint

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.

  1. 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.
  2. 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.
  3. 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.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions