Skip to content

interp: dup pipe fds in pipelines so EOF and SIGPIPE propagate#1334

Closed
qiangli wants to merge 1 commit into
mvdan:masterfrom
qiangli:interp-pipe-fd-eof
Closed

interp: dup pipe fds in pipelines so EOF and SIGPIPE propagate#1334
qiangli wants to merge 1 commit into
mvdan:masterfrom
qiangli:interp-pipe-fd-eof

Conversation

@qiangli

@qiangli qiangli commented May 9, 2026

Copy link
Copy Markdown

Summary

When the parent Go process holds extra references to a pipeline's read or write end, EOF does not propagate to readers and SIGPIPE is not delivered to writers. Concretely, yes | head -1 hangs forever instead of terminating after head closes its stdin.

Approach

Duplicate both pipe ends with syscall.Dup before handing them to the goroutines that run the left and right sides, then immediately close the originals in the parent. The parent no longer holds any reference to the pipe; closing the duplicates in the goroutines is now sufficient to drive EOF/SIGPIPE through the pipeline.

  • Duplicates are marked CloseOnExec so external commands spawned from inside the goroutines do not inherit them.
  • Restoring r.stdin from oldStdin after wg.Wait() prevents the right-hand side's stdin replacement from leaking into subsequent statements in the enclosing block.
  • A new helper dupPipeFd lives in interp/os_unix.go on unix platforms and as a no-op fallback in interp/os_notunix.go. On non-unix the previous behaviour is preserved (pipelines run but EOF/SIGPIPE propagation is best-effort).

Fixes #1142.

Test plan

  • go test -short ./interp/ passes.
  • Manual: yes | head -1 exits promptly under the patched runner; previously hung indefinitely.

@mvdan

mvdan commented May 19, 2026

Copy link
Copy Markdown
Owner

Can we write a regression test for this that fails without the fix? Otherwise, the fix is going to be rather fragile.

When the parent Go process holds extra references to a pipeline's
read or write end, EOF does not propagate to readers and SIGPIPE
is not delivered to writers. Concretely, `yes | head -1` hangs
forever instead of terminating after head closes its stdin.

Fix this by duplicating both pipe ends with syscall.Dup before
handing them to the goroutines that run the left and right sides,
then immediately closing the originals in the parent. The parent
no longer holds any reference to the pipe; closing the duplicates
in the goroutines is now sufficient to drive EOF/SIGPIPE through
the pipeline.

The duplicate fds are marked CloseOnExec so external commands
spawned from inside the goroutines do not inherit them.

Restoring r.stdin from oldStdin after wg.Wait() prevents the
right-hand side's stdin replacement from leaking into subsequent
statements in the enclosing block.

A new helper, dupPipeFd, lives in interp/os_unix.go on unix
platforms and as a no-op fallback in interp/os_notunix.go. On
non-unix the previous behaviour is preserved (pipelines run but
EOF/SIGPIPE propagation is best-effort).

Fixes mvdan#1142
@qiangli qiangli force-pushed the interp-pipe-fd-eof branch from 15dca86 to 12f5191 Compare May 23, 2026 08:21
@mvdan

mvdan commented May 31, 2026

Copy link
Copy Markdown
Owner

@qiangli did you see my comment above?

@qiangli

qiangli commented Jun 2, 2026

Copy link
Copy Markdown
Author

Sorry, I did not see. I may create another pull request, closing this one for now.

@qiangli qiangli closed this Jun 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

pipelining and redirection are broken because gosh does not close file descriptors

2 participants