Skip to content

Commit b9cbb65

Browse files
qmuntalgopherbot
authored andcommitted
os,internal/poll: support I/O on overlapped handles not added to the poller
Calling syscall.ReadFile and syscall.WriteFile on overlapped handles always need to be passed a valid *syscall.Overlapped structure, even if the handle is not added to a IOCP (like the Go runtime poller). Else, the syscall will fail with ERROR_INVALID_PARAMETER. We also need to handle ERROR_IO_PENDING errors when the overlapped handle is not added to the poller, in which case we need to block until the operation completes. Previous CLs already added support for overlapped handles to the poller, mostly to keep track of the file offset independently of the file pointer (which is not supported for overlapped handles). Fixed #15388. Updates #19098. Change-Id: I2103ab892a37d0e326752ae8c2771a43c13ba42e Reviewed-on: https://go-review.googlesource.com/c/go/+/661795 Auto-Submit: Quim Muntal <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Carlos Amedee <[email protected]> Reviewed-by: Damien Neil <[email protected]>
1 parent eec3745 commit b9cbb65

File tree

6 files changed

+279
-82
lines changed

6 files changed

+279
-82
lines changed
+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
On Windows, [NewFile] supports overlapped (a.k.a non-blocking) file handles even
2+
when the handle can't be added to the Go runtime I/O Completion Port (IOCP), normally
3+
because it is already attached to another IOCP. The I/O operations will be performed in
4+
a blocking manner instead of using the Go runtime IOCP.
5+
Particularly, this means that is now possible to reliably pass overlapped named pipes and
6+
sockets to a Go process standard input, output, and error.

src/internal/poll/fd_windows.go

+49-23
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ type operation struct {
8989
}
9090

9191
func (o *operation) overlapped() *syscall.Overlapped {
92-
if o.runtimeCtx == 0 {
92+
if o.fd.isBlocking {
9393
// Don't return the overlapped object if the file handle
9494
// doesn't use overlapped I/O. It could be used, but
9595
// that would then use the file pointer stored in the
@@ -162,9 +162,36 @@ func execIO(o *operation, submit func(o *operation) error) (int, error) {
162162
if err != nil {
163163
return 0, err
164164
}
165+
getOverlappedResult := func() (int, error) {
166+
if fd.isFile {
167+
err = windows.GetOverlappedResult(fd.Sysfd, &o.o, &o.qty, false)
168+
} else {
169+
err = windows.WSAGetOverlappedResult(fd.Sysfd, &o.o, &o.qty, false, &o.flags)
170+
}
171+
switch err {
172+
case nil:
173+
return int(o.qty), nil
174+
case syscall.ERROR_HANDLE_EOF:
175+
// EOF reached.
176+
return int(o.qty), io.EOF
177+
case syscall.ERROR_MORE_DATA, windows.WSAEMSGSIZE:
178+
// More data available. Return back the size of received data.
179+
return int(o.qty), err
180+
default:
181+
return 0, err
182+
}
183+
}
165184
// Start IO.
166185
err = submit(o)
167186
if !fd.pd.pollable() {
187+
if err == syscall.ERROR_IO_PENDING {
188+
// The overlapped handle is not added to the runtime poller,
189+
// the only way to wait for the IO to complete is block.
190+
_, err = syscall.WaitForSingleObject(fd.Sysfd, syscall.INFINITE)
191+
if err == nil {
192+
return getOverlappedResult()
193+
}
194+
}
168195
if err != nil {
169196
return 0, err
170197
}
@@ -187,20 +214,8 @@ func execIO(o *operation, submit func(o *operation) error) (int, error) {
187214
// Wait for our request to complete.
188215
err = fd.pd.wait(int(o.mode), fd.isFile)
189216
if err == nil {
190-
if fd.isFile {
191-
err = windows.GetOverlappedResult(fd.Sysfd, &o.o, &o.qty, false)
192-
} else {
193-
err = windows.WSAGetOverlappedResult(fd.Sysfd, &o.o, &o.qty, false, &o.flags)
194-
}
195217
// All is good. Extract our IO results and return.
196-
if err != nil {
197-
// More data available. Return back the size of received data.
198-
if err == syscall.ERROR_MORE_DATA || err == windows.WSAEMSGSIZE {
199-
return int(o.qty), err
200-
}
201-
return 0, err
202-
}
203-
return int(o.qty), nil
218+
return getOverlappedResult()
204219
}
205220
// IO is interrupted by "close" or "timeout"
206221
netpollErr := err
@@ -219,21 +234,17 @@ func execIO(o *operation, submit func(o *operation) error) (int, error) {
219234
}
220235
// Wait for cancellation to complete.
221236
fd.pd.waitCanceled(int(o.mode))
222-
if fd.isFile {
223-
err = windows.GetOverlappedResult(fd.Sysfd, &o.o, &o.qty, true)
224-
} else {
225-
err = windows.WSAGetOverlappedResult(fd.Sysfd, &o.o, &o.qty, false, &o.flags)
226-
}
237+
n, err := getOverlappedResult()
227238
if err != nil {
228239
if err == syscall.ERROR_OPERATION_ABORTED { // IO Canceled
229240
err = netpollErr
230241
}
231-
return 0, err
242+
return n, err
232243
}
233244
// We issued a cancellation request. But, it seems, IO operation succeeded
234245
// before the cancellation request run. We need to treat the IO operation as
235246
// succeeded (the bytes are actually sent/recv from network).
236-
return int(o.qty), nil
247+
return n, nil
237248
}
238249

239250
// FD is a file descriptor. The net and os packages embed this type in
@@ -285,6 +296,9 @@ type FD struct {
285296

286297
// The kind of this file.
287298
kind fileKind
299+
300+
// Whether FILE_FLAG_OVERLAPPED was not set when opening the file
301+
isBlocking bool
288302
}
289303

290304
// setOffset sets the offset fields of the overlapped object
@@ -364,11 +378,21 @@ func (fd *FD) Init(net string, pollable bool) error {
364378
// If we could not add the handle to the runtime poller,
365379
// assume the handle hasn't been opened for overlapped I/O.
366380
err = fd.pd.init(fd)
381+
pollable = err == nil
367382
}
368383
if logInitFD != nil {
369384
logInitFD(net, fd, err)
370385
}
371-
if !pollable || err != nil {
386+
if !pollable {
387+
// Handle opened for overlapped I/O (aka non-blocking) that are not added
388+
// to the runtime poller need special handling when reading and writing.
389+
var info windows.FILE_MODE_INFORMATION
390+
if err := windows.NtQueryInformationFile(fd.Sysfd, &windows.IO_STATUS_BLOCK{}, uintptr(unsafe.Pointer(&info)), uint32(unsafe.Sizeof(info)), windows.FileModeInformation); err == nil {
391+
fd.isBlocking = info.Mode&(windows.FILE_SYNCHRONOUS_IO_ALERT|windows.FILE_SYNCHRONOUS_IO_NONALERT) != 0
392+
} else {
393+
// If we fail to get the file mode information, assume the file is blocking.
394+
fd.isBlocking = true
395+
}
372396
return err
373397
}
374398
if fd.kind != kindNet || socketCanUseSetFileCompletionNotificationModes {
@@ -455,6 +479,9 @@ func (fd *FD) Read(buf []byte) (int, error) {
455479
return syscall.ReadFile(o.fd.Sysfd, unsafe.Slice(o.buf.Buf, o.buf.Len), &o.qty, o.overlapped())
456480
})
457481
fd.addOffset(n)
482+
if err == syscall.ERROR_HANDLE_EOF {
483+
err = io.EOF
484+
}
458485
if fd.kind == kindPipe && err != nil {
459486
switch err {
460487
case syscall.ERROR_BROKEN_PIPE:
@@ -591,7 +618,6 @@ func (fd *FD) Pread(b []byte, off int64) (int, error) {
591618
return syscall.ReadFile(o.fd.Sysfd, unsafe.Slice(o.buf.Buf, o.buf.Len), &o.qty, &o.o)
592619
})
593620
if err != nil {
594-
n = 0
595621
if err == syscall.ERROR_HANDLE_EOF {
596622
err = io.EOF
597623
}

src/internal/poll/fd_windows_test.go

+81-59
Original file line numberDiff line numberDiff line change
@@ -191,25 +191,25 @@ type _TCP_INFO_v0 struct {
191191
SynRetrans uint8
192192
}
193193

194-
func newFD(t testing.TB, h syscall.Handle, kind string, overlapped bool) *poll.FD {
194+
func newFD(t testing.TB, h syscall.Handle, kind string, overlapped, pollable bool) *poll.FD {
195195
fd := poll.FD{
196196
Sysfd: h,
197197
IsStream: true,
198198
ZeroReadIsEOF: true,
199199
}
200-
err := fd.Init(kind, true)
200+
err := fd.Init(kind, pollable)
201201
if overlapped && err != nil {
202202
// Overlapped file handles should not error.
203203
t.Fatal(err)
204-
} else if !overlapped && err == nil {
204+
} else if !overlapped && pollable && err == nil {
205205
// Non-overlapped file handles should return an error but still
206206
// be usable as sync handles.
207207
t.Fatal("expected error for non-overlapped file handle")
208208
}
209209
return &fd
210210
}
211211

212-
func newFile(t testing.TB, name string, overlapped bool) *poll.FD {
212+
func newFile(t testing.TB, name string, overlapped, pollable bool) *poll.FD {
213213
namep, err := syscall.UTF16PtrFromString(name)
214214
if err != nil {
215215
t.Fatal(err)
@@ -230,7 +230,15 @@ func newFile(t testing.TB, name string, overlapped bool) *poll.FD {
230230
t.Fatal(err)
231231
}
232232
})
233-
return newFD(t, h, "file", overlapped)
233+
typ, err := syscall.GetFileType(h)
234+
if err != nil {
235+
t.Fatal(err)
236+
}
237+
kind := "file"
238+
if typ == syscall.FILE_TYPE_PIPE {
239+
kind = "pipe"
240+
}
241+
return newFD(t, h, kind, overlapped, pollable)
234242
}
235243

236244
var currentProces = sync.OnceValue(func() string {
@@ -240,8 +248,19 @@ var currentProces = sync.OnceValue(func() string {
240248

241249
var pipeCounter atomic.Uint64
242250

243-
func newPipe(t testing.TB, overlapped, message bool) (string, *poll.FD) {
244-
name := `\\.\pipe\go-internal-poll-test-` + currentProces() + `-` + strconv.FormatUint(pipeCounter.Add(1), 10)
251+
func newBytePipe(t testing.TB, name string, overlapped, pollable bool) *poll.FD {
252+
return newPipe(t, name, false, overlapped, pollable)
253+
}
254+
255+
func newMessagePipe(t testing.TB, name string, overlapped, pollable bool) *poll.FD {
256+
return newPipe(t, name, true, overlapped, pollable)
257+
}
258+
259+
func pipeName() string {
260+
return `\\.\pipe\go-internal-poll-test-` + currentProces() + `-` + strconv.FormatUint(pipeCounter.Add(1), 10)
261+
}
262+
263+
func newPipe(t testing.TB, name string, message, overlapped, pollable bool) *poll.FD {
245264
wname, err := syscall.UTF16PtrFromString(name)
246265
if err != nil {
247266
t.Fatal(err)
@@ -264,7 +283,7 @@ func newPipe(t testing.TB, overlapped, message bool) (string, *poll.FD) {
264283
t.Fatal(err)
265284
}
266285
})
267-
return name, newFD(t, h, "pipe", overlapped)
286+
return newFD(t, h, "pipe", overlapped, pollable)
268287
}
269288

270289
func testReadWrite(t *testing.T, fdr, fdw *poll.FD) {
@@ -341,54 +360,55 @@ func testPreadPwrite(t *testing.T, fdr, fdw *poll.FD) {
341360

342361
func TestFile(t *testing.T) {
343362
t.Parallel()
344-
test := func(t *testing.T, r, w bool) {
345-
t.Parallel()
346-
name := filepath.Join(t.TempDir(), "foo")
347-
rh := newFile(t, name, r)
348-
wh := newFile(t, name, w)
349-
testReadWrite(t, rh, wh)
350-
testPreadPwrite(t, rh, wh)
351-
}
352-
t.Run("overlapped", func(t *testing.T) {
353-
test(t, true, true)
354-
})
355-
t.Run("overlapped-read", func(t *testing.T) {
356-
test(t, true, false)
357-
})
358-
t.Run("overlapped-write", func(t *testing.T) {
359-
test(t, false, true)
360-
})
361-
t.Run("sync", func(t *testing.T) {
362-
test(t, false, false)
363-
})
363+
tests := []struct {
364+
name string
365+
overlappedRead bool
366+
overlappedWrite bool
367+
pollable bool
368+
}{
369+
{"overlapped", true, true, true},
370+
{"overlapped-nonpollable", true, true, false},
371+
{"overlapped-read", true, false, true},
372+
{"overlapped-write", false, true, true},
373+
{"sync", false, false, false},
374+
{"sync-pollable", false, false, true},
375+
}
376+
for _, tt := range tests {
377+
t.Run(tt.name, func(t *testing.T) {
378+
t.Parallel()
379+
name := filepath.Join(t.TempDir(), "foo")
380+
rh := newFile(t, name, tt.overlappedRead, tt.pollable)
381+
wh := newFile(t, name, tt.overlappedWrite, tt.pollable)
382+
testReadWrite(t, rh, wh)
383+
testPreadPwrite(t, rh, wh)
384+
})
385+
}
364386
}
365387

366388
func TestPipe(t *testing.T) {
367389
t.Parallel()
368-
t.Run("overlapped", func(t *testing.T) {
369-
t.Parallel()
370-
name, pipe := newPipe(t, true, false)
371-
file := newFile(t, name, true)
372-
testReadWrite(t, pipe, file)
373-
})
374-
t.Run("overlapped-write", func(t *testing.T) {
375-
t.Parallel()
376-
name, pipe := newPipe(t, true, false)
377-
file := newFile(t, name, false)
378-
testReadWrite(t, file, pipe)
379-
})
380-
t.Run("overlapped-read", func(t *testing.T) {
381-
t.Parallel()
382-
name, pipe := newPipe(t, false, false)
383-
file := newFile(t, name, true)
384-
testReadWrite(t, file, pipe)
385-
})
386-
t.Run("sync", func(t *testing.T) {
387-
t.Parallel()
388-
name, pipe := newPipe(t, false, false)
389-
file := newFile(t, name, false)
390-
testReadWrite(t, file, pipe)
391-
})
390+
tests := []struct {
391+
name string
392+
overlappedRead bool
393+
overlappedWrite bool
394+
pollable bool
395+
}{
396+
{"overlapped", true, true, true},
397+
{"overlapped-nonpollable", true, true, false},
398+
{"overlapped-write", false, true, true},
399+
{"overlapped-read", true, false, true},
400+
{"sync", false, false, false},
401+
{"sync-pollable", false, false, true},
402+
}
403+
for _, tt := range tests {
404+
t.Run(tt.name, func(t *testing.T) {
405+
t.Parallel()
406+
name := pipeName()
407+
pipe := newBytePipe(t, name, tt.overlappedWrite, tt.pollable)
408+
file := newFile(t, name, tt.overlappedRead, tt.pollable)
409+
testReadWrite(t, pipe, file)
410+
})
411+
}
392412
t.Run("anonymous", func(t *testing.T) {
393413
t.Parallel()
394414
var r, w syscall.Handle
@@ -404,16 +424,17 @@ func TestPipe(t *testing.T) {
404424
}
405425
}()
406426
// CreatePipe always returns sync handles.
407-
fdr := newFD(t, r, "pipe", false)
408-
fdw := newFD(t, w, "file", false)
427+
fdr := newFD(t, r, "pipe", false, false)
428+
fdw := newFD(t, w, "file", false, false)
409429
testReadWrite(t, fdr, fdw)
410430
})
411431
}
412432

413433
func TestPipeWriteEOF(t *testing.T) {
414434
t.Parallel()
415-
name, pipe := newPipe(t, false, true)
416-
file := newFile(t, name, false)
435+
name := pipeName()
436+
pipe := newMessagePipe(t, name, false, true)
437+
file := newFile(t, name, false, true)
417438
read := make(chan struct{}, 1)
418439
go func() {
419440
_, err := pipe.Write(nil)
@@ -435,8 +456,9 @@ func TestPipeWriteEOF(t *testing.T) {
435456

436457
func TestPipeCanceled(t *testing.T) {
437458
t.Parallel()
438-
name, _ := newPipe(t, true, false)
439-
file := newFile(t, name, true)
459+
name := pipeName()
460+
_ = newBytePipe(t, name, true, true)
461+
file := newFile(t, name, true, true)
440462
ch := make(chan struct{}, 1)
441463
go func() {
442464
for {
@@ -481,7 +503,7 @@ func benchmarkRead(b *testing.B, overlapped bool) {
481503
if err != nil {
482504
b.Fatal(err)
483505
}
484-
file := newFile(b, name, overlapped)
506+
file := newFile(b, name, overlapped, true)
485507
var buf [len(content)]byte
486508
for b.Loop() {
487509
_, err := io.ReadFull(file, buf[:])

src/internal/syscall/windows/syscall_windows.go

+10
Original file line numberDiff line numberDiff line change
@@ -542,9 +542,19 @@ const (
542542
STATUS_REPARSE_POINT_ENCOUNTERED NTStatus = 0xC000050B
543543
)
544544

545+
const (
546+
FileModeInformation = 16
547+
)
548+
549+
// https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/ns-ntifs-_file_mode_information
550+
type FILE_MODE_INFORMATION struct {
551+
Mode uint32
552+
}
553+
545554
// NT Native APIs
546555
//sys NtCreateFile(handle *syscall.Handle, access uint32, oa *OBJECT_ATTRIBUTES, iosb *IO_STATUS_BLOCK, allocationSize *int64, attributes uint32, share uint32, disposition uint32, options uint32, eabuffer uintptr, ealength uint32) (ntstatus error) = ntdll.NtCreateFile
547556
//sys NtOpenFile(handle *syscall.Handle, access uint32, oa *OBJECT_ATTRIBUTES, iosb *IO_STATUS_BLOCK, share uint32, options uint32) (ntstatus error) = ntdll.NtOpenFile
548557
//sys rtlNtStatusToDosErrorNoTeb(ntstatus NTStatus) (ret syscall.Errno) = ntdll.RtlNtStatusToDosErrorNoTeb
549558
//sys NtSetInformationFile(handle syscall.Handle, iosb *IO_STATUS_BLOCK, inBuffer uintptr, inBufferLen uint32, class uint32) (ntstatus error) = ntdll.NtSetInformationFile
550559
//sys RtlIsDosDeviceName_U(name *uint16) (ret uint32) = ntdll.RtlIsDosDeviceName_U
560+
//sys NtQueryInformationFile(handle syscall.Handle, iosb *IO_STATUS_BLOCK, inBuffer uintptr, inBufferLen uint32, class uint32) (ntstatus error) = ntdll.NtQueryInformationFile

0 commit comments

Comments
 (0)