]> Cypherpunks repositories - gostls13.git/commitdiff
os: add Root.FS
authorDamien Neil <dneil@google.com>
Tue, 19 Nov 2024 01:49:14 +0000 (17:49 -0800)
committerDamien Neil <dneil@google.com>
Wed, 20 Nov 2024 23:21:42 +0000 (23:21 +0000)
For #67002

Change-Id: Ib687c92d645b9172677e5781a3e51ef1a0427c30
Reviewed-on: https://go-review.googlesource.com/c/go/+/629518
Reviewed-by: Ian Lance Taylor <iant@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>

api/next/67002.txt
src/os/file.go
src/os/os_test.go
src/os/root.go
src/os/stat_wasip1.go

index fc839e95e40b7e4bfbafd94f743908145e8f43b0..67c47969f4f3f67b410b3a9ddb7c054a5e82597e 100644 (file)
@@ -1,6 +1,7 @@
 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) FS() fs.FS #67002
 pkg os, method (*Root) Lstat(string) (fs.FileInfo, error) #67002
 pkg os, method (*Root) Mkdir(string, fs.FileMode) error #67002
 pkg os, method (*Root) Name() string #67002
index 0e2948867c841ba79b386b453ff34f5e7eb63ec0..a5063680f90589ab21fa71029817cd5492b31ca2 100644 (file)
@@ -696,6 +696,8 @@ func (f *File) SyscallConn() (syscall.RawConn, error) {
 // a general substitute for a chroot-style security mechanism when the directory tree
 // contains arbitrary content.
 //
+// Use [Root.FS] to obtain a fs.FS that prevents escapes from the tree via symbolic links.
+//
 // The directory dir must not be "".
 //
 // The result implements [io/fs.StatFS], [io/fs.ReadFileFS] and
@@ -800,7 +802,10 @@ func ReadFile(name string) ([]byte, error) {
                return nil, err
        }
        defer f.Close()
+       return readFileContents(f)
+}
 
+func readFileContents(f *File) ([]byte, error) {
        var size int
        if info, err := f.Stat(); err == nil {
                size64 := info.Size()
index c646ca82463f231710c48fe440d8f717945c5023..dbf77db99042afe3e9724c4064a3665d58e8dd3c 100644 (file)
@@ -3188,10 +3188,21 @@ func forceMFTUpdateOnWindows(t *testing.T, path string) {
 
 func TestDirFS(t *testing.T) {
        t.Parallel()
+       testDirFS(t, DirFS("./testdata/dirfs"))
+}
+
+func TestRootDirFS(t *testing.T) {
+       t.Parallel()
+       r, err := OpenRoot("./testdata/dirfs")
+       if err != nil {
+               t.Fatal(err)
+       }
+       testDirFS(t, r.FS())
+}
 
+func testDirFS(t *testing.T, fsys fs.FS) {
        forceMFTUpdateOnWindows(t, "./testdata/dirfs")
 
-       fsys := DirFS("./testdata/dirfs")
        if err := fstest.TestFS(fsys, "a", "b", "dir/x"); err != nil {
                t.Fatal(err)
        }
index 1070698f4d6788748c58bb0145b7a96be9e757e0..c7d9b5b071215daf3e9d4ffc07b8970b518b5568 100644 (file)
@@ -6,8 +6,12 @@ package os
 
 import (
        "errors"
+       "internal/bytealg"
+       "internal/stringslite"
        "internal/testlog"
+       "io/fs"
        "runtime"
+       "slices"
 )
 
 // Root may be used to only access files within a single directory tree.
@@ -213,3 +217,87 @@ func splitPathInRoot(s string, prefix, suffix []string) (_ []string, err error)
        parts = append(parts, suffix...)
        return parts, nil
 }
+
+// FS returns a file system (an fs.FS) for the tree of files in the root.
+//
+// The result implements [io/fs.StatFS], [io/fs.ReadFileFS] and
+// [io/fs.ReadDirFS].
+func (r *Root) FS() fs.FS {
+       return (*rootFS)(r)
+}
+
+type rootFS Root
+
+func (rfs *rootFS) Open(name string) (fs.File, error) {
+       r := (*Root)(rfs)
+       if !isValidRootFSPath(name) {
+               return nil, &PathError{Op: "open", Path: name, Err: ErrInvalid}
+       }
+       f, err := r.Open(name)
+       if err != nil {
+               return nil, err
+       }
+       return f, nil
+}
+
+func (rfs *rootFS) ReadDir(name string) ([]DirEntry, error) {
+       r := (*Root)(rfs)
+       if !isValidRootFSPath(name) {
+               return nil, &PathError{Op: "readdir", Path: name, Err: ErrInvalid}
+       }
+
+       // This isn't efficient: We just open a regular file and ReadDir it.
+       // Ideally, we would skip creating a *File entirely and operate directly
+       // on the file descriptor, but that will require some extensive reworking
+       // of directory reading in general.
+       //
+       // This suffices for the moment.
+       f, err := r.Open(name)
+       if err != nil {
+               return nil, err
+       }
+       defer f.Close()
+       dirs, err := f.ReadDir(-1)
+       slices.SortFunc(dirs, func(a, b DirEntry) int {
+               return bytealg.CompareString(a.Name(), b.Name())
+       })
+       return dirs, err
+}
+
+func (rfs *rootFS) ReadFile(name string) ([]byte, error) {
+       r := (*Root)(rfs)
+       if !isValidRootFSPath(name) {
+               return nil, &PathError{Op: "readfile", Path: name, Err: ErrInvalid}
+       }
+       f, err := r.Open(name)
+       if err != nil {
+               return nil, err
+       }
+       defer f.Close()
+       return readFileContents(f)
+}
+
+func (rfs *rootFS) Stat(name string) (FileInfo, error) {
+       r := (*Root)(rfs)
+       if !isValidRootFSPath(name) {
+               return nil, &PathError{Op: "stat", Path: name, Err: ErrInvalid}
+       }
+       return r.Stat(name)
+}
+
+// isValidRootFSPath reprots whether name is a valid filename to pass a Root.FS method.
+func isValidRootFSPath(name string) bool {
+       if !fs.ValidPath(name) {
+               return false
+       }
+       if runtime.GOOS == "windows" {
+               // fs.FS paths are /-separated.
+               // On Windows, reject the path if it contains any \ separators.
+               // Other forms of invalid path (for example, "NUL") are handled by
+               // Root's usual file lookup mechanisms.
+               if stringslite.IndexByte(name, '\\') >= 0 {
+                       return false
+               }
+       }
+       return true
+}
index 8561e44680df3a234a7037d623ff774732db9faa..dcf2e38ddec2d731b2263de788856cd36f34fb7e 100644 (file)
@@ -15,7 +15,6 @@ import (
 func fillFileStatFromSys(fs *fileStat, name string) {
        fs.name = filepathlite.Base(name)
        fs.size = int64(fs.sys.Size)
-       fs.mode = FileMode(fs.sys.Mode)
        fs.modTime = time.Unix(0, int64(fs.sys.Mtime))
 
        switch fs.sys.Filetype {