123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432 |
- package safefile
- import (
- "errors"
- "io"
- "os"
- "path/filepath"
- "strings"
- "syscall"
- "unicode/utf16"
- "unsafe"
- "github.com/Microsoft/hcsshim/internal/longpath"
- winio "github.com/Microsoft/go-winio"
- )
- //go:generate go run $GOROOT\src\syscall\mksyscall_windows.go -output zsyscall_windows.go safeopen.go
- //sys ntCreateFile(handle *uintptr, accessMask uint32, oa *objectAttributes, iosb *ioStatusBlock, allocationSize *uint64, fileAttributes uint32, shareAccess uint32, createDisposition uint32, createOptions uint32, eaBuffer *byte, eaLength uint32) (status uint32) = ntdll.NtCreateFile
- //sys ntSetInformationFile(handle uintptr, iosb *ioStatusBlock, information uintptr, length uint32, class uint32) (status uint32) = ntdll.NtSetInformationFile
- //sys rtlNtStatusToDosError(status uint32) (winerr error) = ntdll.RtlNtStatusToDosErrorNoTeb
- //sys localAlloc(flags uint32, size int) (ptr uintptr) = kernel32.LocalAlloc
- //sys localFree(ptr uintptr) = kernel32.LocalFree
- type ioStatusBlock struct {
- Status, Information uintptr
- }
- type objectAttributes struct {
- Length uintptr
- RootDirectory uintptr
- ObjectName uintptr
- Attributes uintptr
- SecurityDescriptor uintptr
- SecurityQoS uintptr
- }
- type unicodeString struct {
- Length uint16
- MaximumLength uint16
- Buffer uintptr
- }
- type fileLinkInformation struct {
- ReplaceIfExists bool
- RootDirectory uintptr
- FileNameLength uint32
- FileName [1]uint16
- }
- type fileDispositionInformationEx struct {
- Flags uintptr
- }
- const (
- _FileLinkInformation = 11
- _FileDispositionInformationEx = 64
- FILE_READ_ATTRIBUTES = 0x0080
- FILE_WRITE_ATTRIBUTES = 0x0100
- DELETE = 0x10000
- FILE_OPEN = 1
- FILE_CREATE = 2
- FILE_DIRECTORY_FILE = 0x00000001
- FILE_SYNCHRONOUS_IO_NONALERT = 0x00000020
- FILE_DELETE_ON_CLOSE = 0x00001000
- FILE_OPEN_FOR_BACKUP_INTENT = 0x00004000
- FILE_OPEN_REPARSE_POINT = 0x00200000
- FILE_DISPOSITION_DELETE = 0x00000001
- _OBJ_DONT_REPARSE = 0x1000
- _STATUS_REPARSE_POINT_ENCOUNTERED = 0xC000050B
- )
- func OpenRoot(path string) (*os.File, error) {
- longpath, err := longpath.LongAbs(path)
- if err != nil {
- return nil, err
- }
- return winio.OpenForBackup(longpath, syscall.GENERIC_READ, syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE, syscall.OPEN_EXISTING)
- }
- func ntRelativePath(path string) ([]uint16, error) {
- path = filepath.Clean(path)
- if strings.Contains(path, ":") {
- // Since alternate data streams must follow the file they
- // are attached to, finding one here (out of order) is invalid.
- return nil, errors.New("path contains invalid character `:`")
- }
- fspath := filepath.FromSlash(path)
- if len(fspath) > 0 && fspath[0] == '\\' {
- return nil, errors.New("expected relative path")
- }
- path16 := utf16.Encode(([]rune)(fspath))
- if len(path16) > 32767 {
- return nil, syscall.ENAMETOOLONG
- }
- return path16, nil
- }
- // openRelativeInternal opens a relative path from the given root, failing if
- // any of the intermediate path components are reparse points.
- func openRelativeInternal(path string, root *os.File, accessMask uint32, shareFlags uint32, createDisposition uint32, flags uint32) (*os.File, error) {
- var (
- h uintptr
- iosb ioStatusBlock
- oa objectAttributes
- )
- path16, err := ntRelativePath(path)
- if err != nil {
- return nil, err
- }
- if root == nil || root.Fd() == 0 {
- return nil, errors.New("missing root directory")
- }
- upathBuffer := localAlloc(0, int(unsafe.Sizeof(unicodeString{}))+len(path16)*2)
- defer localFree(upathBuffer)
- upath := (*unicodeString)(unsafe.Pointer(upathBuffer))
- upath.Length = uint16(len(path16) * 2)
- upath.MaximumLength = upath.Length
- upath.Buffer = upathBuffer + unsafe.Sizeof(*upath)
- copy((*[32768]uint16)(unsafe.Pointer(upath.Buffer))[:], path16)
- oa.Length = unsafe.Sizeof(oa)
- oa.ObjectName = upathBuffer
- oa.RootDirectory = uintptr(root.Fd())
- oa.Attributes = _OBJ_DONT_REPARSE
- status := ntCreateFile(
- &h,
- accessMask|syscall.SYNCHRONIZE,
- &oa,
- &iosb,
- nil,
- 0,
- shareFlags,
- createDisposition,
- FILE_OPEN_FOR_BACKUP_INTENT|FILE_SYNCHRONOUS_IO_NONALERT|flags,
- nil,
- 0,
- )
- if status != 0 {
- return nil, rtlNtStatusToDosError(status)
- }
- fullPath, err := longpath.LongAbs(filepath.Join(root.Name(), path))
- if err != nil {
- syscall.Close(syscall.Handle(h))
- return nil, err
- }
- return os.NewFile(h, fullPath), nil
- }
- // OpenRelative opens a relative path from the given root, failing if
- // any of the intermediate path components are reparse points.
- func OpenRelative(path string, root *os.File, accessMask uint32, shareFlags uint32, createDisposition uint32, flags uint32) (*os.File, error) {
- f, err := openRelativeInternal(path, root, accessMask, shareFlags, createDisposition, flags)
- if err != nil {
- err = &os.PathError{Op: "open", Path: filepath.Join(root.Name(), path), Err: err}
- }
- return f, err
- }
- // LinkRelative creates a hard link from oldname to newname (relative to oldroot
- // and newroot), failing if any of the intermediate path components are reparse
- // points.
- func LinkRelative(oldname string, oldroot *os.File, newname string, newroot *os.File) error {
- // Open the old file.
- oldf, err := openRelativeInternal(
- oldname,
- oldroot,
- syscall.FILE_WRITE_ATTRIBUTES,
- syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE,
- FILE_OPEN,
- 0,
- )
- if err != nil {
- return &os.LinkError{Op: "link", Old: filepath.Join(oldroot.Name(), oldname), New: filepath.Join(newroot.Name(), newname), Err: err}
- }
- defer oldf.Close()
- // Open the parent of the new file.
- var parent *os.File
- parentPath := filepath.Dir(newname)
- if parentPath != "." {
- parent, err = openRelativeInternal(
- parentPath,
- newroot,
- syscall.GENERIC_READ,
- syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE,
- FILE_OPEN,
- FILE_DIRECTORY_FILE)
- if err != nil {
- return &os.LinkError{Op: "link", Old: oldf.Name(), New: filepath.Join(newroot.Name(), newname), Err: err}
- }
- defer parent.Close()
- fi, err := winio.GetFileBasicInfo(parent)
- if err != nil {
- return err
- }
- if (fi.FileAttributes & syscall.FILE_ATTRIBUTE_REPARSE_POINT) != 0 {
- return &os.LinkError{Op: "link", Old: oldf.Name(), New: filepath.Join(newroot.Name(), newname), Err: rtlNtStatusToDosError(_STATUS_REPARSE_POINT_ENCOUNTERED)}
- }
- } else {
- parent = newroot
- }
- // Issue an NT call to create the link. This will be safe because NT will
- // not open any more directories to create the link, so it cannot walk any
- // more reparse points.
- newbase := filepath.Base(newname)
- newbase16, err := ntRelativePath(newbase)
- if err != nil {
- return err
- }
- size := int(unsafe.Offsetof(fileLinkInformation{}.FileName)) + len(newbase16)*2
- linkinfoBuffer := localAlloc(0, size)
- defer localFree(linkinfoBuffer)
- linkinfo := (*fileLinkInformation)(unsafe.Pointer(linkinfoBuffer))
- linkinfo.RootDirectory = parent.Fd()
- linkinfo.FileNameLength = uint32(len(newbase16) * 2)
- copy((*[32768]uint16)(unsafe.Pointer(&linkinfo.FileName[0]))[:], newbase16)
- var iosb ioStatusBlock
- status := ntSetInformationFile(
- oldf.Fd(),
- &iosb,
- linkinfoBuffer,
- uint32(size),
- _FileLinkInformation,
- )
- if status != 0 {
- return &os.LinkError{Op: "link", Old: oldf.Name(), New: filepath.Join(parent.Name(), newbase), Err: rtlNtStatusToDosError(status)}
- }
- return nil
- }
- // deleteOnClose marks a file to be deleted when the handle is closed.
- func deleteOnClose(f *os.File) error {
- disposition := fileDispositionInformationEx{Flags: FILE_DISPOSITION_DELETE}
- var iosb ioStatusBlock
- status := ntSetInformationFile(
- f.Fd(),
- &iosb,
- uintptr(unsafe.Pointer(&disposition)),
- uint32(unsafe.Sizeof(disposition)),
- _FileDispositionInformationEx,
- )
- if status != 0 {
- return rtlNtStatusToDosError(status)
- }
- return nil
- }
- // clearReadOnly clears the readonly attribute on a file.
- func clearReadOnly(f *os.File) error {
- bi, err := winio.GetFileBasicInfo(f)
- if err != nil {
- return err
- }
- if bi.FileAttributes&syscall.FILE_ATTRIBUTE_READONLY == 0 {
- return nil
- }
- sbi := winio.FileBasicInfo{
- FileAttributes: bi.FileAttributes &^ syscall.FILE_ATTRIBUTE_READONLY,
- }
- if sbi.FileAttributes == 0 {
- sbi.FileAttributes = syscall.FILE_ATTRIBUTE_NORMAL
- }
- return winio.SetFileBasicInfo(f, &sbi)
- }
- // RemoveRelative removes a file or directory relative to a root, failing if any
- // intermediate path components are reparse points.
- func RemoveRelative(path string, root *os.File) error {
- f, err := openRelativeInternal(
- path,
- root,
- FILE_READ_ATTRIBUTES|FILE_WRITE_ATTRIBUTES|DELETE,
- syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE,
- FILE_OPEN,
- FILE_OPEN_REPARSE_POINT)
- if err == nil {
- defer f.Close()
- err = deleteOnClose(f)
- if err == syscall.ERROR_ACCESS_DENIED {
- // Maybe the file is marked readonly. Clear the bit and retry.
- clearReadOnly(f)
- err = deleteOnClose(f)
- }
- }
- if err != nil {
- return &os.PathError{Op: "remove", Path: filepath.Join(root.Name(), path), Err: err}
- }
- return nil
- }
- // RemoveAllRelative removes a directory tree relative to a root, failing if any
- // intermediate path components are reparse points.
- func RemoveAllRelative(path string, root *os.File) error {
- fi, err := LstatRelative(path, root)
- if err != nil {
- if os.IsNotExist(err) {
- return nil
- }
- return err
- }
- fileAttributes := fi.Sys().(*syscall.Win32FileAttributeData).FileAttributes
- if fileAttributes&syscall.FILE_ATTRIBUTE_DIRECTORY == 0 || fileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT != 0 {
- // If this is a reparse point, it can't have children. Simple remove will do.
- err := RemoveRelative(path, root)
- if err == nil || os.IsNotExist(err) {
- return nil
- }
- return err
- }
- // It is necessary to use os.Open as Readdirnames does not work with
- // OpenRelative. This is safe because the above lstatrelative fails
- // if the target is outside the root, and we know this is not a
- // symlink from the above FILE_ATTRIBUTE_REPARSE_POINT check.
- fd, err := os.Open(filepath.Join(root.Name(), path))
- if err != nil {
- if os.IsNotExist(err) {
- // Race. It was deleted between the Lstat and Open.
- // Return nil per RemoveAll's docs.
- return nil
- }
- return err
- }
- // Remove contents & return first error.
- for {
- names, err1 := fd.Readdirnames(100)
- for _, name := range names {
- err1 := RemoveAllRelative(path+string(os.PathSeparator)+name, root)
- if err == nil {
- err = err1
- }
- }
- if err1 == io.EOF {
- break
- }
- // If Readdirnames returned an error, use it.
- if err == nil {
- err = err1
- }
- if len(names) == 0 {
- break
- }
- }
- fd.Close()
- // Remove directory.
- err1 := RemoveRelative(path, root)
- if err1 == nil || os.IsNotExist(err1) {
- return nil
- }
- if err == nil {
- err = err1
- }
- return err
- }
- // MkdirRelative creates a directory relative to a root, failing if any
- // intermediate path components are reparse points.
- func MkdirRelative(path string, root *os.File) error {
- f, err := openRelativeInternal(
- path,
- root,
- 0,
- syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE,
- FILE_CREATE,
- FILE_DIRECTORY_FILE)
- if err == nil {
- f.Close()
- } else {
- err = &os.PathError{Op: "mkdir", Path: filepath.Join(root.Name(), path), Err: err}
- }
- return err
- }
- // LstatRelative performs a stat operation on a file relative to a root, failing
- // if any intermediate path components are reparse points.
- func LstatRelative(path string, root *os.File) (os.FileInfo, error) {
- f, err := openRelativeInternal(
- path,
- root,
- FILE_READ_ATTRIBUTES,
- syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE,
- FILE_OPEN,
- FILE_OPEN_REPARSE_POINT)
- if err != nil {
- return nil, &os.PathError{Op: "stat", Path: filepath.Join(root.Name(), path), Err: err}
- }
- defer f.Close()
- return f.Stat()
- }
- // EnsureNotReparsePointRelative validates that a given file (relative to a
- // root) and all intermediate path components are not a reparse points.
- func EnsureNotReparsePointRelative(path string, root *os.File) error {
- // Perform an open with OBJ_DONT_REPARSE but without specifying FILE_OPEN_REPARSE_POINT.
- f, err := OpenRelative(
- path,
- root,
- 0,
- syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE,
- FILE_OPEN,
- 0)
- if err != nil {
- return err
- }
- f.Close()
- return nil
- }
|