swiftlang / swiftlang/swift-subprocess

FileDescriptor deinit fatalErrors ("was not closed") when a child closes its stdin early

Open
#367 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Swift
Stars
734
Forks
68
Avg merge
9d 2h
Merged PRs (30d)
6

Description

Summary

Subprocess.run(..., input: .string(...)) traps with a fatalError in its own FileDescriptor deinit when the child process closes its stdin early (reads what it needs, then exits and closes the read end before the parent finishes writing).

Subprocess/Configuration.swift:1115: Fatal error: FileDescriptor 8 was not closed

The deinit at Configuration.swift:1110-1115:

deinit {
    guard self.closeWhenDone else {
        return
    }
    fatalError("FileDescriptor \(self._descriptor) was not closed")
}

Early child exit leaves the input FileDescriptor with closeWhenDone == true and no path for the deinit to reconcile it, so the process traps (SIGTRAP, exit 133) instead of surfacing a recoverable error.

Reproducer

A minimal attached package (subprocess-fd-repro.zip) reproduces it deterministically with head -n 1 fed a large input:

signal(SIGPIPE, SIG_IGN)  // see note below

let input = (0..<50_000).map { "line-\($0)" }.joined(separator: "\n") + "\n"

let result = try await Subprocess.run(
    .name("head"),
    arguments: ["-n", "1"],
    input: .string(input),
    output: .string(limit: 64 * 1024),
    error: .string(limit: 64 * 1024)
)
$ swift run repro
Subprocess/Configuration.swift:1115: Fatal error: FileDescriptor 8 was not closed
$ echo $?
133

SIGPIPE masks the bug

With the default SIGPIPE disposition, the closed-pipe write terminates the parent (exit 141) before any deinit runs — so the fatalError is never reached and the defect is invisible. This is why the bug went unnoticed: most subprocess-feeding apps don't ignore SIGPIPE, so they die at the SIGPIPE step rather than the deinit step.

Ignoring SIGPIPE (standard for any CLI that pipes to children — git, curl, ssh, Python all do) unmasks it: the write error surfaces, and the FileDescriptor deinit then traps.

SIGPIPE disposition Exit Failure point
default (terminate) 141 (SIGPIPE) closed-pipe write — masks the deinit defect
ignored (SIG_IGN) 133 (SIGTRAP) FileDescriptor deinit fatalError

Real-world trigger

git check-ignore --stdin fed a large path list: git reads what it needs, closes stdin, and exits early — same shape as head -n 1. This surfaced in a real tool (wks, which ignores SIGPIPE): the silent exit-141 death looked like a hang/segfault with no stderr and no crash report, and only after installing SIG_IGN did the underlying fatalError become visible.

Expected behavior

A child closing its stdin early is a normal occurrence (any command that short-circuits on its input). The parent should surface a recoverable error (e.g. EPIPE / SubprocessError) rather than trap the process via deinit. The closeWhenDone invariant should be reconciled on early child exit so the deinit doesn't fire.

Environment

  • swift-subprocess 1.0.0
  • Swift 6.4 (Xcode 8, toolchain XcodeDefault.xctoolchain)
  • macOS 27.0 (Darwin 27), arm64

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in Subprocess/Configuration.swift around lines 1110-1115 and trace the input FileDescriptor lifecycle when the child closes stdin early. Reproduce with the attached package using head -n 1 and ignored SIGPIPE, then verify that early child exit surfaces a recoverable error without triggering the deinit fatalError.

Written by the indexing model from the issue text.

Assessment

Tech stack
swift
Domain
operating-systems
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
56/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.