os,internal/syscall/windows: support O_* flags in Root.OpenFile

These file flags are supported by os.OpenFile since CL 699415.

Closes #73676

Change-Id: Ib37102a565f538d394d2a94bd605d6c6004f3028
Reviewed-on: https://go-review.googlesource.com/c/go/+/724621
Auto-Submit: Damien Neil <dneil@google.com>
Reviewed-by: Damien Neil <dneil@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Cherry Mui <cherryyz@google.com>
This commit is contained in:
qmuntal 2025-11-26 12:11:50 +01:00 committed by Gopher Robot
parent e0a4dffb0c
commit b194f5d24a
5 changed files with 148 additions and 18 deletions

View file

@ -5,6 +5,7 @@
package windows package windows
import ( import (
"internal/oserror"
"runtime" "runtime"
"structs" "structs"
"syscall" "syscall"
@ -26,7 +27,6 @@ const (
// to avoid overlap. // to avoid overlap.
const ( const (
O_NOFOLLOW_ANY = 0x200000000 // disallow symlinks anywhere in the path O_NOFOLLOW_ANY = 0x200000000 // disallow symlinks anywhere in the path
O_OPEN_REPARSE = 0x400000000 // FILE_OPEN_REPARSE_POINT, used by Lstat
O_WRITE_ATTRS = 0x800000000 // FILE_WRITE_ATTRIBUTES, used by Chmod O_WRITE_ATTRS = 0x800000000 // FILE_WRITE_ATTRIBUTES, used by Chmod
) )
@ -36,24 +36,54 @@ func Openat(dirfd syscall.Handle, name string, flag uint64, perm uint32) (_ sysc
} }
var access, options uint32 var access, options uint32
// Map Win32 file flags to NT create options.
fileFlags := uint32(flag) & FileFlagsMask
if fileFlags&^ValidFileFlagsMask != 0 {
return syscall.InvalidHandle, oserror.ErrInvalid
}
if fileFlags&O_FILE_FLAG_OVERLAPPED == 0 {
options |= FILE_SYNCHRONOUS_IO_NONALERT
}
if fileFlags&O_FILE_FLAG_DELETE_ON_CLOSE != 0 {
access |= DELETE
}
setOptionFlag := func(ntFlag, win32Flag uint32) {
if fileFlags&win32Flag != 0 {
options |= ntFlag
}
}
setOptionFlag(FILE_NO_INTERMEDIATE_BUFFERING, O_FILE_FLAG_NO_BUFFERING)
setOptionFlag(FILE_WRITE_THROUGH, O_FILE_FLAG_WRITE_THROUGH)
setOptionFlag(FILE_SEQUENTIAL_ONLY, O_FILE_FLAG_SEQUENTIAL_SCAN)
setOptionFlag(FILE_RANDOM_ACCESS, O_FILE_FLAG_RANDOM_ACCESS)
setOptionFlag(FILE_OPEN_FOR_BACKUP_INTENT, O_FILE_FLAG_BACKUP_SEMANTICS)
setOptionFlag(FILE_SESSION_AWARE, O_FILE_FLAG_SESSION_AWARE)
setOptionFlag(FILE_DELETE_ON_CLOSE, O_FILE_FLAG_DELETE_ON_CLOSE)
setOptionFlag(FILE_OPEN_NO_RECALL, O_FILE_FLAG_OPEN_NO_RECALL)
setOptionFlag(FILE_OPEN_REPARSE_POINT, O_FILE_FLAG_OPEN_REPARSE_POINT)
switch flag & (syscall.O_RDONLY | syscall.O_WRONLY | syscall.O_RDWR) { switch flag & (syscall.O_RDONLY | syscall.O_WRONLY | syscall.O_RDWR) {
case syscall.O_RDONLY: case syscall.O_RDONLY:
// FILE_GENERIC_READ includes FILE_LIST_DIRECTORY. // FILE_GENERIC_READ includes FILE_LIST_DIRECTORY.
access = FILE_GENERIC_READ access |= FILE_GENERIC_READ
case syscall.O_WRONLY: case syscall.O_WRONLY:
access = FILE_GENERIC_WRITE access |= FILE_GENERIC_WRITE
options |= FILE_NON_DIRECTORY_FILE options |= FILE_NON_DIRECTORY_FILE
case syscall.O_RDWR: case syscall.O_RDWR:
access = FILE_GENERIC_READ | FILE_GENERIC_WRITE access |= FILE_GENERIC_READ | FILE_GENERIC_WRITE
options |= FILE_NON_DIRECTORY_FILE options |= FILE_NON_DIRECTORY_FILE
default: default:
// Stat opens files without requesting read or write permissions, // Stat opens files without requesting read or write permissions,
// but we still need to request SYNCHRONIZE. // but we still need to request SYNCHRONIZE.
access = SYNCHRONIZE access |= SYNCHRONIZE
} }
if flag&syscall.O_CREAT != 0 { if flag&syscall.O_CREAT != 0 {
access |= FILE_GENERIC_WRITE access |= FILE_GENERIC_WRITE
} }
if fileFlags&O_FILE_FLAG_NO_BUFFERING != 0 {
// Disable buffering implies no implicit append access.
access &^= FILE_APPEND_DATA
}
if flag&syscall.O_APPEND != 0 { if flag&syscall.O_APPEND != 0 {
access |= FILE_APPEND_DATA access |= FILE_APPEND_DATA
// Remove FILE_WRITE_DATA access unless O_TRUNC is set, // Remove FILE_WRITE_DATA access unless O_TRUNC is set,
@ -82,14 +112,13 @@ func Openat(dirfd syscall.Handle, name string, flag uint64, perm uint32) (_ sysc
if flag&syscall.O_CLOEXEC == 0 { if flag&syscall.O_CLOEXEC == 0 {
objAttrs.Attributes |= OBJ_INHERIT objAttrs.Attributes |= OBJ_INHERIT
} }
if fileFlags&O_FILE_FLAG_POSIX_SEMANTICS == 0 {
objAttrs.Attributes |= OBJ_CASE_INSENSITIVE
}
if err := objAttrs.init(dirfd, name); err != nil { if err := objAttrs.init(dirfd, name); err != nil {
return syscall.InvalidHandle, err return syscall.InvalidHandle, err
} }
if flag&O_OPEN_REPARSE != 0 {
options |= FILE_OPEN_REPARSE_POINT
}
// We don't use FILE_OVERWRITE/FILE_OVERWRITE_IF, because when opening // We don't use FILE_OVERWRITE/FILE_OVERWRITE_IF, because when opening
// a file with FILE_ATTRIBUTE_READONLY these will replace an existing // a file with FILE_ATTRIBUTE_READONLY these will replace an existing
// file with a new, read-only one. // file with a new, read-only one.
@ -121,7 +150,7 @@ func Openat(dirfd syscall.Handle, name string, flag uint64, perm uint32) (_ sysc
fileAttrs, fileAttrs,
FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE, FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE,
disposition, disposition,
FILE_SYNCHRONOUS_IO_NONALERT|FILE_OPEN_FOR_BACKUP_INTENT|options, FILE_OPEN_FOR_BACKUP_INTENT|options,
nil, nil,
0, 0,
) )

View file

@ -210,6 +210,7 @@ const (
FILE_NO_COMPRESSION = 0x00008000 FILE_NO_COMPRESSION = 0x00008000
FILE_OPEN_REQUIRING_OPLOCK = 0x00010000 FILE_OPEN_REQUIRING_OPLOCK = 0x00010000
FILE_DISALLOW_EXCLUSIVE = 0x00020000 FILE_DISALLOW_EXCLUSIVE = 0x00020000
FILE_SESSION_AWARE = 0x00040000
FILE_RESERVE_OPFILTER = 0x00100000 FILE_RESERVE_OPFILTER = 0x00100000
FILE_OPEN_REPARSE_POINT = 0x00200000 FILE_OPEN_REPARSE_POINT = 0x00200000
FILE_OPEN_NO_RECALL = 0x00400000 FILE_OPEN_NO_RECALL = 0x00400000
@ -286,3 +287,17 @@ const VER_NT_WORKSTATION = 0x0000001
type MemoryBasicInformation = windows.MemoryBasicInformation type MemoryBasicInformation = windows.MemoryBasicInformation
type Context = windows.Context type Context = windows.Context
const FileFlagsMask = 0xFFF00000
const ValidFileFlagsMask = O_FILE_FLAG_OPEN_REPARSE_POINT |
O_FILE_FLAG_BACKUP_SEMANTICS |
O_FILE_FLAG_OVERLAPPED |
O_FILE_FLAG_OPEN_NO_RECALL |
O_FILE_FLAG_SESSION_AWARE |
O_FILE_FLAG_POSIX_SEMANTICS |
O_FILE_FLAG_DELETE_ON_CLOSE |
O_FILE_FLAG_SEQUENTIAL_SCAN |
O_FILE_FLAG_NO_BUFFERING |
O_FILE_FLAG_RANDOM_ACCESS |
O_FILE_FLAG_WRITE_THROUGH

View file

@ -134,8 +134,8 @@ func rootOpenFileNolog(root *Root, name string, flag int, perm FileMode) (*File,
if err != nil { if err != nil {
return nil, &PathError{Op: "openat", Path: name, Err: err} return nil, &PathError{Op: "openat", Path: name, Err: err}
} }
// openat always returns a non-blocking handle. nonblocking := flag&windows.O_FILE_FLAG_OVERLAPPED != 0
return newFile(fd, joinPath(root.Name(), name), kindOpenFile, false), nil return newFile(fd, joinPath(root.Name(), name), kindOpenFile, nonblocking), nil
} }
func openat(dirfd syscall.Handle, name string, flag uint64, perm FileMode) (syscall.Handle, error) { func openat(dirfd syscall.Handle, name string, flag uint64, perm FileMode) (syscall.Handle, error) {
@ -213,7 +213,7 @@ func rootStat(r *Root, name string, lstat bool) (FileInfo, error) {
lstat = false lstat = false
} }
fi, err := doInRoot(r, name, nil, func(parent syscall.Handle, n string) (FileInfo, error) { fi, err := doInRoot(r, name, nil, func(parent syscall.Handle, n string) (FileInfo, error) {
fd, err := openat(parent, n, windows.O_OPEN_REPARSE, 0) fd, err := openat(parent, n, windows.O_FILE_FLAG_OPEN_REPARSE_POINT, 0)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -290,7 +290,7 @@ func chmodat(parent syscall.Handle, name string, mode FileMode) error {
// This may or may not be the desired behavior: https://go.dev/issue/71492 // This may or may not be the desired behavior: https://go.dev/issue/71492
// //
// For now, be consistent with os.Symlink. // For now, be consistent with os.Symlink.
// Passing O_OPEN_REPARSE causes us to open the named file itself, // Passing O_FILE_FLAG_OPEN_REPARSE_POINT causes us to open the named file itself,
// not any file that it links to. // not any file that it links to.
// //
// If we want to change this in the future, pass O_NOFOLLOW_ANY instead // If we want to change this in the future, pass O_NOFOLLOW_ANY instead
@ -301,7 +301,7 @@ func chmodat(parent syscall.Handle, name string, mode FileMode) error {
// return errSymlink(link) // return errSymlink(link)
// } // }
// } // }
h, err := windows.Openat(parent, name, syscall.O_CLOEXEC|windows.O_OPEN_REPARSE|windows.O_WRITE_ATTRS, 0) h, err := windows.Openat(parent, name, syscall.O_CLOEXEC|windows.O_FILE_FLAG_OPEN_REPARSE_POINT|windows.O_WRITE_ATTRS, 0)
if err != nil { if err != nil {
return err return err
} }
@ -382,7 +382,7 @@ func linkat(oldfd syscall.Handle, oldname string, newfd syscall.Handle, newname
} }
func readlinkat(dirfd syscall.Handle, name string) (string, error) { func readlinkat(dirfd syscall.Handle, name string) (string, error) {
fd, err := openat(dirfd, name, windows.O_OPEN_REPARSE, 0) fd, err := openat(dirfd, name, windows.O_FILE_FLAG_OPEN_REPARSE_POINT, 0)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -391,7 +391,7 @@ func readlinkat(dirfd syscall.Handle, name string) (string, error) {
} }
func modeAt(parent syscall.Handle, name string) (FileMode, error) { func modeAt(parent syscall.Handle, name string) (FileMode, error) {
fd, err := openat(parent, name, windows.O_OPEN_REPARSE|windows.O_DIRECTORY, 0) fd, err := openat(parent, name, windows.O_FILE_FLAG_OPEN_REPARSE_POINT|windows.O_DIRECTORY, 0)
if err != nil { if err != nil {
return 0, err return 0, err
} }

View file

@ -9,6 +9,7 @@ package os_test
import ( import (
"errors" "errors"
"fmt" "fmt"
"internal/strconv"
"internal/syscall/windows" "internal/syscall/windows"
"os" "os"
"path/filepath" "path/filepath"
@ -247,3 +248,89 @@ func TestRootOpenFileTruncateNamedPipe(t *testing.T) {
} }
f.Close() f.Close()
} }
func TestRootOpenFileFlags(t *testing.T) {
t.Parallel()
dir := t.TempDir()
root, err := os.OpenRoot(dir)
if err != nil {
t.Fatal(err)
}
defer root.Close()
// 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) {
f, err := root.OpenFile(strconv.Itoa(i)+".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 TestRootOpenFileDeleteOnClose(t *testing.T) {
t.Parallel()
dir := t.TempDir()
root, err := os.OpenRoot(dir)
if err != nil {
t.Fatal(err)
}
defer root.Close()
const name = "test.txt"
f, err := root.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(filepath.Join(dir, name)); !errors.Is(err, os.ErrNotExist) {
t.Errorf("expected file to be deleted, got %v", err)
}
}
func TestRootOpenFileFlagInvalid(t *testing.T) {
t.Parallel()
dir := t.TempDir()
root, err := os.OpenRoot(dir)
if err != nil {
t.Fatal(err)
}
defer root.Close()
// 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 := root.OpenFile("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()
}

View file

@ -54,7 +54,6 @@ const (
o_DIRECTORY = 0x04000 o_DIRECTORY = 0x04000
O_CLOEXEC = 0x80000 O_CLOEXEC = 0x80000
o_NOFOLLOW_ANY = 0x200000000 // used by internal/syscall/windows o_NOFOLLOW_ANY = 0x200000000 // used by internal/syscall/windows
o_OPEN_REPARSE = 0x400000000 // used by internal/syscall/windows
o_WRITE_ATTRS = 0x800000000 // used by internal/syscall/windows o_WRITE_ATTRS = 0x800000000 // used by internal/syscall/windows
) )