From 9f59cdde4b9a85d6d45e5b6d455743e968b56659 Mon Sep 17 00:00:00 2001 From: broken-circle <252359939+broken-circle@users.noreply.github.com> Date: Wed, 10 Jun 2026 13:04:52 -0700 Subject: [PATCH] Spawn `cat` in its own session in `testSuspendResumeProcess()` The test spawned `/bin/cat` with default `PlatformOptions`, so `cat` inherited the test runner's process group, then sent it `SIGSTOP`, leaving a stopped process in the runner's own process group. On POSIX, when a process group becomes orphaned and any member is stopped, `SIGHUP` followed by `SIGCONT` is sent to every process in the group. Under the full suite's concurrent process churn, that group intermittently became orphaned while `cat` was stopped, and the kernel delivered `SIGHUP` to every member of the group, including the test process, which exited on the signal. This surfaced only when the suite ran in parallel, as a bare signal-1 exit with no recorded test failure. Spawn `cat` with `createSession` so it runs in its own session, and stopping it no longer leaves a stopped member in the runner's group. `cat`'s own group is orphaned at `setsid` time, which POSIX exempts from the SIGHUP, so the suspend/resume assertions are unaffected. --- Tests/SubprocessTests/UnixTests.swift | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Tests/SubprocessTests/UnixTests.swift b/Tests/SubprocessTests/UnixTests.swift index a6032aa7..6d3a1fae 100644 --- a/Tests/SubprocessTests/UnixTests.swift +++ b/Tests/SubprocessTests/UnixTests.swift @@ -378,10 +378,19 @@ extension SubprocessUnixTests { let flags = fcntl(outputPipe.readEnd.rawValue, F_GETFL) try #require(fcntl(outputPipe.readEnd.rawValue, F_SETFL, flags | O_NONBLOCK) == 0) + // Isolate cat in its own session: the SIGSTOP below would otherwise + // leave a stopped process in the test runner's process group, and a + // concurrent child exit that orphans that group makes the kernel + // deliver SIGHUP+SIGCONT to every member of the group, including this + // test process, killing the run. + var platformOptions = PlatformOptions() + platformOptions.createSession = true + try await outputPipe.readEnd.closeAfter { try await inputPipe.writeEnd.closeAfter { _ = try await Subprocess.run( .path("/bin/cat"), + platformOptions: platformOptions, // cat reads from inputPipe.readEnd. The parent keeps writeEnd to feed it. input: .fileDescriptor(inputPipe.readEnd, closeAfterSpawningProcess: true), // cat writes to outputPipe.writeEnd. The parent keeps readEnd to drain it.