blob: 8dc3ac0e40fc6799967ff0974518449533875dd1 [file] [log] [blame]
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package windows
import (
"runtime"
"structs"
"syscall"
"unsafe"
)
// Openat flags supported by syscall.Open.
const (
O_DIRECTORY = 0x04000 // target must be a directory
)
// Openat flags not supported by syscall.Open.
//
// These are invented values, use values in the 33-63 bit range
// to avoid overlap with flags and attributes supported by [syscall.Open].
//
// When adding a new flag here, add an unexported version to
// the set of invented O_ values in syscall/types_windows.go
// 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
)
func Openat(dirfd syscall.Handle, name string, flag uint64, perm uint32) (_ syscall.Handle, e1 error) {
if len(name) == 0 {
return syscall.InvalidHandle, syscall.ERROR_FILE_NOT_FOUND
}
var access, options uint32
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
case syscall.O_WRONLY:
access = FILE_GENERIC_WRITE
options |= FILE_NON_DIRECTORY_FILE
case syscall.O_RDWR:
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
}
if flag&syscall.O_CREAT != 0 {
access |= FILE_GENERIC_WRITE
}
if flag&syscall.O_APPEND != 0 {
access |= FILE_APPEND_DATA
// Remove FILE_WRITE_DATA access unless O_TRUNC is set,
// in which case we need it to truncate the file.
if flag&syscall.O_TRUNC == 0 {
access &^= FILE_WRITE_DATA
}
}
if flag&O_DIRECTORY != 0 {
options |= FILE_DIRECTORY_FILE
access |= FILE_LIST_DIRECTORY
}
if flag&syscall.O_SYNC != 0 {
options |= FILE_WRITE_THROUGH
}
if flag&O_WRITE_ATTRS != 0 {
access |= FILE_WRITE_ATTRIBUTES
}
// Allow File.Stat.
access |= STANDARD_RIGHTS_READ | FILE_READ_ATTRIBUTES | FILE_READ_EA
objAttrs := &OBJECT_ATTRIBUTES{}
if flag&O_NOFOLLOW_ANY != 0 {
objAttrs.Attributes |= OBJ_DONT_REPARSE
}
if flag&syscall.O_CLOEXEC == 0 {
objAttrs.Attributes |= OBJ_INHERIT
}
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.
//
// Instead, we ftruncate the file after opening when O_TRUNC is set.
var disposition uint32
switch {
case flag&(syscall.O_CREAT|syscall.O_EXCL) == (syscall.O_CREAT | syscall.O_EXCL):
disposition = FILE_CREATE
options |= FILE_OPEN_REPARSE_POINT // don't follow symlinks
case flag&syscall.O_CREAT == syscall.O_CREAT:
disposition = FILE_OPEN_IF
default:
disposition = FILE_OPEN
}
fileAttrs := uint32(FILE_ATTRIBUTE_NORMAL)
if perm&syscall.S_IWRITE == 0 {
fileAttrs = FILE_ATTRIBUTE_READONLY
}
var h syscall.Handle
err := NtCreateFile(
&h,
SYNCHRONIZE|access,
objAttrs,
&IO_STATUS_BLOCK{},
nil,
fileAttrs,
FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE,
disposition,
FILE_SYNCHRONOUS_IO_NONALERT|FILE_OPEN_FOR_BACKUP_INTENT|options,
nil,
0,
)
if err != nil {
return h, ntCreateFileError(err, flag)
}
if flag&syscall.O_TRUNC != 0 {
err = syscall.Ftruncate(h, 0)
if err != nil {
syscall.CloseHandle(h)
return syscall.InvalidHandle, err
}
}
return h, nil
}
// ntCreateFileError maps error returns from NTCreateFile to user-visible errors.
func ntCreateFileError(err error, flag uint64) error {
s, ok := err.(NTStatus)
if !ok {
// Shouldn't really be possible, NtCreateFile always returns NTStatus.
return err
}
switch s {
case STATUS_REPARSE_POINT_ENCOUNTERED:
return syscall.ELOOP
case STATUS_NOT_A_DIRECTORY:
// ENOTDIR is the errno returned by open when O_DIRECTORY is specified
// and the target is not a directory.
//
// NtCreateFile can return STATUS_NOT_A_DIRECTORY under other circumstances,
// such as when opening "file/" where "file" is not a directory.
// (This might be Windows version dependent.)
//
// Only map STATUS_NOT_A_DIRECTORY to ENOTDIR when O_DIRECTORY is specified.
if flag&O_DIRECTORY != 0 {
return syscall.ENOTDIR
}
case STATUS_FILE_IS_A_DIRECTORY:
return syscall.EISDIR
case STATUS_OBJECT_NAME_COLLISION:
return syscall.EEXIST
}
return s.Errno()
}
func Mkdirat(dirfd syscall.Handle, name string, mode uint32) error {
objAttrs := &OBJECT_ATTRIBUTES{}
if err := objAttrs.init(dirfd, name); err != nil {
return err
}
var h syscall.Handle
err := NtCreateFile(
&h,
FILE_GENERIC_READ,
objAttrs,
&IO_STATUS_BLOCK{},
nil,
syscall.FILE_ATTRIBUTE_NORMAL,
syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE,
FILE_CREATE,
FILE_DIRECTORY_FILE,
nil,
0,
)
if err != nil {
return ntCreateFileError(err, 0)
}
syscall.CloseHandle(h)
return nil
}
func Deleteat(dirfd syscall.Handle, name string, options uint32) error {
if name == "." {
// NtOpenFile's documentation isn't explicit about what happens when deleting ".".
// Make this an error consistent with that of POSIX.
return syscall.EINVAL
}
objAttrs := &OBJECT_ATTRIBUTES{}
if err := objAttrs.init(dirfd, name); err != nil {
return err
}
var h syscall.Handle
err := NtOpenFile(
&h,
SYNCHRONIZE|DELETE,
objAttrs,
&IO_STATUS_BLOCK{},
FILE_SHARE_DELETE|FILE_SHARE_READ|FILE_SHARE_WRITE,
FILE_OPEN_REPARSE_POINT|FILE_OPEN_FOR_BACKUP_INTENT|FILE_SYNCHRONOUS_IO_NONALERT|options,
)
if err != nil {
return ntCreateFileError(err, 0)
}
defer syscall.CloseHandle(h)
const (
FileDispositionInformation = 13
FileDispositionInformationEx = 64
)
// First, attempt to delete the file using POSIX semantics
// (which permit a file to be deleted while it is still open).
// This matches the behavior of DeleteFileW.
err = NtSetInformationFile(
h,
&IO_STATUS_BLOCK{},
unsafe.Pointer(&FILE_DISPOSITION_INFORMATION_EX{
Flags: FILE_DISPOSITION_DELETE |
FILE_DISPOSITION_FORCE_IMAGE_SECTION_CHECK |
FILE_DISPOSITION_POSIX_SEMANTICS |
// This differs from DeleteFileW, but matches os.Remove's
// behavior on Unix platforms of permitting deletion of
// read-only files.
FILE_DISPOSITION_IGNORE_READONLY_ATTRIBUTE,
}),
uint32(unsafe.Sizeof(FILE_DISPOSITION_INFORMATION_EX{})),
FileDispositionInformationEx,
)
switch err {
case nil:
return nil
case STATUS_CANNOT_DELETE, STATUS_DIRECTORY_NOT_EMPTY:
return err.(NTStatus).Errno()
}
// If the prior deletion failed, the filesystem either doesn't support
// POSIX semantics (for example, FAT), or hasn't implemented
// FILE_DISPOSITION_INFORMATION_EX.
//
// Try again.
err = NtSetInformationFile(
h,
&IO_STATUS_BLOCK{},
unsafe.Pointer(&FILE_DISPOSITION_INFORMATION{
DeleteFile: true,
}),
uint32(unsafe.Sizeof(FILE_DISPOSITION_INFORMATION{})),
FileDispositionInformation,
)
if st, ok := err.(NTStatus); ok {
return st.Errno()
}
return err
}
func Renameat(olddirfd syscall.Handle, oldpath string, newdirfd syscall.Handle, newpath string) error {
objAttrs := &OBJECT_ATTRIBUTES{}
if err := objAttrs.init(olddirfd, oldpath); err != nil {
return err
}
var h syscall.Handle
err := NtOpenFile(
&h,
SYNCHRONIZE|DELETE,
objAttrs,
&IO_STATUS_BLOCK{},
FILE_SHARE_DELETE|FILE_SHARE_READ|FILE_SHARE_WRITE,
FILE_OPEN_REPARSE_POINT|FILE_OPEN_FOR_BACKUP_INTENT|FILE_SYNCHRONOUS_IO_NONALERT,
)
if err != nil {
return ntCreateFileError(err, 0)
}
defer syscall.CloseHandle(h)
renameInfoEx := FILE_RENAME_INFORMATION_EX{
Flags: FILE_RENAME_REPLACE_IF_EXISTS |
FILE_RENAME_POSIX_SEMANTICS,
RootDirectory: newdirfd,
}
p16, err := syscall.UTF16FromString(newpath)
if err != nil {
return err
}
if len(p16) > len(renameInfoEx.FileName) {
return syscall.EINVAL
}
copy(renameInfoEx.FileName[:], p16)
renameInfoEx.FileNameLength = uint32((len(p16) - 1) * 2)
const (
FileRenameInformation = 10
FileRenameInformationEx = 65
)
err = NtSetInformationFile(
h,
&IO_STATUS_BLOCK{},
unsafe.Pointer(&renameInfoEx),
uint32(unsafe.Sizeof(FILE_RENAME_INFORMATION_EX{})),
FileRenameInformationEx,
)
if err == nil {
return nil
}
// If the prior rename failed, the filesystem might not support
// POSIX semantics (for example, FAT), or might not have implemented
// FILE_RENAME_INFORMATION_EX.
//
// Try again.
renameInfo := FILE_RENAME_INFORMATION{
ReplaceIfExists: true,
RootDirectory: newdirfd,
}
copy(renameInfo.FileName[:], p16)
renameInfo.FileNameLength = renameInfoEx.FileNameLength
err = NtSetInformationFile(
h,
&IO_STATUS_BLOCK{},
unsafe.Pointer(&renameInfo),
uint32(unsafe.Sizeof(FILE_RENAME_INFORMATION{})),
FileRenameInformation,
)
if st, ok := err.(NTStatus); ok {
return st.Errno()
}
return err
}
func Linkat(olddirfd syscall.Handle, oldpath string, newdirfd syscall.Handle, newpath string) error {
objAttrs := &OBJECT_ATTRIBUTES{}
if err := objAttrs.init(olddirfd, oldpath); err != nil {
return err
}
var h syscall.Handle
err := NtOpenFile(
&h,
SYNCHRONIZE|FILE_WRITE_ATTRIBUTES,
objAttrs,
&IO_STATUS_BLOCK{},
FILE_SHARE_DELETE|FILE_SHARE_READ|FILE_SHARE_WRITE,
FILE_OPEN_REPARSE_POINT|FILE_OPEN_FOR_BACKUP_INTENT|FILE_SYNCHRONOUS_IO_NONALERT,
)
if err != nil {
return ntCreateFileError(err, 0)
}
defer syscall.CloseHandle(h)
linkInfo := FILE_LINK_INFORMATION{
RootDirectory: newdirfd,
}
p16, err := syscall.UTF16FromString(newpath)
if err != nil {
return err
}
if len(p16) > len(linkInfo.FileName) {
return syscall.EINVAL
}
copy(linkInfo.FileName[:], p16)
linkInfo.FileNameLength = uint32((len(p16) - 1) * 2)
const (
FileLinkInformation = 11
)
err = NtSetInformationFile(
h,
&IO_STATUS_BLOCK{},
unsafe.Pointer(&linkInfo),
uint32(unsafe.Sizeof(FILE_LINK_INFORMATION{})),
FileLinkInformation,
)
if st, ok := err.(NTStatus); ok {
return st.Errno()
}
return err
}
// SymlinkatFlags configure Symlinkat.
//
// Symbolic links have two properties: They may be directory or file links,
// and they may be absolute or relative.
//
// The Windows API defines flags describing these properties
// (SYMBOLIC_LINK_FLAG_DIRECTORY and SYMLINK_FLAG_RELATIVE),
// but the flags are passed to different system calls and
// do not have distinct values, so we define our own enumeration
// that permits expressing both.
type SymlinkatFlags uint
const (
SYMLINKAT_DIRECTORY = SymlinkatFlags(1 << iota)
SYMLINKAT_RELATIVE
)
func Symlinkat(oldname string, newdirfd syscall.Handle, newname string, flags SymlinkatFlags) error {
// Temporarily acquire symlink-creating privileges if possible.
// This is the behavior of CreateSymbolicLinkW.
//
// (When passed the SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE flag,
// CreateSymbolicLinkW ignores errors in acquiring privileges, as we do here.)
return withPrivilege("SeCreateSymbolicLinkPrivilege", func() error {
return symlinkat(oldname, newdirfd, newname, flags)
})
}
func symlinkat(oldname string, newdirfd syscall.Handle, newname string, flags SymlinkatFlags) error {
oldnameu16, err := syscall.UTF16FromString(oldname)
if err != nil {
return err
}
oldnameu16 = oldnameu16[:len(oldnameu16)-1] // trim off terminal NUL
var options uint32
if flags&SYMLINKAT_DIRECTORY != 0 {
options |= FILE_DIRECTORY_FILE
} else {
options |= FILE_NON_DIRECTORY_FILE
}
objAttrs := &OBJECT_ATTRIBUTES{}
if err := objAttrs.init(newdirfd, newname); err != nil {
return err
}
var h syscall.Handle
err = NtCreateFile(
&h,
SYNCHRONIZE|FILE_WRITE_ATTRIBUTES|DELETE,
objAttrs,
&IO_STATUS_BLOCK{},
nil,
syscall.FILE_ATTRIBUTE_NORMAL,
0,
FILE_CREATE,
FILE_OPEN_REPARSE_POINT|FILE_OPEN_FOR_BACKUP_INTENT|FILE_SYNCHRONOUS_IO_NONALERT|options,
nil,
0,
)
if err != nil {
return ntCreateFileError(err, 0)
}
defer syscall.CloseHandle(h)
// https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/ns-ntifs-_reparse_data_buffer
type reparseDataBufferT struct {
_ structs.HostLayout
ReparseTag uint32
ReparseDataLength uint16
Reserved uint16
SubstituteNameOffset uint16
SubstituteNameLength uint16
PrintNameOffset uint16
PrintNameLength uint16
Flags uint32
}
const (
headerSize = uint16(unsafe.Offsetof(reparseDataBufferT{}.SubstituteNameOffset))
bufferSize = uint16(unsafe.Sizeof(reparseDataBufferT{}))
)
// Data buffer containing a SymbolicLinkReparseBuffer followed by the link target.
rdbbuf := make([]byte, bufferSize+uint16(2*len(oldnameu16)))
rdb := (*reparseDataBufferT)(unsafe.Pointer(&rdbbuf[0]))
rdb.ReparseTag = syscall.IO_REPARSE_TAG_SYMLINK
rdb.ReparseDataLength = uint16(len(rdbbuf)) - uint16(headerSize)
rdb.SubstituteNameOffset = 0
rdb.SubstituteNameLength = uint16(2 * len(oldnameu16))
rdb.PrintNameOffset = 0
rdb.PrintNameLength = rdb.SubstituteNameLength
if flags&SYMLINKAT_RELATIVE != 0 {
rdb.Flags = SYMLINK_FLAG_RELATIVE
}
namebuf := rdbbuf[bufferSize:]
copy(namebuf, unsafe.String((*byte)(unsafe.Pointer(&oldnameu16[0])), 2*len(oldnameu16)))
err = syscall.DeviceIoControl(
h,
FSCTL_SET_REPARSE_POINT,
&rdbbuf[0],
uint32(len(rdbbuf)),
nil,
0,
nil,
nil)
if err != nil {
// Creating the symlink has failed, so try to remove the file.
const FileDispositionInformation = 13
NtSetInformationFile(
h,
&IO_STATUS_BLOCK{},
unsafe.Pointer(&FILE_DISPOSITION_INFORMATION{
DeleteFile: true,
}),
uint32(unsafe.Sizeof(FILE_DISPOSITION_INFORMATION{})),
FileDispositionInformation,
)
return err
}
return nil
}
// withPrivilege temporariliy acquires the named privilege and runs f.
// If the privilege cannot be acquired it runs f anyway,
// which should fail with an appropriate error.
func withPrivilege(privilege string, f func() error) error {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
err := ImpersonateSelf(SecurityImpersonation)
if err != nil {
return f()
}
defer RevertToSelf()
curThread, err := GetCurrentThread()
if err != nil {
return f()
}
var token syscall.Token
err = OpenThreadToken(curThread, syscall.TOKEN_QUERY|TOKEN_ADJUST_PRIVILEGES, false, &token)
if err != nil {
return f()
}
defer syscall.CloseHandle(syscall.Handle(token))
privStr, err := syscall.UTF16PtrFromString(privilege)
if err != nil {
return f()
}
var tokenPriv TOKEN_PRIVILEGES
err = LookupPrivilegeValue(nil, privStr, &tokenPriv.Privileges[0].Luid)
if err != nil {
return f()
}
tokenPriv.PrivilegeCount = 1
tokenPriv.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED
err = AdjustTokenPrivileges(token, false, &tokenPriv, 0, nil, nil)
if err != nil {
return f()
}
return f()
}