]> Cypherpunks repositories - gostls13.git/commitdiff
os,syscall: pass file flags to CreateFile on Windows
authorqmuntal <quimmuntal@gmail.com>
Wed, 27 Aug 2025 09:18:47 +0000 (11:18 +0200)
committerQuim Muntal <quimmuntal@gmail.com>
Fri, 29 Aug 2025 14:25:08 +0000 (07:25 -0700)
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 <dneil@google.com>
Reviewed-by: Cherry Mui <cherryyz@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>

doc/next/6-stdlib/99-minor/os/73676.md [new file with mode: 0644]
src/internal/syscall/windows/nonblocking_windows.go
src/internal/syscall/windows/types_windows.go
src/os/file_windows.go
src/os/os_windows_test.go
src/syscall/syscall_windows.go
src/syscall/types_windows.go

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 (file)
index 0000000..70d01f2
--- /dev/null
@@ -0,0 +1,4 @@
+On Windows, the [OpenFile] `flag` parameter can now contain any combination of\r
+Windows-specific file flags, such as `FILE_FLAG_OVERLAPPED` and\r
+`FILE_FLAG_SEQUENTIAL_SCAN`, for control of file or device caching behavior,\r
+access modes, and other special-purpose flags.
\ No newline at end of file
index ec6f520a8edd193b4eaa91ce53985ae3bafaa8e0..1565e87e2658f75b6dec0a209a26bfbecb973f1e 100644 (file)
@@ -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
index 3855df393d633e03a99ea4ed284b6c47fcf0bfae..cb0a48f0802c7ab9ed6f42e6b4a0a8a5df8824fc 100644 (file)
@@ -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
index 9f96aa8f89bb37b2203e42f6b58c31a443c98679..83f5fde7f9de468eff782ae9d88eb72049ec6848 100644 (file)
@@ -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) {
index 7c5e8ac4a410d877b912dcb9146c275ce113f738..8258c4ff4bc8d11581ee124bb2225eecd82876a8 100644 (file)
@@ -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()
+}
index 6ee2e463ed3d6f3dd392b31daa656546985be037..f86f03e20f0a0fdc605c62d4e07430bed1deabf9 100644 (file)
@@ -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
index 8578763284b8eb043c38d4c6406128aeac0286e7..b40b455e7de9b2091ba2f79bcfc976e7e4c81294 100644 (file)
@@ -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