Skip to content

Fix bugs with process spawning on Linux/macOS - #2536

Open
mdsitton wants to merge 10 commits into
beefytech:masterfrom
mdsitton:fix/posix-spawn-workingdir
Open

mdsitton wants to merge 10 commits into
beefytech:masterfrom
mdsitton:fix/posix-spawn-workingdir

Conversation

@mdsitton

Copy link
Copy Markdown
Contributor

This fixes bugs in BfpSpawn on Linux, macOS and Android, the code behind SpawnedProcess. Windows isn't changed.

  • Spawning with a working directory changed the directory of the whole program. The code tried to change it back afterwards, but that didn't work, so the parent process stayed in the child's folder. It now only changes the directory in the new process.
  • Pipes leaked into other processes. If two processes were started around the same time, the second one could get a copy of the first one's pipes. Then the first process never saw its input end and could hang.
  • The parent's own stdin could get closed. Asking for an output stream that wasn't redirected (or asking for the same stream twice) handed out fd 0, and closing it closed the program's stdin. Now you get an error instead.
  • Pipes you never used were never closed. A process with a redirected stdin that you never wrote to would wait forever. They're now closed when the spawn is released.
  • Leaks when spawning failed. If creating a pipe or forking failed, the pipes and argument strings were leaked.
  • Unsafe code in the child process. After forking, the child could call exit() and fprintf, which can print the parent's output twice or deadlock. It now uses _exit() and plain write(). The "couldn't execute" error message also shows up now; before, it was printed after stderr had been closed.
  • Small cleanups: the child closed the wrong stderr pipe end, and some unused variables and dead commented-out code were removed.

The first commit adds tests to corlib (SpawnedProcessTests, non-Windows only). Four of them fail on master, and each fix commit makes its own test pass, so you can follow the series commit by commit. All 54 corlib tests pass at the end.

Some additional notes:

  • Tested on Linux only. The macOS path (it uses fcntl instead of pipe2) hasn't been built or run.
  • Doesn't seem like the GitHub CI is currently running run corlib's tests on any platform?

Covers the working directory applying only to the child, a missing working
directory failing Start, a redirected stdin/stdout/stderr round trip,
redirected pipes not leaking into other spawned processes, only redirected
streams being attachable (and only once), and Close releasing a stdin pipe
the caller never took.

Several of these fail against the current POSIX BfpSpawn implementation;
the following commits fix them.
BfpSpawn_Create called chdir(workingDir) in the parent before forking and
tried to restore the previous directory afterwards, but getcwd was called
after the chdir so the 'restore' was a no-op. Every spawn with a working
directory permanently moved the parent's cwd, and even a correct restore
would race with other threads resolving relative paths.

Validate the directory up front in the parent (preserving the synchronous
error result) and perform the chdir in the child before exec, matching the
Windows behavior.
After dup2'ing the stderr pipe onto STDERR_FILENO the child closed
stdErrFD[0] (already closed) instead of stdErrFD[1], leaking an extra
write end of the pipe into the exec'd process.
If exec (or the working directory chdir) fails in the forked child, exit()
would run the parent's atexit handlers and flush stdio buffers inherited
from the parent, duplicating any pending parent output. Use _exit instead.
The stdio redirection pipes were created without close-on-exec, so any
process spawned concurrently (or later, for the long-lived stdin write
end) inherited them. A child whose stdin write end leaked into another
process would never see EOF when the parent closed its stdin, and stdout
and stderr readers could likewise block until the unrelated process exited.

Create the pipes close-on-exec (pipe2 where available, fcntl otherwise).
dup2 clears the flag on the redirected std handle; the case where the pipe
end already is the target handle is handled explicitly.
The forked child reported chdir/exec failures with fprintf, which is not
async-signal-safe and may deadlock after fork in a multithreaded parent.
The exec failure message was also never seen, because stdout, stderr and
stdin were closed just before printing it. Write the messages directly to
STDERR_FILENO instead and drop the redundant closes (_exit closes them).
BfpSpawn used 0 to mean 'no pipe' for streams that weren't redirected or
whose handle was already taken. On POSIX fd 0 is the parent's stdin, so
BfpSpawn_GetStdHandles wrapped it in a BfpFile and releasing that file
closed the parent's stdin; the next open or pipe then reused fd 0.

Use -1 for 'no pipe' and return NULL from BfpSpawn_GetStdHandles in that
case, which the existing callers already check for.
Pipe ends that were redirected but never retrieved through
BfpSpawn_GetStdHandles were never closed. Besides leaking the fds, a
leaked stdin write end left the child blocked waiting for input forever.
Close them on release, matching what ~BfpSpawn already does on Windows.
If creating one of the redirection pipes or the fork failed, the pipes
already created and the strdup'd argv strings were leaked. Close any
created pipe ends and free argv before returning the error.
Drop the unused shell-execute verb (still stripping the |verb suffix from
the target path), a redundant copy of the target path, an unused argv
variable, the abandoned commented-out posix_spawn call, and a commented-out
printf that referenced its no-longer-existing status variable.
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.

1 participant