Repository: go
Issue: io: Copy to a pipe prevents process exit (Go 1.24rc2 on Linux regression)
Labels: NeedsInvestigation,release-blocker,compiler/runtime,BugReport

### Go version

go version go1.24rc2 linux/amd64

### Output of `go env` in your module/workspace:

```shell
AR='ar'
CC='gcc'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='g++'
GCCGO='gccgo'
GO111MODULE=''
GOAMD64='v1'
GOARCH='amd64'
GOAUTH='netrc'
GOBIN=''
GOCACHE='/home/kir/.cache/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/home/kir/.config/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build957638587=/tmp/go-build -gno-record-gcc-switches'
GOHOSTARCH='amd64'
GOHOSTOS='linux'
GOINSECURE=''
GOMOD='/dev/null'
GOMODCACHE='/home/kir/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='linux'
GOPATH='/home/kir/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/home/kir/sdk/go1.24rc2'
GOSUMDB='sum.golang.org'
GOTELEMETRY='local'
GOTELEMETRYDIR='/home/kir/.config/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/home/kir/sdk/go1.24rc2/pkg/tool/linux_amd64'
GOVCS=''
GOVERSION='go1.24rc2'
GOWORK=''
PKG_CONFIG='pkg-config'
```

### What did you do?

There is a regression in Go 1.24rc2 (git-bisect points to [1]) caused by a (not yet confirmed) Linux kernel bug with sendmail(2)/splice(2), which I just reported [2].

In short, when sendfile(2) or splice(2) is used to copy data to a pipe, and another process is having the other end of this file, this prevents that other process from exit. You can get a short C repro from [2], and here's a Go repro:

```go
package main

import (
	"io"
	"log"
	"os"
	"os/exec"
)

func main() {
	// Create a pipe for stdin
	r, w, err := os.Pipe()
	if err != nil {
		log.Fatal(err)
	}

	// Create a short-lived process (like ps) with stdin connected to pipe
	cmd := exec.Command("ps")
	cmd.Stdin = r
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	if err := cmd.Start(); err != nil {
		log.Fatal(err)
	}
	log.Print("Run child pid=", cmd.Process.Pid)

	// Close read end in parent.
	r.Close()

	// Copy from stdin to pipe - this should trigger sendfile in Go 1.24.
	go func() {
		_, err = io.Copy(w, os.Stdin)
		if err != nil {
			log.Fatal("io.Copy: ", err)
		}
	}()

	if err := cmd.Wait(); err != nil {
		log.Fatal("wait: ", err)
	}
}
```


[1]: https://go-review.googlesource.com/c/go/+/603295
[2]: https://lore.kernel.org/linux-fsdevel/20250122020850.2175427-1-kolyshkin@gmail.com/T/#u

### What did you see happen?

When the above repro is run with go1.24rc2, the process is not exiting:

```console
[kir@kir-tp1 sendfile-vs-pipe]$ go1.24rc2 run repro.go 
2025/01/21 18:32:54 Run child pid=2180908
    PID TTY          TIME CMD
  63304 pts/1    00:00:03 bash
2180738 pts/1    00:00:00 go1.24rc2
2180747 pts/1    00:00:00 go
2180902 pts/1    00:00:00 repro
2180908 pts/1    00:00:00 ps
```

(and the process hangs here).

NOTE if you can't repro this, you probably pressed Enter in the terminal. Do not do anything with this terminal!.

In a different terminal, you can check the status of the child process (using the pid from the above output):

```console
[kir@kir-tp1 linux]$ ps -f -p 2180908
UID          PID    PPID  C STIME TTY          TIME CMD
kir      2180908 2180902  0 18:32 pts/1    00:00:00 [ps]
```

As you can see, `ps` thinks that the process is a kernel thread. This happens because the process is half-exited and some /proc/$PID/ entries (`root`, `cwd`, `exe) are no longer valid, like it is with kernel threads.

Now, to see where the process is stuck:

```console
[kir@kir-tp1 linux]$ sudo cat /proc/2180908/stack
[<0>] pipe_release+0x1f/0x100
[<0>] __fput+0xde/0x2a0
[<0>] task_work_run+0x59/0x90
[<0>] do_exit+0x309/0xab0
[<0>] do_group_exit+0x30/0x80
[<0>] __x64_sys_exit_group+0x18/0x20
[<0>] x64_sys_call+0x14b4/0x14c0
[<0>] do_syscall_64+0x82/0x160
[<0>] entry_SYSCALL_64_after_hwframe+0x76/0x7e
```

### What did you expect to see?

No stuck process.

With an older Go version, everything works as it should:

```console
[kir@kir-tp1 sendfile-vs-pipe]$ go1.23.4 run repro.go 
2025/01/21 18:38:16 Run child pid=2181265
    PID TTY          TIME CMD
  63304 pts/1    00:00:03 bash
2181065 pts/1    00:00:00 go1.23.4
2181071 pts/1    00:00:00 go
2181259 pts/1    00:00:00 repro
2181265 pts/1    00:00:00 ps
[kir@kir-tp1 sendfile-vs-pipe]$ 
```