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