diff --git a/src/internal/syscall/windows/at_windows.go b/src/internal/syscall/windows/at_windows.go index b7ca8433c2a..de733c523cd 100644 --- a/src/internal/syscall/windows/at_windows.go +++ b/src/internal/syscall/windows/at_windows.go @@ -5,6 +5,7 @@ package windows import ( + "internal/oserror" "runtime" "structs" "syscall" @@ -26,7 +27,6 @@ const ( // to avoid overlap. const ( 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 ) @@ -36,24 +36,54 @@ func Openat(dirfd syscall.Handle, name string, flag uint64, perm uint32) (_ sysc } 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) { case syscall.O_RDONLY: // FILE_GENERIC_READ includes FILE_LIST_DIRECTORY. - access = FILE_GENERIC_READ + access |= FILE_GENERIC_READ case syscall.O_WRONLY: - access = FILE_GENERIC_WRITE + access |= FILE_GENERIC_WRITE options |= FILE_NON_DIRECTORY_FILE case syscall.O_RDWR: - access = FILE_GENERIC_READ | FILE_GENERIC_WRITE + access |= FILE_GENERIC_READ | FILE_GENERIC_WRITE options |= FILE_NON_DIRECTORY_FILE default: // Stat opens files without requesting read or write permissions, // but we still need to request SYNCHRONIZE. - access = SYNCHRONIZE + access |= SYNCHRONIZE } if flag&syscall.O_CREAT != 0 { 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 { access |= FILE_APPEND_DATA // 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 { 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 { 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 // a file with FILE_ATTRIBUTE_READONLY these will replace an existing // file with a new, read-only one. @@ -121,7 +150,7 @@ func Openat(dirfd syscall.Handle, name string, flag uint64, perm uint32) (_ sysc fileAttrs, FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE, disposition, - FILE_SYNCHRONOUS_IO_NONALERT|FILE_OPEN_FOR_BACKUP_INTENT|options, + FILE_OPEN_FOR_BACKUP_INTENT|options, nil, 0, ) diff --git a/src/internal/syscall/windows/types_windows.go b/src/internal/syscall/windows/types_windows.go index 49daf9b31b1..568f94624c4 100644 --- a/src/internal/syscall/windows/types_windows.go +++ b/src/internal/syscall/windows/types_windows.go @@ -210,6 +210,7 @@ const ( FILE_NO_COMPRESSION = 0x00008000 FILE_OPEN_REQUIRING_OPLOCK = 0x00010000 FILE_DISALLOW_EXCLUSIVE = 0x00020000 + FILE_SESSION_AWARE = 0x00040000 FILE_RESERVE_OPFILTER = 0x00100000 FILE_OPEN_REPARSE_POINT = 0x00200000 FILE_OPEN_NO_RECALL = 0x00400000 @@ -286,3 +287,17 @@ const VER_NT_WORKSTATION = 0x0000001 type MemoryBasicInformation = windows.MemoryBasicInformation 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 diff --git a/src/os/root_windows.go b/src/os/root_windows.go index 381e33fc0e5..da995e6d7f5 100644 --- a/src/os/root_windows.go +++ b/src/os/root_windows.go @@ -134,8 +134,8 @@ func rootOpenFileNolog(root *Root, name string, flag int, perm FileMode) (*File, if err != nil { return nil, &PathError{Op: "openat", Path: name, Err: err} } - // openat always returns a non-blocking handle. - return newFile(fd, joinPath(root.Name(), name), kindOpenFile, false), nil + nonblocking := flag&windows.O_FILE_FLAG_OVERLAPPED != 0 + return newFile(fd, joinPath(root.Name(), name), kindOpenFile, nonblocking), nil } 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 } 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 { 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 // // 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. // // 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) // } // } - 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 { 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) { - 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 { return "", err } @@ -391,7 +391,7 @@ func readlinkat(dirfd syscall.Handle, name string) (string, 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 { return 0, err } diff --git a/src/os/root_windows_test.go b/src/os/root_windows_test.go index 47643f98d10..31d54f25716 100644 --- a/src/os/root_windows_test.go +++ b/src/os/root_windows_test.go @@ -9,6 +9,7 @@ package os_test import ( "errors" "fmt" + "internal/strconv" "internal/syscall/windows" "os" "path/filepath" @@ -247,3 +248,89 @@ func TestRootOpenFileTruncateNamedPipe(t *testing.T) { } 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() +} diff --git a/src/syscall/types_windows.go b/src/syscall/types_windows.go index 3c6d18a8509..1bffb2769d2 100644 --- a/src/syscall/types_windows.go +++ b/src/syscall/types_windows.go @@ -54,7 +54,6 @@ const ( o_DIRECTORY = 0x04000 O_CLOEXEC = 0x80000 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 )