Skip to content

Commit 8a8f506

Browse files
committed
os,internal/poll: disassociate handle from IOCP in File.Fd
Go 1.25 will gain support for overlapped IO on handles passed to os.NewFile thanks to CL 662236. It was previously not possible to add an overlapped handle to the Go runtime's IO completion port (IOCP), and now happens on the first call the an IO method. This means that there is code that relies on the fact that File.Fd returns a handle that can always be associated with a custom IOCP. That wouldn't be the case anymore, as a handle can only be associated with one IOCP at a time and it must be explicitly disassociated. To fix this breaking change, File.Fd will disassociate the handle from the Go runtime IOCP before returning it. It is then not necessary to defer the association until the first IO method is called, which was recently added in CL 661955 to support this same use case, but in a more complex and unreliable way. Updates #19098. Cq-Include-Trybots: luci.golang.try:gotip-windows-amd64-race,gotip-windows-amd64-longtest,gotip-windows-arm64 Change-Id: Id8a7e04d35057047c61d1733bad5bf45494b2c28 Reviewed-on: https://go-review.googlesource.com/c/go/+/664455 Reviewed-by: Damien Neil <[email protected]> Reviewed-by: Alex Brainman <[email protected]> Reviewed-by: Michael Pratt <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent c1fc209 commit 8a8f506

File tree

9 files changed

+140
-83
lines changed

9 files changed

+140
-83
lines changed

src/internal/poll/fd_plan9.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,6 @@ type FD struct {
3636
isFile bool
3737
}
3838

39-
func (fd *FD) initIO() error {
40-
return nil
41-
}
42-
4339
// We need this to close out a file descriptor when it is unlocked,
4440
// but the real implementation has to live in the net package because
4541
// it uses os.File's.

src/internal/poll/fd_poll_runtime.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,6 @@ func setDeadlineImpl(fd *FD, t time.Time, mode int) error {
156156
}
157157
defer fd.decref()
158158

159-
fd.initIO()
160159
if fd.pd.runtimeCtx == 0 {
161160
return ErrNoDeadline
162161
}

src/internal/poll/fd_unix.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,6 @@ type FD struct {
4747
isFile bool
4848
}
4949

50-
func (fd *FD) initIO() error {
51-
return nil
52-
}
53-
5450
// Init initializes the FD. The Sysfd field should already be set.
5551
// This can be called multiple times on a single FD.
5652
// The net argument is a network name from the net package (e.g., "tcp"),

src/internal/poll/fd_windows.go

Lines changed: 70 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"internal/syscall/windows"
1111
"io"
1212
"sync"
13+
"sync/atomic"
1314
"syscall"
1415
"unicode/utf16"
1516
"unicode/utf8"
@@ -98,6 +99,12 @@ func (o *operation) setEvent() {
9899
o.o.HEvent = h | 1
99100
}
100101

102+
func (o *operation) close() {
103+
if o.o.HEvent != 0 {
104+
syscall.CloseHandle(o.o.HEvent)
105+
}
106+
}
107+
101108
func (o *operation) overlapped() *syscall.Overlapped {
102109
if o.fd.isBlocking {
103110
// Don't return the overlapped object if the file handle
@@ -169,7 +176,7 @@ func waitIO(o *operation) error {
169176
panic("can't wait on blocking operations")
170177
}
171178
fd := o.fd
172-
if !fd.pd.pollable() {
179+
if !fd.pollable() {
173180
// The overlapped handle is not added to the runtime poller,
174181
// the only way to wait for the IO to complete is block until
175182
// the overlapped event is signaled.
@@ -190,7 +197,7 @@ func waitIO(o *operation) error {
190197
// cancelIO cancels the IO operation o and waits for it to complete.
191198
func cancelIO(o *operation) {
192199
fd := o.fd
193-
if !fd.pd.pollable() {
200+
if !fd.pollable() {
194201
return
195202
}
196203
// Cancel our request.
@@ -209,14 +216,13 @@ func cancelIO(o *operation) {
209216
// to avoid reusing the values from a previous call.
210217
func execIO(o *operation, submit func(o *operation) error) (int, error) {
211218
fd := o.fd
212-
fd.initIO()
213219
// Notify runtime netpoll about starting IO.
214220
err := fd.pd.prepare(int(o.mode), fd.isFile)
215221
if err != nil {
216222
return 0, err
217223
}
218224
// Start IO.
219-
if !fd.isBlocking && o.o.HEvent == 0 && !fd.pd.pollable() {
225+
if !fd.isBlocking && o.o.HEvent == 0 && !fd.pollable() {
220226
// If the handle is opened for overlapped IO but we can't
221227
// use the runtime poller, then we need to use an
222228
// event to wait for the IO to complete.
@@ -244,10 +250,11 @@ func execIO(o *operation, submit func(o *operation) error) (int, error) {
244250
err = windows.WSAGetOverlappedResult(fd.Sysfd, &o.o, &o.qty, false, &o.flags)
245251
}
246252
}
247-
// ERROR_OPERATION_ABORTED may have been caused by us. In that case,
248-
// map it to our own error. Don't do more than that, each submitted
249-
// function may have its own meaning for each error.
250-
if err == syscall.ERROR_OPERATION_ABORTED {
253+
switch err {
254+
case syscall.ERROR_OPERATION_ABORTED:
255+
// ERROR_OPERATION_ABORTED may have been caused by us. In that case,
256+
// map it to our own error. Don't do more than that, each submitted
257+
// function may have its own meaning for each error.
251258
if waitErr != nil {
252259
// IO canceled by the poller while waiting for completion.
253260
err = waitErr
@@ -257,6 +264,12 @@ func execIO(o *operation, submit func(o *operation) error) (int, error) {
257264
// we assume it is interrupted by Close.
258265
err = errClosing(fd.isFile)
259266
}
267+
case windows.ERROR_IO_INCOMPLETE:
268+
// waitIO couldn't wait for the IO to complete.
269+
if waitErr != nil {
270+
// The wait error will be more informative.
271+
err = waitErr
272+
}
260273
}
261274
return int(o.qty), err
262275
}
@@ -314,9 +327,7 @@ type FD struct {
314327
// Whether FILE_FLAG_OVERLAPPED was not set when opening the file.
315328
isBlocking bool
316329

317-
// Initialization parameters.
318-
initIOOnce sync.Once
319-
initIOErr error // only used in the net package
330+
disassociated atomic.Bool
320331
}
321332

322333
// setOffset sets the offset fields of the overlapped object
@@ -343,6 +354,12 @@ func (fd *FD) addOffset(off int) {
343354
fd.setOffset(fd.offset + int64(off))
344355
}
345356

357+
// pollable should be used instead of fd.pd.pollable(),
358+
// as it is aware of the disassociated state.
359+
func (fd *FD) pollable() bool {
360+
return fd.pd.pollable() && !fd.disassociated.Load()
361+
}
362+
346363
// fileKind describes the kind of file.
347364
type fileKind byte
348365

@@ -353,35 +370,6 @@ const (
353370
kindPipe
354371
)
355372

356-
func (fd *FD) initIO() error {
357-
if fd.isBlocking {
358-
return nil
359-
}
360-
fd.initIOOnce.Do(func() {
361-
if fd.closing() {
362-
// Closing, nothing to do.
363-
return
364-
}
365-
// The runtime poller will ignore I/O completion
366-
// notifications not initiated by this package,
367-
// so it is safe to add handles owned by the caller.
368-
fd.initIOErr = fd.pd.init(fd)
369-
if fd.initIOErr != nil {
370-
return
371-
}
372-
fd.rop.runtimeCtx = fd.pd.runtimeCtx
373-
fd.wop.runtimeCtx = fd.pd.runtimeCtx
374-
if fd.kind != kindNet || socketCanUseSetFileCompletionNotificationModes {
375-
// Non-socket handles can use SetFileCompletionNotificationModes without problems.
376-
err := syscall.SetFileCompletionNotificationModes(fd.Sysfd,
377-
syscall.FILE_SKIP_SET_EVENT_ON_HANDLE|syscall.FILE_SKIP_COMPLETION_PORT_ON_SUCCESS,
378-
)
379-
fd.skipSyncNotif = err == nil
380-
}
381-
})
382-
return fd.initIOErr
383-
}
384-
385373
// Init initializes the FD. The Sysfd field should already be set.
386374
// This can be called multiple times on a single FD.
387375
// The net argument is a network name from the net package (e.g., "tcp"),
@@ -411,27 +399,55 @@ func (fd *FD) Init(net string, pollable bool) error {
411399
fd.rop.fd = fd
412400
fd.wop.fd = fd
413401

414-
// A file handle (and its duplicated handles) can only be associated
415-
// with one IOCP. A new association will fail if the handle is already
416-
// associated. Defer the association until the first I/O operation so that
417-
// overlapped handles passed in os.NewFile have a chance to be used
418-
// with an external IOCP. This is common case, for example, when calling
419-
// os.NewFile on a handle just to pass it to a exec.Command standard
420-
// input/output/error. If the association fails, the I/O operations
421-
// will be performed synchronously.
422-
if fd.kind == kindNet {
423-
// The net package is the only consumer that requires overlapped
424-
// handles and that cares about handle IOCP association errors.
425-
// We can should do the IOCP association here.
426-
return fd.initIO()
402+
// It is safe to add overlapped handles that also perform I/O
403+
// outside of the runtime poller. The runtime poller will ignore
404+
// I/O completion notifications not initiated by us.
405+
err := fd.pd.init(fd)
406+
if err != nil {
407+
return err
408+
}
409+
fd.rop.runtimeCtx = fd.pd.runtimeCtx
410+
fd.wop.runtimeCtx = fd.pd.runtimeCtx
411+
if fd.kind != kindNet || socketCanUseSetFileCompletionNotificationModes {
412+
// Non-socket handles can use SetFileCompletionNotificationModes without problems.
413+
err := syscall.SetFileCompletionNotificationModes(fd.Sysfd,
414+
syscall.FILE_SKIP_SET_EVENT_ON_HANDLE|syscall.FILE_SKIP_COMPLETION_PORT_ON_SUCCESS,
415+
)
416+
fd.skipSyncNotif = err == nil
417+
}
418+
return nil
419+
}
420+
421+
// DisassociateIOCP disassociates the file handle from the IOCP.
422+
// The disassociate operation will not succeed if there is any
423+
// in-progress IO operation on the file handle.
424+
func (fd *FD) DisassociateIOCP() error {
425+
if err := fd.incref(); err != nil {
426+
return err
427+
}
428+
defer fd.decref()
429+
430+
if fd.isBlocking || !fd.pollable() {
431+
// Nothing to disassociate.
432+
return nil
427433
}
434+
435+
info := windows.FILE_COMPLETION_INFORMATION{}
436+
if err := windows.NtSetInformationFile(fd.Sysfd, &windows.IO_STATUS_BLOCK{}, unsafe.Pointer(&info), uint32(unsafe.Sizeof(info)), windows.FileReplaceCompletionInformation); err != nil {
437+
return err
438+
}
439+
fd.disassociated.Store(true)
440+
// Don't call fd.pd.close(), it would be too racy.
441+
// There is no harm on leaving fd.pd open until Close is called.
428442
return nil
429443
}
430444

431445
func (fd *FD) destroy() error {
432446
if fd.Sysfd == syscall.InvalidHandle {
433447
return syscall.EINVAL
434448
}
449+
fd.rop.close()
450+
fd.wop.close()
435451
// Poller may want to unregister fd in readiness notification mechanism,
436452
// so this must be executed before fd.CloseFunc.
437453
fd.pd.close()
@@ -454,12 +470,7 @@ func (fd *FD) Close() error {
454470
if !fd.fdmu.increfAndClose() {
455471
return errClosing(fd.isFile)
456472
}
457-
// There is a potential race between a concurrent call to fd.initIO,
458-
// which calls fd.pd.init, and the call to fd.pd.evict below.
459-
// This is solved by calling fd.initIO ourselves, which will
460-
// block until the concurrent fd.initIO has completed. Note
461-
// that fd.initIO is no-op if first called from here.
462-
fd.initIO()
473+
463474
if fd.kind == kindPipe {
464475
syscall.CancelIoEx(fd.Sysfd, nil)
465476
}

src/internal/syscall/windows/syscall_windows.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,15 @@ func UTF16PtrToString(p *uint16) string {
3232
}
3333

3434
const (
35+
ERROR_INVALID_HANDLE syscall.Errno = 6
3536
ERROR_BAD_LENGTH syscall.Errno = 24
3637
ERROR_SHARING_VIOLATION syscall.Errno = 32
3738
ERROR_LOCK_VIOLATION syscall.Errno = 33
3839
ERROR_NOT_SUPPORTED syscall.Errno = 50
3940
ERROR_CALL_NOT_IMPLEMENTED syscall.Errno = 120
4041
ERROR_INVALID_NAME syscall.Errno = 123
4142
ERROR_LOCK_FAILED syscall.Errno = 167
43+
ERROR_IO_INCOMPLETE syscall.Errno = 996
4244
ERROR_NO_TOKEN syscall.Errno = 1008
4345
ERROR_NO_UNICODE_TRANSLATION syscall.Errno = 1113
4446
ERROR_CANT_ACCESS_FILE syscall.Errno = 1920

src/internal/syscall/windows/types_windows.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,3 +248,11 @@ type FILE_LINK_INFORMATION struct {
248248
FileNameLength uint32
249249
FileName [syscall.MAX_PATH]uint16
250250
}
251+
252+
const FileReplaceCompletionInformation = 61
253+
254+
// https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/ns-ntifs-_file_completion_information
255+
type FILE_COMPLETION_INFORMATION struct {
256+
Port syscall.Handle
257+
Key uintptr
258+
}

src/os/file.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -714,8 +714,12 @@ func (f *File) SyscallConn() (syscall.RawConn, error) {
714714
// Do not close the returned descriptor; that could cause a later
715715
// close of f to close an unrelated descriptor.
716716
//
717-
// On Unix systems this will cause the [File.SetDeadline]
718-
// methods to stop working.
717+
// Fd's behavior differs on some platforms:
718+
//
719+
// - On Unix and Windows, [File.SetDeadline] methods will stop working.
720+
// - On Windows, the file descriptor will be disassociated from the
721+
// Go runtime I/O completion port if there are no concurrent I/O
722+
// operations on the file.
719723
//
720724
// For most uses prefer the f.SyscallConn method.
721725
func (f *File) Fd() uintptr {

src/os/file_windows.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ func (file *File) fd() uintptr {
3737
if file == nil {
3838
return uintptr(syscall.InvalidHandle)
3939
}
40+
// Try to disassociate the file from the runtime poller.
41+
// File.Fd doesn't return an error, so we don't have a way to
42+
// report it. We just ignore it. It's up to the caller to call
43+
// it when there are no concurrent IO operations.
44+
_ = file.pfd.DisassociateIOCP()
4045
return uintptr(file.pfd.Sysfd)
4146
}
4247

src/os/os_windows_test.go

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1985,19 +1985,7 @@ func TestPipeCanceled(t *testing.T) {
19851985
}
19861986

19871987
func iocpAssociateFile(f *os.File, iocp syscall.Handle) error {
1988-
sc, err := f.SyscallConn()
1989-
if err != nil {
1990-
return err
1991-
}
1992-
var syserr error
1993-
err = sc.Control(func(fd uintptr) {
1994-
if _, err = windows.CreateIoCompletionPort(syscall.Handle(fd), iocp, 0, 0); err != nil {
1995-
syserr = err
1996-
}
1997-
})
1998-
if err == nil {
1999-
err = syserr
2000-
}
1988+
_, err := windows.CreateIoCompletionPort(syscall.Handle(f.Fd()), iocp, 0, 0)
20011989
return err
20021990
}
20031991

@@ -2075,6 +2063,54 @@ func TestFileAssociatedWithExternalIOCP(t *testing.T) {
20752063
}
20762064
}
20772065

2066+
func TestFileWriteFdRace(t *testing.T) {
2067+
t.Parallel()
2068+
2069+
f := newFileOverlapped(t, filepath.Join(t.TempDir(), "a"), true)
2070+
2071+
var wg sync.WaitGroup
2072+
wg.Add(2)
2073+
2074+
go func() {
2075+
defer wg.Done()
2076+
n, err := f.Write([]byte("hi"))
2077+
if err != nil {
2078+
// We look at error strings as the
2079+
// expected errors are OS-specific.
2080+
switch {
2081+
case errors.Is(err, windows.ERROR_INVALID_HANDLE):
2082+
// Ignore an expected error.
2083+
default:
2084+
// Unexpected error.
2085+
t.Error(err)
2086+
}
2087+
return
2088+
}
2089+
if n != 2 {
2090+
t.Errorf("wrote %d bytes, expected 2", n)
2091+
return
2092+
}
2093+
}()
2094+
go func() {
2095+
defer wg.Done()
2096+
f.Fd()
2097+
}()
2098+
wg.Wait()
2099+
2100+
iocp, err := windows.CreateIoCompletionPort(syscall.InvalidHandle, 0, 0, 0)
2101+
if err != nil {
2102+
t.Fatal(err)
2103+
}
2104+
defer syscall.CloseHandle(iocp)
2105+
if err := iocpAssociateFile(f, iocp); err != nil {
2106+
t.Fatal(err)
2107+
}
2108+
2109+
if _, err := f.Write([]byte("hi")); err != nil {
2110+
t.Error(err)
2111+
}
2112+
}
2113+
20782114
func TestSplitPath(t *testing.T) {
20792115
t.Parallel()
20802116
for _, tt := range []struct{ path, wantDir, wantBase string }{

0 commit comments

Comments
 (0)