mirror of
https://github.com/golang/go.git
synced 2025-05-05 15:43:04 +00:00
os: add Root
Add os.Root, a type which represents a directory and permits performing file operations within that directory. For #67002 Change-Id: I863f4f1bc320a89b1125ae4237761f3e9320a901 Reviewed-on: https://go-review.googlesource.com/c/go/+/612136 Reviewed-by: Ian Lance Taylor <iant@google.com> Reviewed-by: Quim Muntal <quimmuntal@gmail.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
This commit is contained in:
parent
558f5372fc
commit
43d90c6a14
9
api/next/67002.txt
Normal file
9
api/next/67002.txt
Normal file
@ -0,0 +1,9 @@
|
||||
pkg os, func OpenRoot(string) (*Root, error) #67002
|
||||
pkg os, method (*Root) Close() error #67002
|
||||
pkg os, method (*Root) Create(string) (*File, error) #67002
|
||||
pkg os, method (*Root) Mkdir(string, fs.FileMode) error #67002
|
||||
pkg os, method (*Root) Name() string #67002
|
||||
pkg os, method (*Root) Open(string) (*File, error) #67002
|
||||
pkg os, method (*Root) OpenFile(string, int, fs.FileMode) (*File, error) #67002
|
||||
pkg os, method (*Root) OpenRoot(string) (*Root, error) #67002
|
||||
pkg os, type Root struct #67002
|
16
doc/next/6-stdlib/1-os-root.md
Normal file
16
doc/next/6-stdlib/1-os-root.md
Normal file
@ -0,0 +1,16 @@
|
||||
### Directory-limited filesystem access
|
||||
|
||||
<!-- go.dev/issue/67002 -->
|
||||
The new [os.Root] type provides the ability to perform filesystem
|
||||
operations within a specific directory.
|
||||
|
||||
The [os.OpenRoot] function opens a directory and returns an [os.Root].
|
||||
Methods on [os.Root] operate within the directory and do not permit
|
||||
paths that refer to locations outside the directory, including
|
||||
ones that follow symbolic links out of the directory.
|
||||
|
||||
- [os.Root.Open] opens a file for reading.
|
||||
- [os.Root.Create] creates a file.
|
||||
- [os.Root.OpenFile] is the generalized open call.
|
||||
- [os.Root.Mkdir] creates a directory.
|
||||
|
1
doc/next/6-stdlib/99-minor/os/67002.md
Normal file
1
doc/next/6-stdlib/99-minor/os/67002.md
Normal file
@ -0,0 +1 @@
|
||||
<!-- os.Root -->
|
@ -42,7 +42,7 @@ func Openat(dirfd syscall.Handle, name string, flag int, perm uint32) (_ syscall
|
||||
}
|
||||
if flag&syscall.O_APPEND != 0 {
|
||||
access |= FILE_APPEND_DATA
|
||||
// Remove GENERIC_WRITE access unless O_TRUNC is set,
|
||||
// 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
|
||||
@ -99,7 +99,7 @@ func Openat(dirfd syscall.Handle, name string, flag int, perm uint32) (_ syscall
|
||||
fileAttrs,
|
||||
FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE,
|
||||
disposition,
|
||||
FILE_SYNCHRONOUS_IO_NONALERT|options,
|
||||
FILE_SYNCHRONOUS_IO_NONALERT|FILE_OPEN_FOR_BACKUP_INTENT|options,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
|
@ -398,6 +398,8 @@ func OpenFile(name string, flag int, perm FileMode) (*File, error) {
|
||||
return f, nil
|
||||
}
|
||||
|
||||
var errPathEscapes = errors.New("path escapes from parent")
|
||||
|
||||
// openDir opens a file which is assumed to be a directory. As such, it skips
|
||||
// the syscalls that make the file descriptor non-blocking as these take time
|
||||
// and will fail on file descriptors for directories.
|
||||
|
@ -415,10 +415,13 @@ func readReparseLink(path string) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
defer syscall.CloseHandle(h)
|
||||
return readReparseLinkHandle(h)
|
||||
}
|
||||
|
||||
func readReparseLinkHandle(h syscall.Handle) (string, error) {
|
||||
rdbbuf := make([]byte, syscall.MAXIMUM_REPARSE_DATA_BUFFER_SIZE)
|
||||
var bytesReturned uint32
|
||||
err = syscall.DeviceIoControl(h, syscall.FSCTL_GET_REPARSE_POINT, nil, 0, &rdbbuf[0], uint32(len(rdbbuf)), &bytesReturned, nil)
|
||||
err := syscall.DeviceIoControl(h, syscall.FSCTL_GET_REPARSE_POINT, nil, 0, &rdbbuf[0], uint32(len(rdbbuf)), &bytesReturned, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -1779,15 +1779,24 @@ func TestSeekError(t *testing.T) {
|
||||
|
||||
func TestOpenError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir := t.TempDir()
|
||||
if err := WriteFile(filepath.Join(dir, "is-a-file"), nil, 0o666); err != nil {
|
||||
dir := makefs(t, []string{
|
||||
"is-a-file",
|
||||
"is-a-dir/",
|
||||
})
|
||||
t.Run("NoRoot", func(t *testing.T) { testOpenError(t, dir, false) })
|
||||
t.Run("InRoot", func(t *testing.T) { testOpenError(t, dir, true) })
|
||||
}
|
||||
func testOpenError(t *testing.T, dir string, rooted bool) {
|
||||
t.Parallel()
|
||||
var r *Root
|
||||
if rooted {
|
||||
var err error
|
||||
r, err = OpenRoot(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := Mkdir(filepath.Join(dir, "is-a-dir"), 0o777); err != nil {
|
||||
t.Fatal(err)
|
||||
defer r.Close()
|
||||
}
|
||||
|
||||
for _, tt := range []struct {
|
||||
path string
|
||||
mode int
|
||||
@ -1805,16 +1814,25 @@ func TestOpenError(t *testing.T) {
|
||||
O_WRONLY,
|
||||
syscall.ENOTDIR,
|
||||
}} {
|
||||
var f *File
|
||||
var err error
|
||||
var name string
|
||||
if rooted {
|
||||
name = fmt.Sprintf("Root(%q).OpenFile(%q, %d)", dir, tt.path, tt.mode)
|
||||
f, err = r.OpenFile(tt.path, tt.mode, 0)
|
||||
} else {
|
||||
path := filepath.Join(dir, tt.path)
|
||||
f, err := OpenFile(path, tt.mode, 0)
|
||||
name = fmt.Sprintf("OpenFile(%q, %d)", path, tt.mode)
|
||||
f, err = OpenFile(path, tt.mode, 0)
|
||||
}
|
||||
if err == nil {
|
||||
t.Errorf("Open(%q, %d) succeeded", tt.path, tt.mode)
|
||||
t.Errorf("%v succeeded", name)
|
||||
f.Close()
|
||||
continue
|
||||
}
|
||||
perr, ok := err.(*PathError)
|
||||
if !ok {
|
||||
t.Errorf("Open(%q, %d) returns error of %T type; want *PathError", tt.path, tt.mode, err)
|
||||
t.Errorf("%v returns error of %T type; want *PathError", name, err)
|
||||
}
|
||||
if perr.Err != tt.error {
|
||||
if runtime.GOOS == "plan9" {
|
||||
@ -1827,7 +1845,7 @@ func TestOpenError(t *testing.T) {
|
||||
if tt.error == syscall.EISDIR && strings.HasSuffix(syscallErrStr, syscall.EACCES.Error()) {
|
||||
continue
|
||||
}
|
||||
t.Errorf("Open(%q, %d) = _, %q; want suffix %q", tt.path, tt.mode, syscallErrStr, expectedErrStr)
|
||||
t.Errorf("%v = _, %q; want suffix %q", name, syscallErrStr, expectedErrStr)
|
||||
}
|
||||
continue
|
||||
}
|
||||
@ -1838,7 +1856,7 @@ func TestOpenError(t *testing.T) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
t.Errorf("Open(%q, %d) = _, %q; want %q", tt.path, tt.mode, perr.Err.Error(), tt.error.Error())
|
||||
t.Errorf("%v = _, %q; want %q", name, perr.Err.Error(), tt.error.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2070,8 +2088,15 @@ func TestWriteAtInAppendMode(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func writeFile(t *testing.T, fname string, flag int, text string) string {
|
||||
f, err := OpenFile(fname, flag, 0666)
|
||||
func writeFile(t *testing.T, r *Root, fname string, flag int, text string) string {
|
||||
t.Helper()
|
||||
var f *File
|
||||
var err error
|
||||
if r == nil {
|
||||
f, err = OpenFile(fname, flag, 0666)
|
||||
} else {
|
||||
f, err = r.OpenFile(fname, flag, 0666)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("Open: %v", err)
|
||||
}
|
||||
@ -2088,17 +2113,17 @@ func writeFile(t *testing.T, fname string, flag int, text string) string {
|
||||
}
|
||||
|
||||
func TestAppend(t *testing.T) {
|
||||
t.Chdir(t.TempDir())
|
||||
testMaybeRooted(t, func(t *testing.T, r *Root) {
|
||||
const f = "append.txt"
|
||||
s := writeFile(t, f, O_CREATE|O_TRUNC|O_RDWR, "new")
|
||||
s := writeFile(t, r, f, O_CREATE|O_TRUNC|O_RDWR, "new")
|
||||
if s != "new" {
|
||||
t.Fatalf("writeFile: have %q want %q", s, "new")
|
||||
}
|
||||
s = writeFile(t, f, O_APPEND|O_RDWR, "|append")
|
||||
s = writeFile(t, r, f, O_APPEND|O_RDWR, "|append")
|
||||
if s != "new|append" {
|
||||
t.Fatalf("writeFile: have %q want %q", s, "new|append")
|
||||
}
|
||||
s = writeFile(t, f, O_CREATE|O_APPEND|O_RDWR, "|append")
|
||||
s = writeFile(t, r, f, O_CREATE|O_APPEND|O_RDWR, "|append")
|
||||
if s != "new|append|append" {
|
||||
t.Fatalf("writeFile: have %q want %q", s, "new|append|append")
|
||||
}
|
||||
@ -2106,18 +2131,163 @@ func TestAppend(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Remove: %v", err)
|
||||
}
|
||||
s = writeFile(t, f, O_CREATE|O_APPEND|O_RDWR, "new&append")
|
||||
s = writeFile(t, r, f, O_CREATE|O_APPEND|O_RDWR, "new&append")
|
||||
if s != "new&append" {
|
||||
t.Fatalf("writeFile: after append have %q want %q", s, "new&append")
|
||||
}
|
||||
s = writeFile(t, f, O_CREATE|O_RDWR, "old")
|
||||
s = writeFile(t, r, f, O_CREATE|O_RDWR, "old")
|
||||
if s != "old&append" {
|
||||
t.Fatalf("writeFile: after create have %q want %q", s, "old&append")
|
||||
}
|
||||
s = writeFile(t, f, O_CREATE|O_TRUNC|O_RDWR, "new")
|
||||
s = writeFile(t, r, f, O_CREATE|O_TRUNC|O_RDWR, "new")
|
||||
if s != "new" {
|
||||
t.Fatalf("writeFile: after truncate have %q want %q", s, "new")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestFilePermissions tests setting Unix permission bits on file creation.
|
||||
func TestFilePermissions(t *testing.T) {
|
||||
if Getuid() == 0 {
|
||||
t.Skip("skipping test when running as root")
|
||||
}
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
mode FileMode
|
||||
}{
|
||||
{"r", 0o444},
|
||||
{"w", 0o222},
|
||||
{"rw", 0o666},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
if test.mode&0444 == 0 {
|
||||
t.Skip("write-only files not supported on " + runtime.GOOS)
|
||||
}
|
||||
case "wasip1":
|
||||
t.Skip("file permissions not supported on " + runtime.GOOS)
|
||||
}
|
||||
testMaybeRooted(t, func(t *testing.T, r *Root) {
|
||||
const filename = "f"
|
||||
var f *File
|
||||
var err error
|
||||
if r == nil {
|
||||
f, err = OpenFile(filename, O_RDWR|O_CREATE|O_EXCL, test.mode)
|
||||
} else {
|
||||
f, err = r.OpenFile(filename, O_RDWR|O_CREATE|O_EXCL, test.mode)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.Close()
|
||||
b, err := ReadFile(filename)
|
||||
if test.mode&0o444 != 0 {
|
||||
if err != nil {
|
||||
t.Errorf("ReadFile = %v; want success", err)
|
||||
}
|
||||
} else {
|
||||
if err == nil {
|
||||
t.Errorf("ReadFile = %q, <nil>; want failure", string(b))
|
||||
}
|
||||
}
|
||||
_, err = Stat(filename)
|
||||
if err != nil {
|
||||
t.Errorf("Stat = %v; want success", err)
|
||||
}
|
||||
err = WriteFile(filename, nil, 0666)
|
||||
if test.mode&0o222 != 0 {
|
||||
if err != nil {
|
||||
t.Errorf("WriteFile = %v; want success", err)
|
||||
b, err := ReadFile(filename)
|
||||
t.Errorf("ReadFile: %v", err)
|
||||
t.Errorf("file contents: %q", b)
|
||||
}
|
||||
} else {
|
||||
if err == nil {
|
||||
t.Errorf("WriteFile(%q) = <nil>; want failure", filename)
|
||||
st, err := Stat(filename)
|
||||
if err == nil {
|
||||
t.Errorf("mode: %s", st.Mode())
|
||||
}
|
||||
b, err := ReadFile(filename)
|
||||
t.Errorf("ReadFile: %v", err)
|
||||
t.Errorf("file contents: %q", b)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// TestFileRDWRFlags tests the O_RDONLY, O_WRONLY, and O_RDWR flags.
|
||||
func TestFileRDWRFlags(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
flag int
|
||||
}{
|
||||
{"O_RDONLY", O_RDONLY},
|
||||
{"O_WRONLY", O_WRONLY},
|
||||
{"O_RDWR", O_RDWR},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
testMaybeRooted(t, func(t *testing.T, r *Root) {
|
||||
const filename = "f"
|
||||
content := []byte("content")
|
||||
if err := WriteFile(filename, content, 0666); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var f *File
|
||||
var err error
|
||||
if r == nil {
|
||||
f, err = OpenFile(filename, test.flag, 0)
|
||||
} else {
|
||||
f, err = r.OpenFile(filename, test.flag, 0)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
got, err := io.ReadAll(f)
|
||||
if test.flag == O_WRONLY {
|
||||
if err == nil {
|
||||
t.Errorf("read file: %q, %v; want error", got, err)
|
||||
}
|
||||
} else {
|
||||
if err != nil || !bytes.Equal(got, content) {
|
||||
t.Errorf("read file: %q, %v; want %q, <nil>", got, err, content)
|
||||
}
|
||||
}
|
||||
if _, err := f.Seek(0, 0); err != nil {
|
||||
t.Fatalf("f.Seek: %v", err)
|
||||
}
|
||||
newcontent := []byte("CONTENT")
|
||||
_, err = f.Write(newcontent)
|
||||
if test.flag == O_RDONLY {
|
||||
if err == nil {
|
||||
t.Errorf("write file: succeeded, want error")
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("write file: %v, want success", err)
|
||||
}
|
||||
}
|
||||
f.Close()
|
||||
got, err = ReadFile(filename)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := content
|
||||
if test.flag != O_RDONLY {
|
||||
want = newcontent
|
||||
}
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Fatalf("after write, file contains %q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatDirWithTrailingSlash(t *testing.T) {
|
||||
@ -2937,6 +3107,22 @@ func isDeadlineExceeded(err error) bool {
|
||||
|
||||
// Test that opening a file does not change its permissions. Issue 38225.
|
||||
func TestOpenFileKeepsPermissions(t *testing.T) {
|
||||
t.Run("OpenFile", func(t *testing.T) {
|
||||
testOpenFileKeepsPermissions(t, OpenFile)
|
||||
})
|
||||
t.Run("RootOpenFile", func(t *testing.T) {
|
||||
testOpenFileKeepsPermissions(t, func(name string, flag int, perm FileMode) (*File, error) {
|
||||
dir, file := filepath.Split(name)
|
||||
r, err := OpenRoot(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer r.Close()
|
||||
return r.OpenFile(file, flag, perm)
|
||||
})
|
||||
})
|
||||
}
|
||||
func testOpenFileKeepsPermissions(t *testing.T, openf func(name string, flag int, perm FileMode) (*File, error)) {
|
||||
t.Parallel()
|
||||
|
||||
dir := t.TempDir()
|
||||
@ -2948,7 +3134,7 @@ func TestOpenFileKeepsPermissions(t *testing.T) {
|
||||
if err := f.Close(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
f, err = OpenFile(name, O_WRONLY|O_CREATE|O_TRUNC, 0)
|
||||
f, err = openf(name, O_WRONLY|O_CREATE|O_TRUNC, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -3597,11 +3783,18 @@ func TestCopyFSWithSymlinks(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAppendDoesntOverwrite(t *testing.T) {
|
||||
name := filepath.Join(t.TempDir(), "file")
|
||||
testMaybeRooted(t, func(t *testing.T, r *Root) {
|
||||
name := "file"
|
||||
if err := WriteFile(name, []byte("hello"), 0666); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f, err := OpenFile(name, O_APPEND|O_WRONLY, 0)
|
||||
var f *File
|
||||
var err error
|
||||
if r == nil {
|
||||
f, err = OpenFile(name, O_APPEND|O_WRONLY, 0)
|
||||
} else {
|
||||
f, err = r.OpenFile(name, O_APPEND|O_WRONLY, 0)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -3620,4 +3813,5 @@ func TestAppendDoesntOverwrite(t *testing.T) {
|
||||
if string(got) != want {
|
||||
t.Fatalf("got %q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
185
src/os/root.go
Normal file
185
src/os/root.go
Normal file
@ -0,0 +1,185 @@
|
||||
// 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 os
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"internal/testlog"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// Root may be used to only access files within a single directory tree.
|
||||
//
|
||||
// Methods on Root can only access files and directories beneath a root directory.
|
||||
// If any component of a file name passed to a method of Root references a location
|
||||
// outside the root, the method returns an error.
|
||||
// File names may reference the directory itself (.).
|
||||
//
|
||||
// Methods on Root will follow symbolic links, but symbolic links may not
|
||||
// reference a location outside the root.
|
||||
// Symbolic links must not be absolute.
|
||||
//
|
||||
// Methods on Root do not prohibit traversal of filesystem boundaries,
|
||||
// Linux bind mounts, /proc special files, or access to Unix device files.
|
||||
//
|
||||
// Methods on Root are safe to be used from multiple goroutines simultaneously.
|
||||
//
|
||||
// On most platforms, creating a Root opens a file descriptor or handle referencing
|
||||
// the directory. If the directory is moved, methods on Root reference the original
|
||||
// directory in its new location.
|
||||
//
|
||||
// Root's behavior differs on some platforms:
|
||||
//
|
||||
// - When GOOS=windows, file names may not reference Windows reserved device names
|
||||
// such as NUL and COM1.
|
||||
// - When GOOS=js, Root is vulnerable to TOCTOU (time-of-check-time-of-use)
|
||||
// attacks in symlink validation, and cannot ensure that operations will not
|
||||
// escape the root.
|
||||
// - When GOOS=plan9 or GOOS=js, Root does not track directories across renames.
|
||||
// On these platforms, a Root references a directory name, not a file descriptor.
|
||||
type Root struct {
|
||||
root root
|
||||
}
|
||||
|
||||
const (
|
||||
// Maximum number of symbolic links we will follow when resolving a file in a root.
|
||||
// 8 is __POSIX_SYMLOOP_MAX (the minimum allowed value for SYMLOOP_MAX),
|
||||
// and a common limit.
|
||||
rootMaxSymlinks = 8
|
||||
)
|
||||
|
||||
// OpenRoot opens the named directory.
|
||||
// If there is an error, it will be of type *PathError.
|
||||
func OpenRoot(name string) (*Root, error) {
|
||||
testlog.Open(name)
|
||||
return openRootNolog(name)
|
||||
}
|
||||
|
||||
// Name returns the name of the directory presented to OpenRoot.
|
||||
//
|
||||
// It is safe to call Name after [Close].
|
||||
func (r *Root) Name() string {
|
||||
return r.root.Name()
|
||||
}
|
||||
|
||||
// Close closes the Root.
|
||||
// After Close is called, methods on Root return errors.
|
||||
func (r *Root) Close() error {
|
||||
return r.root.Close()
|
||||
}
|
||||
|
||||
// Open opens the named file in the root for reading.
|
||||
// See [Open] for more details.
|
||||
func (r *Root) Open(name string) (*File, error) {
|
||||
return r.OpenFile(name, O_RDONLY, 0)
|
||||
}
|
||||
|
||||
// Create creates or truncates the named file in the root.
|
||||
// See [Create] for more details.
|
||||
func (r *Root) Create(name string) (*File, error) {
|
||||
return r.OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
|
||||
}
|
||||
|
||||
// OpenFile opens the named file in the root.
|
||||
// See [OpenFile] for more details.
|
||||
//
|
||||
// If perm contains bits other than the nine least-significant bits (0o777),
|
||||
// OpenFile returns an error.
|
||||
func (r *Root) OpenFile(name string, flag int, perm FileMode) (*File, error) {
|
||||
if perm&0o777 != perm {
|
||||
return nil, &PathError{Op: "openat", Path: name, Err: errors.New("unsupported file mode")}
|
||||
}
|
||||
r.logOpen(name)
|
||||
rf, err := rootOpenFileNolog(r, name, flag, perm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rf.appendMode = flag&O_APPEND != 0
|
||||
return rf, nil
|
||||
}
|
||||
|
||||
// OpenRoot opens the named directory in the root.
|
||||
// If there is an error, it will be of type *PathError.
|
||||
func (r *Root) OpenRoot(name string) (*Root, error) {
|
||||
r.logOpen(name)
|
||||
return openRootInRoot(r, name)
|
||||
}
|
||||
|
||||
// Mkdir creates a new directory in the root
|
||||
// with the specified name and permission bits (before umask).
|
||||
// See [Mkdir] for more details.
|
||||
//
|
||||
// If perm contains bits other than the nine least-significant bits (0o777),
|
||||
// OpenFile returns an error.
|
||||
func (r *Root) Mkdir(name string, perm FileMode) error {
|
||||
if perm&0o777 != perm {
|
||||
return &PathError{Op: "mkdirat", Path: name, Err: errors.New("unsupported file mode")}
|
||||
}
|
||||
return rootMkdir(r, name, perm)
|
||||
}
|
||||
|
||||
func (r *Root) logOpen(name string) {
|
||||
if log := testlog.Logger(); log != nil {
|
||||
// This won't be right if r's name has changed since it was opened,
|
||||
// but it's the best we can do.
|
||||
log.Open(joinPath(r.Name(), name))
|
||||
}
|
||||
}
|
||||
|
||||
// splitPathInRoot splits a path into components
|
||||
// and joins it with the given prefix and suffix.
|
||||
//
|
||||
// The path is relative to a Root, and must not be
|
||||
// absolute, volume-relative, or "".
|
||||
//
|
||||
// "." components are removed, except in the last component.
|
||||
//
|
||||
// Path separators following the last component are preserved.
|
||||
func splitPathInRoot(s string, prefix, suffix []string) (_ []string, err error) {
|
||||
if len(s) == 0 {
|
||||
return nil, errors.New("empty path")
|
||||
}
|
||||
if IsPathSeparator(s[0]) {
|
||||
return nil, errPathEscapes
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
// Windows cleans paths before opening them.
|
||||
s, err = rootCleanPath(s, prefix, suffix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
prefix = nil
|
||||
suffix = nil
|
||||
}
|
||||
|
||||
parts := append([]string{}, prefix...)
|
||||
i, j := 0, 1
|
||||
for {
|
||||
if j < len(s) && !IsPathSeparator(s[j]) {
|
||||
// Keep looking for the end of this component.
|
||||
j++
|
||||
continue
|
||||
}
|
||||
parts = append(parts, s[i:j])
|
||||
// Advance to the next component, or end of the path.
|
||||
for j < len(s) && IsPathSeparator(s[j]) {
|
||||
j++
|
||||
}
|
||||
if j == len(s) {
|
||||
// If this is the last path component,
|
||||
// preserve any trailing path separators.
|
||||
parts[len(parts)-1] = s[i:]
|
||||
break
|
||||
}
|
||||
if parts[len(parts)-1] == "." {
|
||||
// Remove "." components, except at the end.
|
||||
parts = parts[:len(parts)-1]
|
||||
}
|
||||
i = j
|
||||
}
|
||||
parts = append(parts, suffix...)
|
||||
return parts, nil
|
||||
}
|
79
src/os/root_js.go
Normal file
79
src/os/root_js.go
Normal file
@ -0,0 +1,79 @@
|
||||
// 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.
|
||||
|
||||
//go:build js && wasm
|
||||
|
||||
package os
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"slices"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func checkPathEscapes(r *Root, name string) error {
|
||||
if r.root.closed.Load() {
|
||||
return ErrClosed
|
||||
}
|
||||
parts, err := splitPathInRoot(name, nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
i := 0
|
||||
symlinks := 0
|
||||
base := r.root.name
|
||||
for i < len(parts) {
|
||||
if parts[i] == ".." {
|
||||
// Resolve one or more parent ("..") path components.
|
||||
end := i + 1
|
||||
for end < len(parts) && parts[end] == ".." {
|
||||
end++
|
||||
}
|
||||
count := end - i
|
||||
if count > i {
|
||||
return errPathEscapes
|
||||
}
|
||||
parts = slices.Delete(parts, i-count, end)
|
||||
i -= count
|
||||
base = r.root.name
|
||||
for j := range i {
|
||||
base = joinPath(base, parts[j])
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
next := joinPath(base, parts[i])
|
||||
fi, err := Lstat(next)
|
||||
if err != nil {
|
||||
if IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return underlyingError(err)
|
||||
}
|
||||
if fi.Mode()&ModeSymlink != 0 {
|
||||
link, err := Readlink(next)
|
||||
if err != nil {
|
||||
return errPathEscapes
|
||||
}
|
||||
symlinks++
|
||||
if symlinks > rootMaxSymlinks {
|
||||
return errors.New("too many symlinks")
|
||||
}
|
||||
newparts, err := splitPathInRoot(link, parts[:i], parts[i+1:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
parts = newparts
|
||||
continue
|
||||
}
|
||||
if !fi.IsDir() && i < len(parts)-1 {
|
||||
return syscall.ENOTDIR
|
||||
}
|
||||
|
||||
base = next
|
||||
i++
|
||||
}
|
||||
return nil
|
||||
}
|
11
src/os/root_nonwindows.go
Normal file
11
src/os/root_nonwindows.go
Normal file
@ -0,0 +1,11 @@
|
||||
// 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.
|
||||
|
||||
//go:build !windows
|
||||
|
||||
package os
|
||||
|
||||
func rootCleanPath(s string, prefix, suffix []string) (string, error) {
|
||||
return s, nil
|
||||
}
|
86
src/os/root_noopenat.go
Normal file
86
src/os/root_noopenat.go
Normal file
@ -0,0 +1,86 @@
|
||||
// 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.
|
||||
|
||||
//go:build (js && wasm) || plan9
|
||||
|
||||
package os
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// root implementation for platforms with no openat.
|
||||
// Currently plan9 and js.
|
||||
type root struct {
|
||||
name string
|
||||
closed atomic.Bool
|
||||
}
|
||||
|
||||
// openRootNolog is OpenRoot.
|
||||
func openRootNolog(name string) (*Root, error) {
|
||||
r, err := newRoot(name)
|
||||
if err != nil {
|
||||
return nil, &PathError{Op: "open", Path: name, Err: err}
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// openRootInRoot is Root.OpenRoot.
|
||||
func openRootInRoot(r *Root, name string) (*Root, error) {
|
||||
if err := checkPathEscapes(r, name); err != nil {
|
||||
return nil, &PathError{Op: "openat", Path: name, Err: err}
|
||||
}
|
||||
r, err := newRoot(joinPath(r.root.name, name))
|
||||
if err != nil {
|
||||
return nil, &PathError{Op: "openat", Path: name, Err: err}
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// newRoot returns a new Root.
|
||||
// If fd is not a directory, it closes it and returns an error.
|
||||
func newRoot(name string) (*Root, error) {
|
||||
fi, err := Stat(name)
|
||||
if err != nil {
|
||||
return nil, err.(*PathError).Err
|
||||
}
|
||||
if !fi.IsDir() {
|
||||
return nil, errors.New("not a directory")
|
||||
}
|
||||
return &Root{root{name: name}}, nil
|
||||
}
|
||||
|
||||
func (r *root) Close() error {
|
||||
// For consistency with platforms where Root.Close closes a handle,
|
||||
// mark the Root as closed and return errors from future calls.
|
||||
r.closed.Store(true)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *root) Name() string {
|
||||
return r.name
|
||||
}
|
||||
|
||||
// rootOpenFileNolog is Root.OpenFile.
|
||||
func rootOpenFileNolog(r *Root, name string, flag int, perm FileMode) (*File, error) {
|
||||
if err := checkPathEscapes(r, name); err != nil {
|
||||
return nil, &PathError{Op: "openat", Path: name, Err: err}
|
||||
}
|
||||
f, err := openFileNolog(joinPath(r.root.name, name), flag, perm)
|
||||
if err != nil {
|
||||
return nil, &PathError{Op: "openat", Path: name, Err: underlyingError(err)}
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func rootMkdir(r *Root, name string, perm FileMode) error {
|
||||
if err := checkPathEscapes(r, name); err != nil {
|
||||
return &PathError{Op: "mkdirat", Path: name, Err: err}
|
||||
}
|
||||
if err := Mkdir(joinPath(r.root.name, name), perm); err != nil {
|
||||
return &PathError{Op: "mkdirat", Path: name, Err: underlyingError(err)}
|
||||
}
|
||||
return nil
|
||||
}
|
199
src/os/root_openat.go
Normal file
199
src/os/root_openat.go
Normal file
@ -0,0 +1,199 @@
|
||||
// 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.
|
||||
|
||||
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || windows || wasip1
|
||||
|
||||
package os
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"slices"
|
||||
"sync"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// root implementation for platforms with a function to open a file
|
||||
// relative to a directory.
|
||||
type root struct {
|
||||
name string
|
||||
|
||||
// refs is incremented while an operation is using fd.
|
||||
// closed is set when Close is called.
|
||||
// fd is closed when closed is true and refs is 0.
|
||||
mu sync.Mutex
|
||||
fd sysfdType
|
||||
refs int // number of active operations
|
||||
closed bool // set when closed
|
||||
}
|
||||
|
||||
func (r *root) Close() error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if !r.closed && r.refs == 0 {
|
||||
syscall.Close(r.fd)
|
||||
}
|
||||
r.closed = true
|
||||
runtime.SetFinalizer(r, nil) // no need for a finalizer any more
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *root) incref() error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if r.closed {
|
||||
return ErrClosed
|
||||
}
|
||||
r.refs++
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *root) decref() {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if r.refs <= 0 {
|
||||
panic("bad Root refcount")
|
||||
}
|
||||
r.refs--
|
||||
if r.closed && r.refs == 0 {
|
||||
syscall.Close(r.fd)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *root) Name() string {
|
||||
return r.name
|
||||
}
|
||||
|
||||
func rootMkdir(r *Root, name string, perm FileMode) error {
|
||||
_, err := doInRoot(r, name, func(parent sysfdType, name string) (struct{}, error) {
|
||||
return struct{}{}, mkdirat(parent, name, perm)
|
||||
})
|
||||
if err != nil {
|
||||
return &PathError{Op: "mkdirat", Path: name, Err: err}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// doInRoot performs an operation on a path in a Root.
|
||||
//
|
||||
// It opens the directory containing the final element of the path,
|
||||
// and calls f with the directory FD and name of the final element.
|
||||
//
|
||||
// If the path refers to a symlink which should be followed,
|
||||
// then f must return errSymlink.
|
||||
// doInRoot will follow the symlink and call f again.
|
||||
func doInRoot[T any](r *Root, name string, f func(parent sysfdType, name string) (T, error)) (ret T, err error) {
|
||||
if err := r.root.incref(); err != nil {
|
||||
return ret, err
|
||||
}
|
||||
defer r.root.decref()
|
||||
|
||||
parts, err := splitPathInRoot(name, nil, nil)
|
||||
if err != nil {
|
||||
return ret, err
|
||||
}
|
||||
|
||||
rootfd := r.root.fd
|
||||
dirfd := rootfd
|
||||
defer func() {
|
||||
if dirfd != rootfd {
|
||||
syscall.Close(dirfd)
|
||||
}
|
||||
}()
|
||||
|
||||
// When resolving .. path components, we restart path resolution from the root.
|
||||
// (We can't openat(dir, "..") to move up to the parent directory,
|
||||
// because dir may have moved since we opened it.)
|
||||
// To limit how many opens a malicious path can cause us to perform, we set
|
||||
// a limit on the total number of path steps and the total number of restarts
|
||||
// caused by .. components. If *both* limits are exceeded, we halt the operation.
|
||||
const maxSteps = 255
|
||||
const maxRestarts = 8
|
||||
|
||||
i := 0
|
||||
steps := 0
|
||||
restarts := 0
|
||||
symlinks := 0
|
||||
for {
|
||||
steps++
|
||||
if steps > maxSteps && restarts > maxRestarts {
|
||||
return ret, syscall.ENAMETOOLONG
|
||||
}
|
||||
|
||||
if parts[i] == ".." {
|
||||
// Resolve one or more parent ("..") path components.
|
||||
//
|
||||
// Rewrite the original path,
|
||||
// removing the elements eliminated by ".." components,
|
||||
// and start over from the beginning.
|
||||
restarts++
|
||||
end := i + 1
|
||||
for end < len(parts) && parts[end] == ".." {
|
||||
end++
|
||||
}
|
||||
count := end - i
|
||||
if count > i {
|
||||
return ret, errPathEscapes
|
||||
}
|
||||
parts = slices.Delete(parts, i-count, end)
|
||||
i = 0
|
||||
if dirfd != rootfd {
|
||||
syscall.Close(dirfd)
|
||||
}
|
||||
dirfd = rootfd
|
||||
continue
|
||||
}
|
||||
|
||||
if i == len(parts)-1 {
|
||||
// This is the last path element.
|
||||
// Call f to decide what to do with it.
|
||||
// If f returns errSymlink, this element is a symlink
|
||||
// which should be followed.
|
||||
ret, err = f(dirfd, parts[i])
|
||||
if _, ok := err.(errSymlink); !ok {
|
||||
return ret, err
|
||||
}
|
||||
} else {
|
||||
var fd sysfdType
|
||||
fd, err = rootOpenDir(dirfd, parts[i])
|
||||
if err == nil {
|
||||
if dirfd != rootfd {
|
||||
syscall.Close(dirfd)
|
||||
}
|
||||
dirfd = fd
|
||||
} else if _, ok := err.(errSymlink); !ok {
|
||||
return ret, err
|
||||
}
|
||||
}
|
||||
|
||||
if e, ok := err.(errSymlink); ok {
|
||||
symlinks++
|
||||
if symlinks > rootMaxSymlinks {
|
||||
return ret, syscall.ELOOP
|
||||
}
|
||||
newparts, err := splitPathInRoot(string(e), parts[:i], parts[i+1:])
|
||||
if err != nil {
|
||||
return ret, err
|
||||
}
|
||||
if len(newparts) < i || !slices.Equal(parts[:i], newparts[:i]) {
|
||||
// Some component in the path which we have already traversed
|
||||
// has changed. We need to restart parsing from the root.
|
||||
i = 0
|
||||
if dirfd != rootfd {
|
||||
syscall.Close(dirfd)
|
||||
}
|
||||
dirfd = rootfd
|
||||
}
|
||||
parts = newparts
|
||||
continue
|
||||
}
|
||||
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
// errSymlink reports that a file being operated on is actually a symlink,
|
||||
// and the target of that symlink.
|
||||
type errSymlink string
|
||||
|
||||
func (errSymlink) Error() string { panic("errSymlink is not user-visible") }
|
21
src/os/root_plan9.go
Normal file
21
src/os/root_plan9.go
Normal file
@ -0,0 +1,21 @@
|
||||
// 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.
|
||||
|
||||
//go:build plan9
|
||||
|
||||
package os
|
||||
|
||||
import (
|
||||
"internal/filepathlite"
|
||||
)
|
||||
|
||||
func checkPathEscapes(r *Root, name string) error {
|
||||
if r.root.closed.Load() {
|
||||
return ErrClosed
|
||||
}
|
||||
if !filepathlite.IsLocal(name) {
|
||||
return errPathEscapes
|
||||
}
|
||||
return nil
|
||||
}
|
960
src/os/root_test.go
Normal file
960
src/os/root_test.go
Normal file
@ -0,0 +1,960 @@
|
||||
// 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 os_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// testMaybeRooted calls f in two subtests,
|
||||
// one with a Root and one with a nil r.
|
||||
func testMaybeRooted(t *testing.T, f func(t *testing.T, r *os.Root)) {
|
||||
t.Run("NoRoot", func(t *testing.T) {
|
||||
t.Chdir(t.TempDir())
|
||||
f(t, nil)
|
||||
})
|
||||
t.Run("InRoot", func(t *testing.T) {
|
||||
t.Chdir(t.TempDir())
|
||||
r, err := os.OpenRoot(".")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer r.Close()
|
||||
f(t, r)
|
||||
})
|
||||
}
|
||||
|
||||
// makefs creates a test filesystem layout and returns the path to its root.
|
||||
//
|
||||
// Each entry in the slice is a file, directory, or symbolic link to create:
|
||||
//
|
||||
// - "d/": directory d
|
||||
// - "f": file f with contents f
|
||||
// - "a => b": symlink a with target b
|
||||
//
|
||||
// The directory containing the filesystem is always named ROOT.
|
||||
// $ABS is replaced with the absolute path of the directory containing the filesystem.
|
||||
//
|
||||
// Parent directories are automatically created as needed.
|
||||
//
|
||||
// makefs calls t.Skip if the layout contains features not supported by the current GOOS.
|
||||
func makefs(t *testing.T, fs []string) string {
|
||||
root := path.Join(t.TempDir(), "ROOT")
|
||||
if err := os.Mkdir(root, 0o777); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, ent := range fs {
|
||||
ent = strings.ReplaceAll(ent, "$ABS", root)
|
||||
base, link, isLink := strings.Cut(ent, " => ")
|
||||
if isLink {
|
||||
if runtime.GOOS == "wasip1" && path.IsAbs(link) {
|
||||
t.Skip("absolute link targets not supported on " + runtime.GOOS)
|
||||
}
|
||||
if runtime.GOOS == "plan9" {
|
||||
t.Skip("symlinks not supported on " + runtime.GOOS)
|
||||
}
|
||||
ent = base
|
||||
}
|
||||
if err := os.MkdirAll(path.Join(root, path.Dir(base)), 0o777); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if isLink {
|
||||
if err := os.Symlink(link, path.Join(root, base)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
} else if strings.HasSuffix(ent, "/") {
|
||||
if err := os.MkdirAll(path.Join(root, ent), 0o777); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
} else {
|
||||
if err := os.WriteFile(path.Join(root, ent), []byte(ent), 0o666); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return root
|
||||
}
|
||||
|
||||
// A rootTest is a test case for os.Root.
|
||||
type rootTest struct {
|
||||
name string
|
||||
|
||||
// fs is the test filesystem layout. See makefs above.
|
||||
fs []string
|
||||
|
||||
// open is the filename to access in the test.
|
||||
open string
|
||||
|
||||
// target is the filename that we expect to be accessed, after resolving all symlinks.
|
||||
// For test cases where the operation fails due to an escaping path such as ../ROOT/x,
|
||||
// the target is the filename that should not have been opened.
|
||||
target string
|
||||
|
||||
// wantError is true if accessing the file should fail.
|
||||
wantError bool
|
||||
|
||||
// alwaysFails is true if the open operation is expected to fail
|
||||
// even when using non-openat operations.
|
||||
//
|
||||
// This lets us check that tests that are expected to fail because (for example)
|
||||
// a path escapes the directory root will succeed when the escaping checks are not
|
||||
// performed.
|
||||
alwaysFails bool
|
||||
}
|
||||
|
||||
// run sets up the test filesystem layout, os.OpenDirs the root, and calls f.
|
||||
func (test *rootTest) run(t *testing.T, f func(t *testing.T, target string, d *os.Root)) {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
root := makefs(t, test.fs)
|
||||
d, err := os.OpenRoot(root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer d.Close()
|
||||
// The target is a file that will be accessed,
|
||||
// or a file that should not be accessed
|
||||
// (because doing so escapes the root).
|
||||
target := test.target
|
||||
if test.target != "" {
|
||||
target = filepath.Join(root, test.target)
|
||||
}
|
||||
f(t, target, d)
|
||||
})
|
||||
}
|
||||
|
||||
// errEndsTest checks the error result of a test,
|
||||
// verifying that it succeeded or failed as expected.
|
||||
//
|
||||
// It returns true if the test is done due to encountering an expected error.
|
||||
// false if the test should continue.
|
||||
func errEndsTest(t *testing.T, err error, wantError bool, format string, args ...any) bool {
|
||||
t.Helper()
|
||||
if wantError {
|
||||
if err == nil {
|
||||
op := fmt.Sprintf(format, args...)
|
||||
t.Fatalf("%v = nil; want error", op)
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
if err != nil {
|
||||
op := fmt.Sprintf(format, args...)
|
||||
t.Fatalf("%v = %v; want success", op, err)
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var rootTestCases = []rootTest{{
|
||||
name: "plain path",
|
||||
fs: []string{},
|
||||
open: "target",
|
||||
target: "target",
|
||||
}, {
|
||||
name: "path in directory",
|
||||
fs: []string{
|
||||
"a/b/c/",
|
||||
},
|
||||
open: "a/b/c/target",
|
||||
target: "a/b/c/target",
|
||||
}, {
|
||||
name: "symlink",
|
||||
fs: []string{
|
||||
"link => target",
|
||||
},
|
||||
open: "link",
|
||||
target: "target",
|
||||
}, {
|
||||
name: "symlink chain",
|
||||
fs: []string{
|
||||
"link => a/b/c/target",
|
||||
"a/b => e",
|
||||
"a/e => ../f",
|
||||
"f => g/h/i",
|
||||
"g/h/i => ..",
|
||||
"g/c/",
|
||||
},
|
||||
open: "link",
|
||||
target: "g/c/target",
|
||||
}, {
|
||||
name: "path with dot",
|
||||
fs: []string{
|
||||
"a/b/",
|
||||
},
|
||||
open: "./a/./b/./target",
|
||||
target: "a/b/target",
|
||||
}, {
|
||||
name: "path with dotdot",
|
||||
fs: []string{
|
||||
"a/b/",
|
||||
},
|
||||
open: "a/../a/b/../../a/b/../b/target",
|
||||
target: "a/b/target",
|
||||
}, {
|
||||
name: "dotdot no symlink",
|
||||
fs: []string{
|
||||
"a/",
|
||||
},
|
||||
open: "a/../target",
|
||||
target: "target",
|
||||
}, {
|
||||
name: "dotdot after symlink",
|
||||
fs: []string{
|
||||
"a => b/c",
|
||||
"b/c/",
|
||||
},
|
||||
open: "a/../target",
|
||||
target: func() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
// On Windows, the path is cleaned before symlink resolution.
|
||||
return "target"
|
||||
}
|
||||
return "b/target"
|
||||
}(),
|
||||
}, {
|
||||
name: "dotdot before symlink",
|
||||
fs: []string{
|
||||
"a => b/c",
|
||||
"b/c/",
|
||||
},
|
||||
open: "b/../a/target",
|
||||
target: "b/c/target",
|
||||
}, {
|
||||
name: "directory does not exist",
|
||||
fs: []string{},
|
||||
open: "a/file",
|
||||
wantError: true,
|
||||
alwaysFails: true,
|
||||
}, {
|
||||
name: "empty path",
|
||||
fs: []string{},
|
||||
open: "",
|
||||
wantError: true,
|
||||
alwaysFails: true,
|
||||
}, {
|
||||
name: "symlink cycle",
|
||||
fs: []string{
|
||||
"a => a",
|
||||
},
|
||||
open: "a",
|
||||
wantError: true,
|
||||
alwaysFails: true,
|
||||
}, {
|
||||
name: "path escapes",
|
||||
fs: []string{},
|
||||
open: "../ROOT/target",
|
||||
target: "target",
|
||||
wantError: true,
|
||||
}, {
|
||||
name: "long path escapes",
|
||||
fs: []string{
|
||||
"a/",
|
||||
},
|
||||
open: "a/../../ROOT/target",
|
||||
target: "target",
|
||||
wantError: true,
|
||||
}, {
|
||||
name: "absolute symlink",
|
||||
fs: []string{
|
||||
"link => $ABS/target",
|
||||
},
|
||||
open: "link",
|
||||
target: "target",
|
||||
wantError: true,
|
||||
}, {
|
||||
name: "relative symlink",
|
||||
fs: []string{
|
||||
"link => ../ROOT/target",
|
||||
},
|
||||
open: "link",
|
||||
target: "target",
|
||||
wantError: true,
|
||||
}, {
|
||||
name: "symlink chain escapes",
|
||||
fs: []string{
|
||||
"link => a/b/c/target",
|
||||
"a/b => e",
|
||||
"a/e => ../../ROOT",
|
||||
"c/",
|
||||
},
|
||||
open: "link",
|
||||
target: "c/target",
|
||||
wantError: true,
|
||||
}}
|
||||
|
||||
func TestRootOpen_File(t *testing.T) {
|
||||
want := []byte("target")
|
||||
for _, test := range rootTestCases {
|
||||
test.run(t, func(t *testing.T, target string, root *os.Root) {
|
||||
if target != "" {
|
||||
if err := os.WriteFile(target, want, 0o666); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
f, err := root.Open(test.open)
|
||||
if errEndsTest(t, err, test.wantError, "root.Open(%q)", test.open) {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
got, err := io.ReadAll(f)
|
||||
if err != nil || !bytes.Equal(got, want) {
|
||||
t.Errorf(`Dir.Open(%q): read content %q, %v; want %q`, test.open, string(got), err, string(want))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRootOpen_Directory(t *testing.T) {
|
||||
for _, test := range rootTestCases {
|
||||
test.run(t, func(t *testing.T, target string, root *os.Root) {
|
||||
if target != "" {
|
||||
if err := os.Mkdir(target, 0o777); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(target+"/found", nil, 0o666); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
f, err := root.Open(test.open)
|
||||
if errEndsTest(t, err, test.wantError, "root.Open(%q)", test.open) {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
got, err := f.Readdirnames(-1)
|
||||
if err != nil {
|
||||
t.Errorf(`Dir.Open(%q).Readdirnames: %v`, test.open, err)
|
||||
}
|
||||
if want := []string{"found"}; !slices.Equal(got, want) {
|
||||
t.Errorf(`Dir.Open(%q).Readdirnames: %q, want %q`, test.open, got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRootCreate(t *testing.T) {
|
||||
want := []byte("target")
|
||||
for _, test := range rootTestCases {
|
||||
test.run(t, func(t *testing.T, target string, root *os.Root) {
|
||||
f, err := root.Create(test.open)
|
||||
if errEndsTest(t, err, test.wantError, "root.Create(%q)", test.open) {
|
||||
return
|
||||
}
|
||||
if _, err := f.Write(want); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.Close()
|
||||
got, err := os.ReadFile(target)
|
||||
if err != nil {
|
||||
t.Fatalf(`reading file created with root.Create(%q): %v`, test.open, err)
|
||||
}
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Fatalf(`reading file created with root.Create(%q): got %q; want %q`, test.open, got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRootMkdir(t *testing.T) {
|
||||
for _, test := range rootTestCases {
|
||||
test.run(t, func(t *testing.T, target string, root *os.Root) {
|
||||
wantError := test.wantError
|
||||
if !wantError {
|
||||
fi, err := os.Lstat(filepath.Join(root.Name(), test.open))
|
||||
if err == nil && fi.Mode().Type() == fs.ModeSymlink {
|
||||
// This case is trying to mkdir("some symlink"),
|
||||
// which is an error.
|
||||
wantError = true
|
||||
}
|
||||
}
|
||||
|
||||
err := root.Mkdir(test.open, 0o777)
|
||||
if errEndsTest(t, err, wantError, "root.Create(%q)", test.open) {
|
||||
return
|
||||
}
|
||||
fi, err := os.Lstat(target)
|
||||
if err != nil {
|
||||
t.Fatalf(`stat file created with Root.Mkdir(%q): %v`, test.open, err)
|
||||
}
|
||||
if !fi.IsDir() {
|
||||
t.Fatalf(`stat file created with Root.Mkdir(%q): not a directory`, test.open)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRootOpenRoot(t *testing.T) {
|
||||
for _, test := range rootTestCases {
|
||||
test.run(t, func(t *testing.T, target string, root *os.Root) {
|
||||
if target != "" {
|
||||
if err := os.Mkdir(target, 0o777); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(target+"/f", nil, 0o666); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
rr, err := root.OpenRoot(test.open)
|
||||
if errEndsTest(t, err, test.wantError, "root.OpenRoot(%q)", test.open) {
|
||||
return
|
||||
}
|
||||
defer rr.Close()
|
||||
f, err := rr.Open("f")
|
||||
if err != nil {
|
||||
t.Fatalf(`root.OpenRoot(%q).Open("f") = %v`, test.open, err)
|
||||
}
|
||||
f.Close()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRootOpenFileAsRoot(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
target := filepath.Join(dir, "target")
|
||||
if err := os.WriteFile(target, nil, 0o666); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err := os.OpenRoot(target)
|
||||
if err == nil {
|
||||
t.Fatal("os.OpenRoot(file) succeeded; want failure")
|
||||
}
|
||||
r, err := os.OpenRoot(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer r.Close()
|
||||
_, err = r.OpenRoot("target")
|
||||
if err == nil {
|
||||
t.Fatal("Root.OpenRoot(file) succeeded; want failure")
|
||||
}
|
||||
}
|
||||
|
||||
// A rootConsistencyTest is a test case comparing os.Root behavior with
|
||||
// the corresponding non-Root function.
|
||||
//
|
||||
// These tests verify that, for example, Root.Open("file/./") and os.Open("file/./")
|
||||
// have the same result, although the specific result may vary by platform.
|
||||
type rootConsistencyTest struct {
|
||||
name string
|
||||
|
||||
// fs is the test filesystem layout. See makefs above.
|
||||
// fsFunc is called to modify the test filesystem, or replace it.
|
||||
fs []string
|
||||
fsFunc func(t *testing.T, dir string) string
|
||||
|
||||
// open is the filename to access in the test.
|
||||
open string
|
||||
|
||||
// detailedErrorMismatch indicates that os.Root and the corresponding non-Root
|
||||
// function return different errors for this test.
|
||||
detailedErrorMismatch func(t *testing.T) bool
|
||||
}
|
||||
|
||||
var rootConsistencyTestCases = []rootConsistencyTest{{
|
||||
name: "file",
|
||||
fs: []string{
|
||||
"target",
|
||||
},
|
||||
open: "target",
|
||||
}, {
|
||||
name: "dir slash dot",
|
||||
fs: []string{
|
||||
"target/file",
|
||||
},
|
||||
open: "target/.",
|
||||
}, {
|
||||
name: "dot",
|
||||
fs: []string{
|
||||
"file",
|
||||
},
|
||||
open: ".",
|
||||
}, {
|
||||
name: "file slash dot",
|
||||
fs: []string{
|
||||
"target",
|
||||
},
|
||||
open: "target/.",
|
||||
detailedErrorMismatch: func(t *testing.T) bool {
|
||||
// FreeBSD returns EPERM in the non-Root case.
|
||||
return runtime.GOOS == "freebsd" && strings.HasPrefix(t.Name(), "TestRootConsistencyRemove")
|
||||
},
|
||||
}, {
|
||||
name: "dir slash",
|
||||
fs: []string{
|
||||
"target/file",
|
||||
},
|
||||
open: "target/",
|
||||
}, {
|
||||
name: "dot slash",
|
||||
fs: []string{
|
||||
"file",
|
||||
},
|
||||
open: "./",
|
||||
}, {
|
||||
name: "file slash",
|
||||
fs: []string{
|
||||
"target",
|
||||
},
|
||||
open: "target/",
|
||||
detailedErrorMismatch: func(t *testing.T) bool {
|
||||
// os.Create returns ENOTDIR or EISDIR depending on the platform.
|
||||
return runtime.GOOS == "js"
|
||||
},
|
||||
}, {
|
||||
name: "file in path",
|
||||
fs: []string{
|
||||
"file",
|
||||
},
|
||||
open: "file/target",
|
||||
}, {
|
||||
name: "directory in path missing",
|
||||
open: "dir/target",
|
||||
}, {
|
||||
name: "target does not exist",
|
||||
open: "target",
|
||||
}, {
|
||||
name: "symlink slash",
|
||||
fs: []string{
|
||||
"target/file",
|
||||
"link => target",
|
||||
},
|
||||
open: "link/",
|
||||
}, {
|
||||
name: "unresolved symlink",
|
||||
fs: []string{
|
||||
"link => target",
|
||||
},
|
||||
open: "link",
|
||||
}, {
|
||||
name: "resolved symlink",
|
||||
fs: []string{
|
||||
"link => target",
|
||||
"target",
|
||||
},
|
||||
open: "link",
|
||||
}, {
|
||||
name: "dotdot in path after symlink",
|
||||
fs: []string{
|
||||
"a => b/c",
|
||||
"b/c/",
|
||||
"b/target",
|
||||
},
|
||||
open: "a/../target",
|
||||
}, {
|
||||
name: "long file name",
|
||||
open: strings.Repeat("a", 500),
|
||||
}, {
|
||||
name: "unreadable directory",
|
||||
fs: []string{
|
||||
"dir/target",
|
||||
},
|
||||
fsFunc: func(t *testing.T, dir string) string {
|
||||
os.Chmod(filepath.Join(dir, "dir"), 0)
|
||||
t.Cleanup(func() {
|
||||
os.Chmod(filepath.Join(dir, "dir"), 0o700)
|
||||
})
|
||||
return dir
|
||||
},
|
||||
open: "dir/target",
|
||||
}, {
|
||||
name: "unix domain socket target",
|
||||
fsFunc: func(t *testing.T, dir string) string {
|
||||
return tempDirWithUnixSocket(t, "a")
|
||||
},
|
||||
open: "a",
|
||||
}, {
|
||||
name: "unix domain socket in path",
|
||||
fsFunc: func(t *testing.T, dir string) string {
|
||||
return tempDirWithUnixSocket(t, "a")
|
||||
},
|
||||
open: "a/b",
|
||||
detailedErrorMismatch: func(t *testing.T) bool {
|
||||
// On Windows, os.Root.Open returns "The directory name is invalid."
|
||||
// and os.Open returns "The file cannot be accessed by the system.".
|
||||
return runtime.GOOS == "windows"
|
||||
},
|
||||
}, {
|
||||
name: "question mark",
|
||||
open: "?",
|
||||
}, {
|
||||
name: "nul byte",
|
||||
open: "\x00",
|
||||
}}
|
||||
|
||||
func tempDirWithUnixSocket(t *testing.T, name string) string {
|
||||
dir, err := os.MkdirTemp("", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
})
|
||||
addr, err := net.ResolveUnixAddr("unix", filepath.Join(dir, name))
|
||||
if err != nil {
|
||||
t.Skipf("net.ResolveUnixAddr: %v", err)
|
||||
}
|
||||
conn, err := net.ListenUnix("unix", addr)
|
||||
if err != nil {
|
||||
t.Skipf("net.ListenUnix: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
conn.Close()
|
||||
})
|
||||
return dir
|
||||
}
|
||||
|
||||
func (test rootConsistencyTest) run(t *testing.T, f func(t *testing.T, path string, r *os.Root) (string, error)) {
|
||||
if runtime.GOOS == "wasip1" {
|
||||
// On wasip, non-Root functions clean paths before opening them,
|
||||
// resulting in inconsistent behavior.
|
||||
// https://go.dev/issue/69509
|
||||
t.Skip("#69509: inconsistent results on wasip1")
|
||||
}
|
||||
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
dir1 := makefs(t, test.fs)
|
||||
dir2 := makefs(t, test.fs)
|
||||
if test.fsFunc != nil {
|
||||
dir1 = test.fsFunc(t, dir1)
|
||||
dir2 = test.fsFunc(t, dir2)
|
||||
}
|
||||
|
||||
r, err := os.OpenRoot(dir1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
res1, err1 := f(t, test.open, r)
|
||||
res2, err2 := f(t, dir2+"/"+test.open, nil)
|
||||
|
||||
if res1 != res2 || ((err1 == nil) != (err2 == nil)) {
|
||||
t.Errorf("with root: res=%v", res1)
|
||||
t.Errorf(" err=%v", err1)
|
||||
t.Errorf("without root: res=%v", res2)
|
||||
t.Errorf(" err=%v", err2)
|
||||
t.Errorf("want consistent results, got mismatch")
|
||||
}
|
||||
|
||||
if err1 != nil || err2 != nil {
|
||||
e1, ok := err1.(*os.PathError)
|
||||
if !ok {
|
||||
t.Fatalf("with root, expected PathError; got: %v", err1)
|
||||
}
|
||||
e2, ok := err2.(*os.PathError)
|
||||
if !ok {
|
||||
t.Fatalf("without root, expected PathError; got: %v", err1)
|
||||
}
|
||||
detailedErrorMismatch := false
|
||||
if f := test.detailedErrorMismatch; f != nil {
|
||||
detailedErrorMismatch = f(t)
|
||||
}
|
||||
if !detailedErrorMismatch && e1.Err != e2.Err {
|
||||
t.Errorf("with root: err=%v", e1.Err)
|
||||
t.Errorf("without root: err=%v", e2.Err)
|
||||
t.Errorf("want consistent results, got mismatch")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRootConsistencyOpen(t *testing.T) {
|
||||
for _, test := range rootConsistencyTestCases {
|
||||
test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
|
||||
var f *os.File
|
||||
var err error
|
||||
if r == nil {
|
||||
f, err = os.Open(path)
|
||||
} else {
|
||||
f, err = r.Open(path)
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
fi, err := f.Stat()
|
||||
if err == nil && !fi.IsDir() {
|
||||
b, err := io.ReadAll(f)
|
||||
return string(b), err
|
||||
} else {
|
||||
names, err := f.Readdirnames(-1)
|
||||
slices.Sort(names)
|
||||
return fmt.Sprintf("%q", names), err
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRootConsistencyCreate(t *testing.T) {
|
||||
for _, test := range rootConsistencyTestCases {
|
||||
test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
|
||||
var f *os.File
|
||||
var err error
|
||||
if r == nil {
|
||||
f, err = os.Create(path)
|
||||
} else {
|
||||
f, err = r.Create(path)
|
||||
}
|
||||
if err == nil {
|
||||
f.Write([]byte("file contents"))
|
||||
f.Close()
|
||||
}
|
||||
return "", err
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRootConsistencyMkdir(t *testing.T) {
|
||||
for _, test := range rootConsistencyTestCases {
|
||||
test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
|
||||
var err error
|
||||
if r == nil {
|
||||
err = os.Mkdir(path, 0o777)
|
||||
} else {
|
||||
err = r.Mkdir(path, 0o777)
|
||||
}
|
||||
return "", err
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRootRenameAfterOpen(t *testing.T) {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
t.Skip("renaming open files not supported on " + runtime.GOOS)
|
||||
case "js", "plan9":
|
||||
t.Skip("openat not supported on " + runtime.GOOS)
|
||||
case "wasip1":
|
||||
if os.Getenv("GOWASIRUNTIME") == "wazero" {
|
||||
t.Skip("wazero does not track renamed directories")
|
||||
}
|
||||
}
|
||||
|
||||
dir := t.TempDir()
|
||||
|
||||
// Create directory "a" and open it.
|
||||
if err := os.Mkdir(filepath.Join(dir, "a"), 0o777); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dirf, err := os.OpenRoot(filepath.Join(dir, "a"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer dirf.Close()
|
||||
|
||||
// Rename "a" => "b", and create "b/f".
|
||||
if err := os.Rename(filepath.Join(dir, "a"), filepath.Join(dir, "b")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "b/f"), []byte("hello"), 0o666); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Open "f", and confirm that we see it.
|
||||
f, err := dirf.OpenFile("f", os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("reading file after renaming parent: %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
b, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := string(b), "hello"; got != want {
|
||||
t.Fatalf("file contents: %q, want %q", got, want)
|
||||
}
|
||||
|
||||
// f.Name reflects the original path we opened the directory under (".../a"), not "b".
|
||||
if got, want := f.Name(), dirf.Name()+string(os.PathSeparator)+"f"; got != want {
|
||||
t.Errorf("f.Name() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRootNonPermissionMode(t *testing.T) {
|
||||
r, err := os.OpenRoot(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer r.Close()
|
||||
if _, err := r.OpenFile("file", os.O_RDWR|os.O_CREATE, 0o1777); err == nil {
|
||||
t.Errorf("r.OpenFile(file, O_RDWR|O_CREATE, 0o1777) succeeded; want error")
|
||||
}
|
||||
if err := r.Mkdir("file", 0o1777); err == nil {
|
||||
t.Errorf("r.Mkdir(file, 0o1777) succeeded; want error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRootUseAfterClose(t *testing.T) {
|
||||
r, err := os.OpenRoot(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r.Close()
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
f func(r *os.Root, filename string) error
|
||||
}{{
|
||||
name: "Open",
|
||||
f: func(r *os.Root, filename string) error {
|
||||
_, err := r.Open(filename)
|
||||
return err
|
||||
},
|
||||
}, {
|
||||
name: "Create",
|
||||
f: func(r *os.Root, filename string) error {
|
||||
_, err := r.Create(filename)
|
||||
return err
|
||||
},
|
||||
}, {
|
||||
name: "OpenFile",
|
||||
f: func(r *os.Root, filename string) error {
|
||||
_, err := r.OpenFile(filename, os.O_RDWR, 0o666)
|
||||
return err
|
||||
},
|
||||
}, {
|
||||
name: "OpenRoot",
|
||||
f: func(r *os.Root, filename string) error {
|
||||
_, err := r.OpenRoot(filename)
|
||||
return err
|
||||
},
|
||||
}, {
|
||||
name: "Mkdir",
|
||||
f: func(r *os.Root, filename string) error {
|
||||
return r.Mkdir(filename, 0o777)
|
||||
},
|
||||
}} {
|
||||
err := test.f(r, "target")
|
||||
pe, ok := err.(*os.PathError)
|
||||
if !ok || pe.Path != "target" || pe.Err != os.ErrClosed {
|
||||
t.Errorf(`r.%v = %v; want &PathError{Path: "target", Err: ErrClosed}`, test.name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRootConcurrentClose(t *testing.T) {
|
||||
r, err := os.OpenRoot(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ch := make(chan error, 1)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
first := true
|
||||
for {
|
||||
f, err := r.OpenFile("file", os.O_RDWR|os.O_CREATE, 0o666)
|
||||
if err != nil {
|
||||
ch <- err
|
||||
return
|
||||
}
|
||||
if first {
|
||||
ch <- nil
|
||||
first = false
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
}()
|
||||
if err := <-ch; err != nil {
|
||||
t.Errorf("OpenFile: %v, want success", err)
|
||||
}
|
||||
r.Close()
|
||||
if err := <-ch; !errors.Is(err, os.ErrClosed) {
|
||||
t.Errorf("OpenFile: %v, want ErrClosed", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRootRaceRenameDir attempts to escape a Root by renaming a path component mid-parse.
|
||||
//
|
||||
// We create a deeply nested directory:
|
||||
//
|
||||
// base/a/a/a/a/ [...] /a
|
||||
//
|
||||
// And a path that descends into the tree, then returns to the top using ..:
|
||||
//
|
||||
// base/a/a/a/a/ [...] /a/../../../ [..] /../a/f
|
||||
//
|
||||
// While opening this file, we rename base/a/a to base/b.
|
||||
// A naive lookup operation will resolve the path to base/f.
|
||||
func TestRootRaceRenameDir(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
r, err := os.OpenRoot(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
const depth = 4
|
||||
|
||||
os.MkdirAll(dir+"/base/"+strings.Repeat("/a", depth), 0o777)
|
||||
|
||||
path := "base/" + strings.Repeat("a/", depth) + strings.Repeat("../", depth) + "a/f"
|
||||
os.WriteFile(dir+"/f", []byte("secret"), 0o666)
|
||||
os.WriteFile(dir+"/base/a/f", []byte("public"), 0o666)
|
||||
|
||||
// Compute how long it takes to open the path in the common case.
|
||||
const tries = 10
|
||||
var total time.Duration
|
||||
for range tries {
|
||||
start := time.Now()
|
||||
f, err := r.Open(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(b) != "public" {
|
||||
t.Fatalf("read %q, want %q", b, "public")
|
||||
}
|
||||
f.Close()
|
||||
total += time.Since(start)
|
||||
}
|
||||
avg := total / tries
|
||||
|
||||
// We're trying to exploit a race, so try this a number of times.
|
||||
for range 100 {
|
||||
// Start a goroutine to open the file.
|
||||
gotc := make(chan []byte)
|
||||
go func() {
|
||||
f, err := r.Open(path)
|
||||
if err != nil {
|
||||
gotc <- nil
|
||||
}
|
||||
defer f.Close()
|
||||
b, _ := io.ReadAll(f)
|
||||
gotc <- b
|
||||
}()
|
||||
|
||||
// Wait for the open operation to partially complete,
|
||||
// and then rename a directory near the root.
|
||||
time.Sleep(avg / 4)
|
||||
if err := os.Rename(dir+"/base/a", dir+"/b"); err != nil {
|
||||
// Windows won't let us rename a directory if we have
|
||||
// an open handle for it, so an error here is expected.
|
||||
if runtime.GOOS != "windows" {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
got := <-gotc
|
||||
os.Rename(dir+"/b", dir+"/base/a")
|
||||
if len(got) > 0 && string(got) != "public" {
|
||||
t.Errorf("read file: %q; want error or 'public'", got)
|
||||
}
|
||||
}
|
||||
}
|
158
src/os/root_unix.go
Normal file
158
src/os/root_unix.go
Normal file
@ -0,0 +1,158 @@
|
||||
// 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.
|
||||
|
||||
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || wasip1
|
||||
|
||||
package os
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"internal/syscall/unix"
|
||||
"runtime"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
type sysfdType = int
|
||||
|
||||
// openRootNolog is OpenRoot.
|
||||
func openRootNolog(name string) (*Root, error) {
|
||||
var fd int
|
||||
err := ignoringEINTR(func() error {
|
||||
var err error
|
||||
fd, _, err = open(name, syscall.O_CLOEXEC, 0)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, &PathError{Op: "open", Path: name, Err: err}
|
||||
}
|
||||
return newRoot(fd, name)
|
||||
}
|
||||
|
||||
// newRoot returns a new Root.
|
||||
// If fd is not a directory, it closes it and returns an error.
|
||||
func newRoot(fd int, name string) (*Root, error) {
|
||||
var fs fileStat
|
||||
err := ignoringEINTR(func() error {
|
||||
return syscall.Fstat(fd, &fs.sys)
|
||||
})
|
||||
fillFileStatFromSys(&fs, name)
|
||||
if err == nil && !fs.IsDir() {
|
||||
syscall.Close(fd)
|
||||
return nil, &PathError{Op: "open", Path: name, Err: errors.New("not a directory")}
|
||||
}
|
||||
|
||||
// There's a race here with fork/exec, which we are
|
||||
// content to live with. See ../syscall/exec_unix.go.
|
||||
if !supportsCloseOnExec {
|
||||
syscall.CloseOnExec(fd)
|
||||
}
|
||||
|
||||
r := &Root{root{
|
||||
fd: fd,
|
||||
name: name,
|
||||
}}
|
||||
runtime.SetFinalizer(&r.root, (*root).Close)
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// openRootInRoot is Root.OpenRoot.
|
||||
func openRootInRoot(r *Root, name string) (*Root, error) {
|
||||
fd, err := doInRoot(r, name, func(parent int, name string) (fd int, err error) {
|
||||
ignoringEINTR(func() error {
|
||||
fd, err = unix.Openat(parent, name, syscall.O_NOFOLLOW|syscall.O_CLOEXEC, 0)
|
||||
if err == syscall.ELOOP || err == syscall.EMLINK {
|
||||
err = checkSymlink(parent, name, err)
|
||||
}
|
||||
return err
|
||||
})
|
||||
return fd, err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, &PathError{Op: "openat", Path: name, Err: err}
|
||||
}
|
||||
return newRoot(fd, name)
|
||||
}
|
||||
|
||||
// rootOpenFileNolog is Root.OpenFile.
|
||||
func rootOpenFileNolog(root *Root, name string, flag int, perm FileMode) (*File, error) {
|
||||
fd, err := doInRoot(root, name, func(parent int, name string) (fd int, err error) {
|
||||
ignoringEINTR(func() error {
|
||||
fd, err = unix.Openat(parent, name, syscall.O_NOFOLLOW|syscall.O_CLOEXEC|flag, uint32(perm))
|
||||
if err == syscall.ELOOP || err == syscall.ENOTDIR || err == syscall.EMLINK {
|
||||
err = checkSymlink(parent, name, err)
|
||||
}
|
||||
return err
|
||||
})
|
||||
return fd, err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, &PathError{Op: "openat", Path: name, Err: err}
|
||||
}
|
||||
f := newFile(fd, joinPath(root.Name(), name), kindOpenFile, unix.HasNonblockFlag(flag))
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func rootOpenDir(parent int, name string) (int, error) {
|
||||
var (
|
||||
fd int
|
||||
err error
|
||||
)
|
||||
ignoringEINTR(func() error {
|
||||
fd, err = unix.Openat(parent, name, syscall.O_NOFOLLOW|syscall.O_CLOEXEC|syscall.O_DIRECTORY, 0)
|
||||
if err == syscall.ELOOP || err == syscall.ENOTDIR || err == syscall.EMLINK {
|
||||
err = checkSymlink(parent, name, err)
|
||||
} else if err == syscall.ENOTSUP || err == syscall.EOPNOTSUPP {
|
||||
// ENOTSUP and EOPNOTSUPP are often, but not always, the same errno.
|
||||
// Translate both to ENOTDIR, since this indicates a non-terminal
|
||||
// path component was not a directory.
|
||||
err = syscall.ENOTDIR
|
||||
}
|
||||
return err
|
||||
})
|
||||
return fd, err
|
||||
}
|
||||
|
||||
func mkdirat(fd int, name string, perm FileMode) error {
|
||||
return ignoringEINTR(func() error {
|
||||
return unix.Mkdirat(fd, name, syscallMode(perm))
|
||||
})
|
||||
}
|
||||
|
||||
// checkSymlink resolves the symlink name in parent,
|
||||
// and returns errSymlink with the link contents.
|
||||
//
|
||||
// If name is not a symlink, return origError.
|
||||
func checkSymlink(parent int, name string, origError error) error {
|
||||
link, err := readlinkat(parent, name)
|
||||
if err != nil {
|
||||
return origError
|
||||
}
|
||||
return errSymlink(link)
|
||||
}
|
||||
|
||||
func readlinkat(fd int, name string) (string, error) {
|
||||
for len := 128; ; len *= 2 {
|
||||
b := make([]byte, len)
|
||||
var (
|
||||
n int
|
||||
e error
|
||||
)
|
||||
ignoringEINTR(func() error {
|
||||
n, e = unix.Readlinkat(fd, name, b)
|
||||
return e
|
||||
})
|
||||
if e == syscall.ERANGE {
|
||||
continue
|
||||
}
|
||||
if e != nil {
|
||||
return "", e
|
||||
}
|
||||
if n < 0 {
|
||||
n = 0
|
||||
}
|
||||
if n < len {
|
||||
return string(b[0:n]), nil
|
||||
}
|
||||
}
|
||||
}
|
203
src/os/root_windows.go
Normal file
203
src/os/root_windows.go
Normal file
@ -0,0 +1,203 @@
|
||||
// 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.
|
||||
|
||||
//go:build windows
|
||||
|
||||
package os
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"internal/filepathlite"
|
||||
"internal/stringslite"
|
||||
"internal/syscall/windows"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// rootCleanPath uses GetFullPathName to perform lexical path cleaning.
|
||||
//
|
||||
// On Windows, file names are lexically cleaned at the start of a file operation.
|
||||
// For example, on Windows the path `a\..\b` is exactly equivalent to `b` alone,
|
||||
// even if `a` does not exist or is not a directory.
|
||||
//
|
||||
// We use the Windows API function GetFullPathName to perform this cleaning.
|
||||
// We could do this ourselves, but there are a number of subtle behaviors here,
|
||||
// and deferring to the OS maintains consistency.
|
||||
// (For example, `a\.\` cleans to `a\`.)
|
||||
//
|
||||
// GetFullPathName operates on absolute paths, and our input path is relative.
|
||||
// We make the path absolute by prepending a fixed prefix of \\?\?\.
|
||||
//
|
||||
// We want to detect paths which use .. components to escape the root.
|
||||
// We do this by ensuring the cleaned path still begins with \\?\?\.
|
||||
// We catch the corner case of a path which includes a ..\?\. component
|
||||
// by rejecting any input paths which contain a ?, which is not a valid character
|
||||
// in a Windows filename.
|
||||
func rootCleanPath(s string, prefix, suffix []string) (string, error) {
|
||||
// Reject paths which include a ? component (see above).
|
||||
if stringslite.IndexByte(s, '?') >= 0 {
|
||||
return "", windows.ERROR_INVALID_NAME
|
||||
}
|
||||
|
||||
const fixedPrefix = `\\?\?`
|
||||
buf := []byte(fixedPrefix)
|
||||
for _, p := range prefix {
|
||||
buf = append(buf, '\\')
|
||||
buf = append(buf, []byte(p)...)
|
||||
}
|
||||
buf = append(buf, '\\')
|
||||
buf = append(buf, []byte(s)...)
|
||||
for _, p := range suffix {
|
||||
buf = append(buf, '\\')
|
||||
buf = append(buf, []byte(p)...)
|
||||
}
|
||||
s = string(buf)
|
||||
|
||||
s, err := syscall.FullPath(s)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
s, ok := stringslite.CutPrefix(s, fixedPrefix)
|
||||
if !ok {
|
||||
return "", errPathEscapes
|
||||
}
|
||||
s = stringslite.TrimPrefix(s, `\`)
|
||||
if s == "" {
|
||||
s = "."
|
||||
}
|
||||
|
||||
if !filepathlite.IsLocal(s) {
|
||||
return "", errPathEscapes
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
type sysfdType = syscall.Handle
|
||||
|
||||
// openRootNolog is OpenRoot.
|
||||
func openRootNolog(name string) (*Root, error) {
|
||||
if name == "" {
|
||||
return nil, &PathError{Op: "open", Path: name, Err: syscall.ENOENT}
|
||||
}
|
||||
path := fixLongPath(name)
|
||||
fd, err := syscall.Open(path, syscall.O_RDONLY|syscall.O_CLOEXEC, 0)
|
||||
if err != nil {
|
||||
return nil, &PathError{Op: "open", Path: name, Err: err}
|
||||
}
|
||||
return newRoot(fd, name)
|
||||
}
|
||||
|
||||
// newRoot returns a new Root.
|
||||
// If fd is not a directory, it closes it and returns an error.
|
||||
func newRoot(fd syscall.Handle, name string) (*Root, error) {
|
||||
// Check that this is a directory.
|
||||
//
|
||||
// If we get any errors here, ignore them; worst case we create a Root
|
||||
// which returns errors when you try to use it.
|
||||
var fi syscall.ByHandleFileInformation
|
||||
err := syscall.GetFileInformationByHandle(fd, &fi)
|
||||
if err == nil && fi.FileAttributes&syscall.FILE_ATTRIBUTE_DIRECTORY == 0 {
|
||||
syscall.CloseHandle(fd)
|
||||
return nil, &PathError{Op: "open", Path: name, Err: errors.New("not a directory")}
|
||||
}
|
||||
|
||||
r := &Root{root{
|
||||
fd: fd,
|
||||
name: name,
|
||||
}}
|
||||
runtime.SetFinalizer(&r.root, (*root).Close)
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// openRootInRoot is Root.OpenRoot.
|
||||
func openRootInRoot(r *Root, name string) (*Root, error) {
|
||||
fd, err := doInRoot(r, name, rootOpenDir)
|
||||
if err != nil {
|
||||
return nil, &PathError{Op: "openat", Path: name, Err: err}
|
||||
}
|
||||
return newRoot(fd, name)
|
||||
}
|
||||
|
||||
// rootOpenFileNolog is Root.OpenFile.
|
||||
func rootOpenFileNolog(root *Root, name string, flag int, perm FileMode) (*File, error) {
|
||||
fd, err := doInRoot(root, name, func(parent syscall.Handle, name string) (syscall.Handle, error) {
|
||||
return openat(parent, name, flag, perm)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, &PathError{Op: "openat", Path: name, Err: err}
|
||||
}
|
||||
return newFile(fd, joinPath(root.Name(), name), "file"), nil
|
||||
}
|
||||
|
||||
func openat(dirfd syscall.Handle, name string, flag int, perm FileMode) (syscall.Handle, error) {
|
||||
h, err := windows.Openat(dirfd, name, flag|syscall.O_CLOEXEC|windows.O_NOFOLLOW_ANY, syscallMode(perm))
|
||||
if err == syscall.ELOOP || err == syscall.ENOTDIR {
|
||||
if link, err := readReparseLinkAt(dirfd, name); err == nil {
|
||||
return syscall.InvalidHandle, errSymlink(link)
|
||||
}
|
||||
}
|
||||
return h, err
|
||||
}
|
||||
|
||||
func readReparseLinkAt(dirfd syscall.Handle, name string) (string, error) {
|
||||
objectName, err := windows.NewNTUnicodeString(name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
objAttrs := &windows.OBJECT_ATTRIBUTES{
|
||||
ObjectName: objectName,
|
||||
}
|
||||
if dirfd != syscall.InvalidHandle {
|
||||
objAttrs.RootDirectory = dirfd
|
||||
}
|
||||
objAttrs.Length = uint32(unsafe.Sizeof(*objAttrs))
|
||||
var h syscall.Handle
|
||||
err = windows.NtCreateFile(
|
||||
&h,
|
||||
windows.FILE_GENERIC_READ,
|
||||
objAttrs,
|
||||
&windows.IO_STATUS_BLOCK{},
|
||||
nil,
|
||||
uint32(syscall.FILE_ATTRIBUTE_NORMAL),
|
||||
syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE,
|
||||
windows.FILE_OPEN,
|
||||
windows.FILE_SYNCHRONOUS_IO_NONALERT|windows.FILE_OPEN_REPARSE_POINT,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer syscall.CloseHandle(h)
|
||||
return readReparseLinkHandle(h)
|
||||
}
|
||||
|
||||
func rootOpenDir(parent syscall.Handle, name string) (syscall.Handle, error) {
|
||||
h, err := openat(parent, name, syscall.O_RDONLY|syscall.O_CLOEXEC|windows.O_DIRECTORY, 0)
|
||||
if err == syscall.ERROR_FILE_NOT_FOUND {
|
||||
// Windows returns:
|
||||
// - ERROR_PATH_NOT_FOUND if any path compoenent before the leaf
|
||||
// does not exist or is not a directory.
|
||||
// - ERROR_FILE_NOT_FOUND if the leaf does not exist.
|
||||
//
|
||||
// This differs from Unix behavior, which is:
|
||||
// - ENOENT if any path component does not exist, including the leaf.
|
||||
// - ENOTDIR if any path component before the leaf is not a directory.
|
||||
//
|
||||
// We map syscall.ENOENT to ERROR_FILE_NOT_FOUND and syscall.ENOTDIR
|
||||
// to ERROR_PATH_NOT_FOUND, but the Windows errors don't quite match.
|
||||
//
|
||||
// For consistency with os.Open, convert ERROR_FILE_NOT_FOUND here into
|
||||
// ERROR_PATH_NOT_FOUND, since we're opening a non-leaf path component.
|
||||
err = syscall.ERROR_PATH_NOT_FOUND
|
||||
}
|
||||
return h, err
|
||||
}
|
||||
|
||||
func mkdirat(dirfd syscall.Handle, name string, perm FileMode) error {
|
||||
return windows.Mkdirat(dirfd, name, syscallMode(perm))
|
||||
}
|
46
src/os/root_windows_test.go
Normal file
46
src/os/root_windows_test.go
Normal file
@ -0,0 +1,46 @@
|
||||
// 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.
|
||||
|
||||
//go:build windows
|
||||
|
||||
package os_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Verify that Root.Open rejects Windows reserved names.
|
||||
func TestRootWindowsDeviceNames(t *testing.T) {
|
||||
r, err := os.OpenRoot(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer r.Close()
|
||||
if f, err := r.Open("NUL"); err == nil {
|
||||
t.Errorf(`r.Open("NUL") succeeded; want error"`)
|
||||
f.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that Root.Open is case-insensitive.
|
||||
// (The wrong options to NtOpenFile could make operations case-sensitive,
|
||||
// so this is worth checking.)
|
||||
func TestRootWindowsCaseInsensitivity(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(dir, "file"), nil, 0666); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r, err := os.OpenRoot(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer r.Close()
|
||||
f, err := r.Open("FILE")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.Close()
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user