1package safefile
2
3import (
4	"errors"
5	"io"
6	"os"
7	"path/filepath"
8	"strings"
9	"syscall"
10	"unicode/utf16"
11	"unsafe"
12
13	"github.com/Microsoft/hcsshim/internal/longpath"
14
15	winio "github.com/Microsoft/go-winio"
16)
17
18//go:generate go run $GOROOT\src\syscall\mksyscall_windows.go -output zsyscall_windows.go safeopen.go
19
20//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
21//sys ntSetInformationFile(handle uintptr, iosb *ioStatusBlock, information uintptr, length uint32, class uint32) (status uint32) = ntdll.NtSetInformationFile
22//sys rtlNtStatusToDosError(status uint32) (winerr error) = ntdll.RtlNtStatusToDosErrorNoTeb
23//sys localAlloc(flags uint32, size int) (ptr uintptr) = kernel32.LocalAlloc
24//sys localFree(ptr uintptr) = kernel32.LocalFree
25
26type ioStatusBlock struct {
27	Status, Information uintptr
28}
29
30type objectAttributes struct {
31	Length             uintptr
32	RootDirectory      uintptr
33	ObjectName         uintptr
34	Attributes         uintptr
35	SecurityDescriptor uintptr
36	SecurityQoS        uintptr
37}
38
39type unicodeString struct {
40	Length        uint16
41	MaximumLength uint16
42	Buffer        uintptr
43}
44
45type fileLinkInformation struct {
46	ReplaceIfExists bool
47	RootDirectory   uintptr
48	FileNameLength  uint32
49	FileName        [1]uint16
50}
51
52type fileDispositionInformationEx struct {
53	Flags uintptr
54}
55
56const (
57	_FileLinkInformation          = 11
58	_FileDispositionInformationEx = 64
59
60	FILE_READ_ATTRIBUTES  = 0x0080
61	FILE_WRITE_ATTRIBUTES = 0x0100
62	DELETE                = 0x10000
63
64	FILE_OPEN   = 1
65	FILE_CREATE = 2
66
67	FILE_DIRECTORY_FILE          = 0x00000001
68	FILE_SYNCHRONOUS_IO_NONALERT = 0x00000020
69	FILE_DELETE_ON_CLOSE         = 0x00001000
70	FILE_OPEN_FOR_BACKUP_INTENT  = 0x00004000
71	FILE_OPEN_REPARSE_POINT      = 0x00200000
72
73	FILE_DISPOSITION_DELETE = 0x00000001
74
75	_OBJ_DONT_REPARSE = 0x1000
76
77	_STATUS_REPARSE_POINT_ENCOUNTERED = 0xC000050B
78)
79
80func OpenRoot(path string) (*os.File, error) {
81	longpath, err := longpath.LongAbs(path)
82	if err != nil {
83		return nil, err
84	}
85	return winio.OpenForBackup(longpath, syscall.GENERIC_READ, syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE, syscall.OPEN_EXISTING)
86}
87
88func ntRelativePath(path string) ([]uint16, error) {
89	path = filepath.Clean(path)
90	if strings.Contains(path, ":") {
91		// Since alternate data streams must follow the file they
92		// are attached to, finding one here (out of order) is invalid.
93		return nil, errors.New("path contains invalid character `:`")
94	}
95	fspath := filepath.FromSlash(path)
96	if len(fspath) > 0 && fspath[0] == '\\' {
97		return nil, errors.New("expected relative path")
98	}
99
100	path16 := utf16.Encode(([]rune)(fspath))
101	if len(path16) > 32767 {
102		return nil, syscall.ENAMETOOLONG
103	}
104
105	return path16, nil
106}
107
108// openRelativeInternal opens a relative path from the given root, failing if
109// any of the intermediate path components are reparse points.
110func openRelativeInternal(path string, root *os.File, accessMask uint32, shareFlags uint32, createDisposition uint32, flags uint32) (*os.File, error) {
111	var (
112		h    uintptr
113		iosb ioStatusBlock
114		oa   objectAttributes
115	)
116
117	path16, err := ntRelativePath(path)
118	if err != nil {
119		return nil, err
120	}
121
122	if root == nil || root.Fd() == 0 {
123		return nil, errors.New("missing root directory")
124	}
125
126	upathBuffer := localAlloc(0, int(unsafe.Sizeof(unicodeString{}))+len(path16)*2)
127	defer localFree(upathBuffer)
128
129	upath := (*unicodeString)(unsafe.Pointer(upathBuffer))
130	upath.Length = uint16(len(path16) * 2)
131	upath.MaximumLength = upath.Length
132	upath.Buffer = upathBuffer + unsafe.Sizeof(*upath)
133	copy((*[32768]uint16)(unsafe.Pointer(upath.Buffer))[:], path16)
134
135	oa.Length = unsafe.Sizeof(oa)
136	oa.ObjectName = upathBuffer
137	oa.RootDirectory = uintptr(root.Fd())
138	oa.Attributes = _OBJ_DONT_REPARSE
139	status := ntCreateFile(
140		&h,
141		accessMask|syscall.SYNCHRONIZE,
142		&oa,
143		&iosb,
144		nil,
145		0,
146		shareFlags,
147		createDisposition,
148		FILE_OPEN_FOR_BACKUP_INTENT|FILE_SYNCHRONOUS_IO_NONALERT|flags,
149		nil,
150		0,
151	)
152	if status != 0 {
153		return nil, rtlNtStatusToDosError(status)
154	}
155
156	fullPath, err := longpath.LongAbs(filepath.Join(root.Name(), path))
157	if err != nil {
158		syscall.Close(syscall.Handle(h))
159		return nil, err
160	}
161
162	return os.NewFile(h, fullPath), nil
163}
164
165// OpenRelative opens a relative path from the given root, failing if
166// any of the intermediate path components are reparse points.
167func OpenRelative(path string, root *os.File, accessMask uint32, shareFlags uint32, createDisposition uint32, flags uint32) (*os.File, error) {
168	f, err := openRelativeInternal(path, root, accessMask, shareFlags, createDisposition, flags)
169	if err != nil {
170		err = &os.PathError{Op: "open", Path: filepath.Join(root.Name(), path), Err: err}
171	}
172	return f, err
173}
174
175// LinkRelative creates a hard link from oldname to newname (relative to oldroot
176// and newroot), failing if any of the intermediate path components are reparse
177// points.
178func LinkRelative(oldname string, oldroot *os.File, newname string, newroot *os.File) error {
179	// Open the old file.
180	oldf, err := openRelativeInternal(
181		oldname,
182		oldroot,
183		syscall.FILE_WRITE_ATTRIBUTES,
184		syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE,
185		FILE_OPEN,
186		0,
187	)
188	if err != nil {
189		return &os.LinkError{Op: "link", Old: filepath.Join(oldroot.Name(), oldname), New: filepath.Join(newroot.Name(), newname), Err: err}
190	}
191	defer oldf.Close()
192
193	// Open the parent of the new file.
194	var parent *os.File
195	parentPath := filepath.Dir(newname)
196	if parentPath != "." {
197		parent, err = openRelativeInternal(
198			parentPath,
199			newroot,
200			syscall.GENERIC_READ,
201			syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE,
202			FILE_OPEN,
203			FILE_DIRECTORY_FILE)
204		if err != nil {
205			return &os.LinkError{Op: "link", Old: oldf.Name(), New: filepath.Join(newroot.Name(), newname), Err: err}
206		}
207		defer parent.Close()
208
209		fi, err := winio.GetFileBasicInfo(parent)
210		if err != nil {
211			return err
212		}
213		if (fi.FileAttributes & syscall.FILE_ATTRIBUTE_REPARSE_POINT) != 0 {
214			return &os.LinkError{Op: "link", Old: oldf.Name(), New: filepath.Join(newroot.Name(), newname), Err: rtlNtStatusToDosError(_STATUS_REPARSE_POINT_ENCOUNTERED)}
215		}
216
217	} else {
218		parent = newroot
219	}
220
221	// Issue an NT call to create the link. This will be safe because NT will
222	// not open any more directories to create the link, so it cannot walk any
223	// more reparse points.
224	newbase := filepath.Base(newname)
225	newbase16, err := ntRelativePath(newbase)
226	if err != nil {
227		return err
228	}
229
230	size := int(unsafe.Offsetof(fileLinkInformation{}.FileName)) + len(newbase16)*2
231	linkinfoBuffer := localAlloc(0, size)
232	defer localFree(linkinfoBuffer)
233	linkinfo := (*fileLinkInformation)(unsafe.Pointer(linkinfoBuffer))
234	linkinfo.RootDirectory = parent.Fd()
235	linkinfo.FileNameLength = uint32(len(newbase16) * 2)
236	copy((*[32768]uint16)(unsafe.Pointer(&linkinfo.FileName[0]))[:], newbase16)
237
238	var iosb ioStatusBlock
239	status := ntSetInformationFile(
240		oldf.Fd(),
241		&iosb,
242		linkinfoBuffer,
243		uint32(size),
244		_FileLinkInformation,
245	)
246	if status != 0 {
247		return &os.LinkError{Op: "link", Old: oldf.Name(), New: filepath.Join(parent.Name(), newbase), Err: rtlNtStatusToDosError(status)}
248	}
249
250	return nil
251}
252
253// deleteOnClose marks a file to be deleted when the handle is closed.
254func deleteOnClose(f *os.File) error {
255	disposition := fileDispositionInformationEx{Flags: FILE_DISPOSITION_DELETE}
256	var iosb ioStatusBlock
257	status := ntSetInformationFile(
258		f.Fd(),
259		&iosb,
260		uintptr(unsafe.Pointer(&disposition)),
261		uint32(unsafe.Sizeof(disposition)),
262		_FileDispositionInformationEx,
263	)
264	if status != 0 {
265		return rtlNtStatusToDosError(status)
266	}
267	return nil
268}
269
270// clearReadOnly clears the readonly attribute on a file.
271func clearReadOnly(f *os.File) error {
272	bi, err := winio.GetFileBasicInfo(f)
273	if err != nil {
274		return err
275	}
276	if bi.FileAttributes&syscall.FILE_ATTRIBUTE_READONLY == 0 {
277		return nil
278	}
279	sbi := winio.FileBasicInfo{
280		FileAttributes: bi.FileAttributes &^ syscall.FILE_ATTRIBUTE_READONLY,
281	}
282	if sbi.FileAttributes == 0 {
283		sbi.FileAttributes = syscall.FILE_ATTRIBUTE_NORMAL
284	}
285	return winio.SetFileBasicInfo(f, &sbi)
286}
287
288// RemoveRelative removes a file or directory relative to a root, failing if any
289// intermediate path components are reparse points.
290func RemoveRelative(path string, root *os.File) error {
291	f, err := openRelativeInternal(
292		path,
293		root,
294		FILE_READ_ATTRIBUTES|FILE_WRITE_ATTRIBUTES|DELETE,
295		syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE,
296		FILE_OPEN,
297		FILE_OPEN_REPARSE_POINT)
298	if err == nil {
299		defer f.Close()
300		err = deleteOnClose(f)
301		if err == syscall.ERROR_ACCESS_DENIED {
302			// Maybe the file is marked readonly. Clear the bit and retry.
303			clearReadOnly(f)
304			err = deleteOnClose(f)
305		}
306	}
307	if err != nil {
308		return &os.PathError{Op: "remove", Path: filepath.Join(root.Name(), path), Err: err}
309	}
310	return nil
311}
312
313// RemoveAllRelative removes a directory tree relative to a root, failing if any
314// intermediate path components are reparse points.
315func RemoveAllRelative(path string, root *os.File) error {
316	fi, err := LstatRelative(path, root)
317	if err != nil {
318		if os.IsNotExist(err) {
319			return nil
320		}
321		return err
322	}
323	fileAttributes := fi.Sys().(*syscall.Win32FileAttributeData).FileAttributes
324	if fileAttributes&syscall.FILE_ATTRIBUTE_DIRECTORY == 0 || fileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT != 0 {
325		// If this is a reparse point, it can't have children. Simple remove will do.
326		err := RemoveRelative(path, root)
327		if err == nil || os.IsNotExist(err) {
328			return nil
329		}
330		return err
331	}
332
333	// It is necessary to use os.Open as Readdirnames does not work with
334	// OpenRelative. This is safe because the above lstatrelative fails
335	// if the target is outside the root, and we know this is not a
336	// symlink from the above FILE_ATTRIBUTE_REPARSE_POINT check.
337	fd, err := os.Open(filepath.Join(root.Name(), path))
338	if err != nil {
339		if os.IsNotExist(err) {
340			// Race. It was deleted between the Lstat and Open.
341			// Return nil per RemoveAll's docs.
342			return nil
343		}
344		return err
345	}
346
347	// Remove contents & return first error.
348	for {
349		names, err1 := fd.Readdirnames(100)
350		for _, name := range names {
351			err1 := RemoveAllRelative(path+string(os.PathSeparator)+name, root)
352			if err == nil {
353				err = err1
354			}
355		}
356		if err1 == io.EOF {
357			break
358		}
359		// If Readdirnames returned an error, use it.
360		if err == nil {
361			err = err1
362		}
363		if len(names) == 0 {
364			break
365		}
366	}
367	fd.Close()
368
369	// Remove directory.
370	err1 := RemoveRelative(path, root)
371	if err1 == nil || os.IsNotExist(err1) {
372		return nil
373	}
374	if err == nil {
375		err = err1
376	}
377	return err
378}
379
380// MkdirRelative creates a directory relative to a root, failing if any
381// intermediate path components are reparse points.
382func MkdirRelative(path string, root *os.File) error {
383	f, err := openRelativeInternal(
384		path,
385		root,
386		0,
387		syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE,
388		FILE_CREATE,
389		FILE_DIRECTORY_FILE)
390	if err == nil {
391		f.Close()
392	} else {
393		err = &os.PathError{Op: "mkdir", Path: filepath.Join(root.Name(), path), Err: err}
394	}
395	return err
396}
397
398// LstatRelative performs a stat operation on a file relative to a root, failing
399// if any intermediate path components are reparse points.
400func LstatRelative(path string, root *os.File) (os.FileInfo, error) {
401	f, err := openRelativeInternal(
402		path,
403		root,
404		FILE_READ_ATTRIBUTES,
405		syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE,
406		FILE_OPEN,
407		FILE_OPEN_REPARSE_POINT)
408	if err != nil {
409		return nil, &os.PathError{Op: "stat", Path: filepath.Join(root.Name(), path), Err: err}
410	}
411	defer f.Close()
412	return f.Stat()
413}
414
415// EnsureNotReparsePointRelative validates that a given file (relative to a
416// root) and all intermediate path components are not a reparse points.
417func EnsureNotReparsePointRelative(path string, root *os.File) error {
418	// Perform an open with OBJ_DONT_REPARSE but without specifying FILE_OPEN_REPARSE_POINT.
419	f, err := OpenRelative(
420		path,
421		root,
422		0,
423		syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE,
424		FILE_OPEN,
425		0)
426	if err != nil {
427		return err
428	}
429	f.Close()
430	return nil
431}
432