| 1 | //go:build windows |
| 2 | |
| 3 | package winaclresidue |
| 4 | |
| 5 | import ( |
| 6 | "fmt" |
| 7 | "path/filepath" |
| 8 | "strings" |
| 9 | "unsafe" |
| 10 | |
| 11 | "golang.org/x/sys/windows" |
| 12 | ) |
| 13 | |
| 14 | // openExact opens path with exactly the requested access rights. CreateFile |
| 15 | // silently adds SYNCHRONIZE and FILE_READ_ATTRIBUTES, both of which a legacy |
| 16 | // DENY RX entry removes, so a WRITE_DAC- or DELETE-only open has to go |
| 17 | // through NtCreateFile. Reparse points are opened as themselves, never |
| 18 | // followed. |
| 19 | func openExact(path string, access uint32) (windows.Handle, error) { |
| 20 | name, err := windows.NewNTUnicodeString(ntPath(path)) |
| 21 | if err != nil { |
| 22 | return 0, err |
| 23 | } |
| 24 | oa := windows.OBJECT_ATTRIBUTES{ObjectName: name, Attributes: windows.OBJ_CASE_INSENSITIVE} |
| 25 | oa.Length = uint32(unsafe.Sizeof(oa)) |
| 26 | var ( |
| 27 | handle windows.Handle |
| 28 | iosb windows.IO_STATUS_BLOCK |
| 29 | ) |
| 30 | err = windows.NtCreateFile(&handle, access, &oa, &iosb, nil, 0, |
| 31 | windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE, |
| 32 | windows.FILE_OPEN, windows.FILE_NON_DIRECTORY_FILE|windows.FILE_OPEN_REPARSE_POINT, 0, 0) |
| 33 | if err != nil { |
| 34 | return 0, fmt.Errorf("open %q with access 0x%x: %w", path, access, err) |
| 35 | } |
| 36 | return handle, nil |
| 37 | } |
| 38 | |
| 39 | // ntPath converts an absolute Win32 path to the NT object namespace. |
| 40 | func ntPath(path string) string { |
| 41 | clean := filepath.Clean(path) |
| 42 | if rest, ok := strings.CutPrefix(clean, `\\`); ok { |
| 43 | return `\??\UNC\` + rest |
| 44 | } |
| 45 | return `\??\` + clean |
| 46 | } |
| 47 | |
| 48 | // fileRenameInformation mirrors FILE_RENAME_INFORMATION; the name is stored |
| 49 | // inline after the header, so callers allocate the struct plus the name. |
| 50 | type fileRenameInformation struct { |
| 51 | ReplaceIfExists uint8 |
| 52 | RootDirectory windows.Handle |
| 53 | FileNameLength uint32 |
| 54 | FileName [1]uint16 |
| 55 | } |
| 56 | |
| 57 | // RenameLockedFile moves path to target through a DELETE-only handle so a |
| 58 | // deny that removes read rights cannot block the move; MoveFileEx would also |
| 59 | // ask for SYNCHRONIZE and fail. The target must not exist. |
| 60 | func RenameLockedFile(path, target string) error { |
| 61 | handle, err := openExact(path, windows.DELETE) |
| 62 | if err != nil { |
| 63 | return err |
| 64 | } |
| 65 | defer windows.CloseHandle(handle) |
| 66 | name, err := windows.UTF16FromString(ntPath(target)) |
| 67 | if err != nil { |
| 68 | return err |
| 69 | } |
| 70 | name = name[:len(name)-1] |
| 71 | header := unsafe.Offsetof(fileRenameInformation{}.FileName) |
| 72 | size := int(header) + len(name)*2 |
| 73 | buffer := make([]uint64, (size+7)/8) |
| 74 | info := (*fileRenameInformation)(unsafe.Pointer(&buffer[0])) |
| 75 | info.FileNameLength = uint32(len(name) * 2) |
| 76 | copy(unsafe.Slice((*uint16)(unsafe.Add(unsafe.Pointer(&buffer[0]), header)), len(name)), name) |
| 77 | var iosb windows.IO_STATUS_BLOCK |
| 78 | if err := windows.NtSetInformationFile(handle, &iosb, (*byte)(unsafe.Pointer(&buffer[0])), uint32(size), windows.FileRenameInformation); err != nil { |
| 79 | return fmt.Errorf("rename %q to %q: %w", path, target, err) |
| 80 | } |
| 81 | return nil |
| 82 | } |
| 83 |