Skip to content

[Bug]: the subprocess timeout is not a bound when the child has children of its own #583

Description

@thcp

What happens

child_output_with_timeout in desktop/src-tauri/src/main.rs is supposed to
give up on a wedged subprocess after a fixed timeout. It does not, when the
child spawned children of its own. It waits for as long as the grandchild lives,
whatever the timeout said.

a_gpu_probe_that_never_returns_is_given_up_on catches this. It asks for a
2 second timeout and asserts the call returns inside 30 seconds. It takes 300.
The Linux Rust Check on #569 has been failing on it since 8dc1577.

Why

The timeout branch kills the child and then joins both reader threads:

let _ = child.kill();
let _ = child.wait();
// Joined rather than detached: killing the child closes its ends,
// so the readers finish, and dropping the handles without joining
// would leak two threads per timeout.
let _ = collect(stdout_reader);
let _ = collect(stderr_reader);

The comment is wrong. Killing the child closes only the handles the child
itself held. A grandchild inherited the same pipe write ends, so the pipe stays
open and read_to_end in the reader thread keeps blocking. Joining it is
waiting for the grandchild.

The test's stand-in makes this concrete: sh -c "sleep 300" forks on Ubuntu
rather than exec-ing, so there really are two processes. Killing sh leaves
sleep holding the stderr pipe for its full 300 seconds, and the join waits it
out.

Why it matters beyond the test

This is the same failure #502 is about, one level down. run_pip_install uses
this path with a 20 minute timeout, so a wedged pip whose child outlives it
would hang setup rather than being given up on, with the timeout offering no
protection at all. The bound exists precisely so setup cannot stop forever.

Constraints

  • The timeout has to be hard. Nothing on that path may wait on something whose
    lifetime we do not control.
  • Not joining is not a leak worth worrying about: the reader thread ends by
    itself when the last writer closes the pipe. That is the same instant the
    join would have returned, minus the part where it blocks the caller.
  • The non-timeout path must keep joining. There the child has exited, the reader
    is at EOF, and the collected output is the return value.
  • Killing the whole process group so grandchildren die too is a separate and
    larger change (process_group plus killpg on Unix, a Job Object on
    Windows). None of the real callers here spawn grandchildren -- the GPU probe
    is a bare python -c, and pip installs wheels without build subprocesses --
    so it is worth doing on its own terms rather than folded into this.

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions