Skip to content

os: NewFile blocks if handle is blocked #76391

@qmuntal

Description

@qmuntal

Go version

go version go1.25.1 windows/arm64

Output of go env in your module/workspace:

set AR=ar
set CC=gcc
set CGO_CFLAGS=-O2 -g
set CGO_CPPFLAGS=
set CGO_CXXFLAGS=-O2 -g
set CGO_ENABLED=1
set CGO_FFLAGS=-O2 -g
set CGO_LDFLAGS=-O2 -g
set CXX=g++
set GCCGO=gccgo
set GO111MODULE=
set GOARCH=arm64
set GOARM64=v8.0
set GOAUTH=netrc
set GOBIN=
set GOCACHE=C:\Users\qmuntaldiaz\AppData\Local\go-build
set GOCACHEPROG=
set GODEBUG=
set GOENV=C:\Users\qmuntaldiaz\AppData\Roaming\go\env
set GOEXE=.exe
set GOEXPERIMENT=
set GOFIPS140=off
set GOFLAGS=
set GOGCCFLAGS=-mthreads -fno-caret-diagnostics -Qunused-arguments -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=C:\Users\QMUNTA~1\AppData\Local\Temp\go-build1302331125=/tmp/go-build -gno-record-gcc-switches
set GOHOSTARCH=arm64
set GOHOSTOS=windows
set GOINSECURE=
set GOMOD=NUL
set GOMODCACHE=C:\Users\qmuntaldiaz\go\pkg\mod
set GONOPROXY=github.com/microsoft
set GONOSUMDB=github.com/microsoft
set GOOS=windows
set GOPATH=C:\Users\qmuntaldiaz\go
set GOPRIVATE=github.com/microsoft
set GOPROXY=https://proxy.golang.org,direct
set GOROOT=C:\Users\qmuntaldiaz\sdk\go1.25.1
set GOSUMDB=sum.golang.org
set GOTELEMETRY=on
set GOTELEMETRYDIR=C:\Users\qmuntaldiaz\AppData\Roaming\go\telemetry
set GOTMPDIR=
set GOTOOLCHAIN=auto
set GOTOOLDIR=C:\Users\qmuntaldiaz\sdk\go1.25.1\pkg\tool\windows_arm64
set GOVCS=
set GOVERSION=go1.25.1
set GOWORK=
set PKG_CONFIG=pkg-config

What did you do?

Call os.NewFile using a handle that is blocked doing I/O, like in here:

package main

import (
	"fmt"
	"os"
	"syscall"
	"time"

	"golang.org/x/sys/windows"
)

func main() {
	const name = `\\.\pipe\go_testpipe`
	h, err := windows.CreateNamedPipe(syscall.StringToUTF16Ptr(name), windows.PIPE_ACCESS_DUPLEX, 0, 1, 4096, 4096, 0, nil)
	if err != nil {
		panic(err)
	}
	pipe := os.NewFile(uintptr(h), name) // OK
	defer pipe.Close()

	file, err := os.Open(name)
	if err != nil {
		panic(err)
	}
	defer file.Close()
	go func() {
		// Give some time for the Read to start and block.
		time.Sleep(100 * time.Millisecond)
		fmt.Println("NewFile()")
		os.NewFile(uintptr(file.Fd()), name) // Unexpected block
		fmt.Println("NewFile() returned")
	}()

	fmt.Println("Read()")
	var tmp [1]byte
	// Read will blocks
	file.Read(tmp[:]) // Expected block
	fmt.Println("Read() returned")
}

What did you see happen?

os.NewFile blocks until the handle is unblocked:

Read()
NewFile()

What did you expect to see?

os.NewFile doesn't block even if the handle is blocked:

Read()
NewFile()
NewFile() returned"

Note that this has always been the behavior until Go 1.25. The regression was introduced in CL 662236.

This situation is more common than one would expect because the os package calls NewFile(uintptr(syscall.Stdin), "/dev/stdin") when initializing. And if someone has passed a blocked stdin handle, then the os package initialization hangs. This means that a Go application can hand even before calling any user-controlled code. See for example #75949.

@golang/windows

Metadata

Metadata

Assignees

Labels

BugReportIssues describing a possible bug in the Go implementation.CriticalA critical problem that affects the availability or correctness of production systems built using GoNeedsFixThe path to resolution is known, but the work has not been done.OS-Windowsokay-after-rc1Used by release team to mark a release-blocker issue as okay to resolve either before or after rc1release-blocker

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions