From 252c9011255872e21284480754400f9be7d48bf9 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Wed, 27 Aug 2025 11:18:47 +0200 Subject: [PATCH] os,syscall: pass file flags to CreateFile on Windows Add support for FILE_FLAG_* constants in the flag argument of os.OpenFile and syscall.Open on Windows. Passing invalid flags will result in an error. Updates #73676 Change-Id: Ie215a3dd14f0d74141533f0a07865a02a67a3846 Reviewed-on: https://go-review.googlesource.com/c/go/+/699415 Reviewed-by: Damien Neil Reviewed-by: Cherry Mui LUCI-TryBot-Result: Go LUCI --- doc/next/6-stdlib/99-minor/os/73676.md | 4 + .../syscall/windows/nonblocking_windows.go | 2 +- src/internal/syscall/windows/types_windows.go | 16 +++ src/os/file_windows.go | 4 +- src/os/os_windows_test.go | 101 +++++++++++++----- src/syscall/syscall_windows.go | 11 +- src/syscall/types_windows.go | 24 +++++ 7 files changed, 131 insertions(+), 31 deletions(-) create mode 100644 doc/next/6-stdlib/99-minor/os/73676.md diff --git a/doc/next/6-stdlib/99-minor/os/73676.md b/doc/next/6-stdlib/99-minor/os/73676.md new file mode 100644 index 0000000000..70d01f262d --- /dev/null +++ b/doc/next/6-stdlib/99-minor/os/73676.md @@ -0,0 +1,4 @@ +On Windows, the [OpenFile] `flag` parameter can now contain any combination of +Windows-specific file flags, such as `FILE_FLAG_OVERLAPPED` and +`FILE_FLAG_SEQUENTIAL_SCAN`, for control of file or device caching behavior, +access modes, and other special-purpose flags. \ No newline at end of file diff --git a/src/internal/syscall/windows/nonblocking_windows.go b/src/internal/syscall/windows/nonblocking_windows.go index ec6f520a8e..1565e87e26 100644 --- a/src/internal/syscall/windows/nonblocking_windows.go +++ b/src/internal/syscall/windows/nonblocking_windows.go @@ -10,7 +10,7 @@ import ( ) // IsNonblock returns whether the file descriptor fd is opened -// in non-blocking mode, that is, the [syscall.FILE_FLAG_OVERLAPPED] flag +// in non-blocking mode, that is, the [windows.O_FILE_FLAG_OVERLAPPED] flag // was set when the file was opened. func IsNonblock(fd syscall.Handle) (nonblocking bool, err error) { var info FILE_MODE_INFORMATION diff --git a/src/internal/syscall/windows/types_windows.go b/src/internal/syscall/windows/types_windows.go index 3855df393d..cb0a48f080 100644 --- a/src/internal/syscall/windows/types_windows.go +++ b/src/internal/syscall/windows/types_windows.go @@ -164,6 +164,22 @@ type SECURITY_QUALITY_OF_SERVICE struct { EffectiveOnly byte } +// File flags for [os.OpenFile]. The O_ prefix is used to indicate +// that these flags are specific to the OpenFile function. +const ( + O_FILE_FLAG_OPEN_NO_RECALL = 0x00100000 + O_FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000 + O_FILE_FLAG_SESSION_AWARE = 0x00800000 + O_FILE_FLAG_POSIX_SEMANTICS = 0x01000000 + O_FILE_FLAG_BACKUP_SEMANTICS = 0x02000000 + O_FILE_FLAG_DELETE_ON_CLOSE = 0x04000000 + O_FILE_FLAG_SEQUENTIAL_SCAN = 0x08000000 + O_FILE_FLAG_RANDOM_ACCESS = 0x10000000 + O_FILE_FLAG_NO_BUFFERING = 0x20000000 + O_FILE_FLAG_OVERLAPPED = 0x40000000 + O_FILE_FLAG_WRITE_THROUGH = 0x80000000 +) + const ( // CreateDisposition flags for NtCreateFile and NtCreateNamedPipeFile. FILE_SUPERSEDE = 0x00000000 diff --git a/src/os/file_windows.go b/src/os/file_windows.go index 9f96aa8f89..83f5fde7f9 100644 --- a/src/os/file_windows.go +++ b/src/os/file_windows.go @@ -120,8 +120,8 @@ func openFileNolog(name string, flag int, perm FileMode) (*File, error) { if err != nil { return nil, &PathError{Op: "open", Path: name, Err: err} } - // syscall.Open always returns a non-blocking handle. - return newFile(r, name, "file", false), nil + nonblocking := flag&windows.O_FILE_FLAG_OVERLAPPED != 0 + return newFile(r, name, "file", nonblocking), nil } func openDirNolog(name string) (*File, error) { diff --git a/src/os/os_windows_test.go b/src/os/os_windows_test.go index 7c5e8ac4a4..8258c4ff4b 100644 --- a/src/os/os_windows_test.go +++ b/src/os/os_windows_test.go @@ -1571,15 +1571,10 @@ func TestReadWriteFileOverlapped(t *testing.T) { t.Parallel() name := filepath.Join(t.TempDir(), "test.txt") - wname, err := syscall.UTF16PtrFromString(name) - if err != nil { - t.Fatal(err) - } - h, err := syscall.CreateFile(wname, syscall.GENERIC_ALL, 0, nil, syscall.CREATE_NEW, syscall.FILE_ATTRIBUTE_NORMAL|syscall.FILE_FLAG_OVERLAPPED, 0) + f, err := os.OpenFile(name, os.O_RDWR|os.O_CREATE|windows.O_FILE_FLAG_OVERLAPPED, 0666) if err != nil { t.Fatal(err) } - f := os.NewFile(uintptr(h), name) defer f.Close() data := []byte("test") @@ -1655,22 +1650,14 @@ func TestStdinOverlappedPipe(t *testing.T) { } func newFileOverlapped(t testing.TB, name string, overlapped bool) *os.File { - namep, err := syscall.UTF16PtrFromString(name) - if err != nil { - t.Fatal(err) - } - flags := syscall.FILE_ATTRIBUTE_NORMAL + flags := os.O_RDWR | os.O_CREATE if overlapped { - flags |= syscall.FILE_FLAG_OVERLAPPED + flags |= windows.O_FILE_FLAG_OVERLAPPED } - h, err := syscall.CreateFile(namep, - syscall.GENERIC_READ|syscall.GENERIC_WRITE, - syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_READ, - nil, syscall.OPEN_ALWAYS, uint32(flags), 0) + f, err := os.OpenFile(name, flags, 0666) if err != nil { t.Fatal(err) } - f := os.NewFile(uintptr(h), name) t.Cleanup(func() { if err := f.Close(); err != nil && !errors.Is(err, os.ErrClosed) { t.Fatal(err) @@ -1706,7 +1693,7 @@ func newPipe(t testing.TB, name string, message, overlapped bool) *os.File { // Create the read handle. flags := windows.PIPE_ACCESS_DUPLEX if overlapped { - flags |= syscall.FILE_FLAG_OVERLAPPED + flags |= windows.O_FILE_FLAG_OVERLAPPED } typ := windows.PIPE_TYPE_BYTE | windows.PIPE_READMODE_BYTE if message { @@ -1888,21 +1875,13 @@ func TestFileOverlappedReadAtVolume(t *testing.T) { // See https://go.dev/issues/74951. t.Parallel() name := `\\.\` + filepath.VolumeName(t.TempDir()) - namep, err := syscall.UTF16PtrFromString(name) - if err != nil { - t.Fatal(err) - } - h, err := syscall.CreateFile(namep, - syscall.GENERIC_READ|syscall.GENERIC_WRITE, - syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_READ, - nil, syscall.OPEN_ALWAYS, syscall.FILE_FLAG_OVERLAPPED, 0) + f, err := os.OpenFile(name, os.O_RDWR|os.O_CREATE|windows.O_FILE_FLAG_OVERLAPPED, 0666) if err != nil { if errors.Is(err, syscall.ERROR_ACCESS_DENIED) { t.Skip("skipping test: access denied") } t.Fatal(err) } - f := os.NewFile(uintptr(h), name) defer f.Close() var buf [0]byte @@ -2209,3 +2188,71 @@ func TestSplitPath(t *testing.T) { } } } + +func TestOpenFileFlags(t *testing.T) { + t.Parallel() + + // The only way to retrieve some of the flags passed in CreateFile + // is using NtQueryInformationFile, which returns the file flags + // NT equivalent. Note that FILE_SYNCHRONOUS_IO_NONALERT is always + // set when FILE_FLAG_OVERLAPPED is not passed. + // The flags that can't be retrieved using NtQueryInformationFile won't + // be tested in here, but we at least know that the logic to handle them is correct. + tests := []struct { + flag uint32 + wantMode uint32 + }{ + {0, windows.FILE_SYNCHRONOUS_IO_NONALERT}, + {windows.O_FILE_FLAG_OVERLAPPED, 0}, + {windows.O_FILE_FLAG_NO_BUFFERING, windows.FILE_NO_INTERMEDIATE_BUFFERING | windows.FILE_SYNCHRONOUS_IO_NONALERT}, + {windows.O_FILE_FLAG_NO_BUFFERING | windows.O_FILE_FLAG_OVERLAPPED, windows.FILE_NO_INTERMEDIATE_BUFFERING}, + {windows.O_FILE_FLAG_SEQUENTIAL_SCAN, windows.FILE_SEQUENTIAL_ONLY | windows.FILE_SYNCHRONOUS_IO_NONALERT}, + {windows.O_FILE_FLAG_WRITE_THROUGH, windows.FILE_WRITE_THROUGH | windows.FILE_SYNCHRONOUS_IO_NONALERT}, + } + for i, tt := range tests { + t.Run(strconv.Itoa(i), func(t *testing.T) { + t.Parallel() + f, err := os.OpenFile(filepath.Join(t.TempDir(), "test.txt"), syscall.O_RDWR|syscall.O_CREAT|int(tt.flag), 0666) + if err != nil { + t.Fatal(err) + } + defer f.Close() + var info windows.FILE_MODE_INFORMATION + if err := windows.NtQueryInformationFile(syscall.Handle(f.Fd()), &windows.IO_STATUS_BLOCK{}, + unsafe.Pointer(&info), uint32(unsafe.Sizeof(info)), windows.FileModeInformation); err != nil { + t.Fatal(err) + } + if info.Mode != tt.wantMode { + t.Errorf("file mode = 0x%x; want 0x%x", info.Mode, tt.wantMode) + } + }) + } +} + +func TestOpenFileDeleteOnClose(t *testing.T) { + t.Parallel() + name := filepath.Join(t.TempDir(), "test.txt") + f, err := os.OpenFile(name, syscall.O_RDWR|syscall.O_CREAT|windows.O_FILE_FLAG_DELETE_ON_CLOSE, 0666) + if err != nil { + t.Fatal(err) + } + if err := f.Close(); err != nil { + t.Fatal(err) + } + // The file should be deleted after closing. + if _, err := os.Stat(name); !errors.Is(err, os.ErrNotExist) { + t.Errorf("expected file to be deleted, got %v", err) + } +} + +func TestOpenFileFlagInvalid(t *testing.T) { + t.Parallel() + // invalidFileFlag is the only value in the file flag range that is not supported, + // as it is not defined in the Windows API. + const invalidFileFlag = 0x00400000 + f, err := os.OpenFile(filepath.Join(t.TempDir(), "test.txt"), syscall.O_RDWR|syscall.O_CREAT|invalidFileFlag, 0666) + if !errors.Is(err, os.ErrInvalid) { + t.Fatalf("expected os.ErrInvalid, got %v", err) + } + f.Close() +} diff --git a/src/syscall/syscall_windows.go b/src/syscall/syscall_windows.go index 6ee2e463ed..f86f03e20f 100644 --- a/src/syscall/syscall_windows.go +++ b/src/syscall/syscall_windows.go @@ -401,6 +401,16 @@ func Open(name string, flag int, perm uint32) (fd Handle, err error) { if perm&S_IWRITE == 0 { attrs = FILE_ATTRIBUTE_READONLY } + // fileFlags contains the high 12 bits of flag. + // This bit range can be used by the caller to specify the file flags + // passed to CreateFile. It is an error to use if the bits can't be + // mapped to the supported FILE_FLAG_* constants. + if fileFlags := uint32(flag) & fileFlagsMask; fileFlags&^validFileFlagsMask == 0 { + attrs |= fileFlags + } else { + return InvalidHandle, oserror.ErrInvalid + } + switch accessFlags { case O_WRONLY, O_RDWR: // Unix doesn't allow opening a directory with O_WRONLY @@ -414,7 +424,6 @@ func Open(name string, flag int, perm uint32) (fd Handle, err error) { attrs |= FILE_FLAG_BACKUP_SEMANTICS } if flag&O_SYNC != 0 { - const _FILE_FLAG_WRITE_THROUGH = 0x80000000 attrs |= _FILE_FLAG_WRITE_THROUGH } // We don't use CREATE_ALWAYS, because when opening a file with diff --git a/src/syscall/types_windows.go b/src/syscall/types_windows.go index 8578763284..b40b455e7d 100644 --- a/src/syscall/types_windows.go +++ b/src/syscall/types_windows.go @@ -89,6 +89,20 @@ var signals = [...]string{ 15: "terminated", } +const fileFlagsMask = 0xFFF00000 + +const validFileFlagsMask = FILE_FLAG_OPEN_REPARSE_POINT | + FILE_FLAG_BACKUP_SEMANTICS | + FILE_FLAG_OVERLAPPED | + _FILE_FLAG_OPEN_NO_RECALL | + _FILE_FLAG_SESSION_AWARE | + _FILE_FLAG_POSIX_SEMANTICS | + _FILE_FLAG_DELETE_ON_CLOSE | + _FILE_FLAG_SEQUENTIAL_SCAN | + _FILE_FLAG_NO_BUFFERING | + _FILE_FLAG_RANDOM_ACCESS | + _FILE_FLAG_WRITE_THROUGH + const ( GENERIC_READ = 0x80000000 GENERIC_WRITE = 0x40000000 @@ -119,9 +133,19 @@ const ( OPEN_ALWAYS = 4 TRUNCATE_EXISTING = 5 + // The following flags are supported by [Open] + // and exported in [golang.org/x/sys/windows]. + _FILE_FLAG_OPEN_NO_RECALL = 0x00100000 FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000 + _FILE_FLAG_SESSION_AWARE = 0x00800000 + _FILE_FLAG_POSIX_SEMANTICS = 0x01000000 FILE_FLAG_BACKUP_SEMANTICS = 0x02000000 + _FILE_FLAG_DELETE_ON_CLOSE = 0x04000000 + _FILE_FLAG_SEQUENTIAL_SCAN = 0x08000000 + _FILE_FLAG_RANDOM_ACCESS = 0x10000000 + _FILE_FLAG_NO_BUFFERING = 0x20000000 FILE_FLAG_OVERLAPPED = 0x40000000 + _FILE_FLAG_WRITE_THROUGH = 0x80000000 HANDLE_FLAG_INHERIT = 0x00000001 STARTF_USESTDHANDLES = 0x00000100 -- 2.52.0