From e2ebce5d230eceff7d89e4d8677a166dd9ebabca Mon Sep 17 00:00:00 2001 From: Crypt Keeper <64215+codefromthecrypt@users.noreply.github.com> Date: Fri, 17 Feb 2023 20:55:03 +0900 Subject: [PATCH] sysfs: adds chmod (#1135) This adds `FS.Chmod` and implements it for `GOOS=js`. This function isn't defined in WASI snapshot01, but it is in `wasi-filesystem`, e.g. `change-file-permissions-at`. Signed-off-by: Adrian Cole --- internal/gojs/errno.go | 4 ++ internal/gojs/errno_test.go | 5 +++ internal/gojs/fs.go | 18 ++++++-- internal/gojs/testdata/writefs/main.go | 23 ++++++++++ internal/sysfs/adapter_test.go | 9 ++++ internal/sysfs/dirfs.go | 15 ++++--- internal/sysfs/dirfs_test.go | 45 +++++++++++++++++-- internal/sysfs/readfs_test.go | 8 ++++ internal/sysfs/sysfs.go | 33 +++++++++++++- internal/sysfs/sysfs_test.go | 22 +++++++++ internal/sysfs/unsupported.go | 5 +++ internal/wasi_snapshot_preview1/errno.go | 3 +- internal/wasi_snapshot_preview1/errno_test.go | 5 +++ 13 files changed, 181 insertions(+), 14 deletions(-) diff --git a/internal/gojs/errno.go b/internal/gojs/errno.go index 90fb1d7632..ea09bb4824 100644 --- a/internal/gojs/errno.go +++ b/internal/gojs/errno.go @@ -21,6 +21,8 @@ func (e *Errno) Error() string { // This order match constants from wasi_snapshot_preview1.ErrnoSuccess for // easier maintenance. var ( + // ErrnoAcces Permission denied. + ErrnoAcces = &Errno{"EACCES"} // ErrnoAgain Resource unavailable, or operation would block. ErrnoAgain = &Errno{"EAGAIN"} // ErrnoBadf Bad file descriptor. @@ -61,6 +63,8 @@ func ToErrno(err error) *Errno { errno := sysfs.UnwrapOSError(err) switch errno { + case syscall.EACCES: + return ErrnoAcces case syscall.EAGAIN: return ErrnoAgain case syscall.EBADF: diff --git a/internal/gojs/errno_test.go b/internal/gojs/errno_test.go index 48f66efa72..47bc972aff 100644 --- a/internal/gojs/errno_test.go +++ b/internal/gojs/errno_test.go @@ -13,6 +13,11 @@ func TestToErrno(t *testing.T) { input error expected *Errno }{ + { + name: "syscall.EACCES", + input: syscall.EACCES, + expected: ErrnoAcces, + }, { name: "syscall.EAGAIN", input: syscall.EAGAIN, diff --git a/internal/gojs/fs.go b/internal/gojs/fs.go index 9731727a12..d1f468d0f4 100644 --- a/internal/gojs/fs.go +++ b/internal/gojs/fs.go @@ -84,6 +84,8 @@ var ( // The following interfaces are used until we finalize our own FD-scoped file. type ( + // chmoder is implemented by os.File in file_posix.go + chmoder interface{ Chmod(fs.FileMode) error } // syncer is implemented by os.File in file_posix.go syncer interface{ Sync() error } // truncater is implemented by os.File in file_posix.go @@ -528,8 +530,8 @@ func (jsfsChmod) invoke(ctx context.Context, mod api.Module, args ...interface{} mode := goos.ValueToUint32(args[1]) callback := args[2].(funcWrapper) - _, _ = path, mode // TODO - var err error = syscall.ENOSYS + fsc := mod.(*wasm.CallContext).Sys.FS() + err := fsc.RootFS().Chmod(path, fs.FileMode(mode)) return jsfsInvoke(ctx, mod, callback, err) } @@ -544,8 +546,16 @@ func (jsfsFchmod) invoke(ctx context.Context, mod api.Module, args ...interface{ mode := goos.ValueToUint32(args[1]) callback := args[2].(funcWrapper) - _, _ = fd, mode // TODO - var err error = syscall.ENOSYS + // Check to see if the file descriptor is available + fsc := mod.(*wasm.CallContext).Sys.FS() + var err error + if f, ok := fsc.LookupFile(fd); !ok { + err = syscall.EBADF + } else if chmoder, ok := f.File.(chmoder); !ok { + err = syscall.EBADF // possibly a fake file + } else { + err = chmoder.Chmod(fs.FileMode(mode)) + } return jsfsInvoke(ctx, mod, callback, err) } diff --git a/internal/gojs/testdata/writefs/main.go b/internal/gojs/testdata/writefs/main.go index bbd4a68b8c..529165f6e7 100644 --- a/internal/gojs/testdata/writefs/main.go +++ b/internal/gojs/testdata/writefs/main.go @@ -3,6 +3,7 @@ package writefs import ( "errors" "fmt" + "io/fs" "log" "os" "path" @@ -73,9 +74,31 @@ func Main() { if err = f.Sync(); err != nil { log.Panicln(err) } + // Next, chmod it (tests Fchmod) + if err = f.Chmod(0o400); err != nil { + log.Panicln(err) + } + if stat, err := f.Stat(); err != nil { + log.Panicln(err) + } else if mode := stat.Mode() & fs.ModePerm; mode != 0o400 { + log.Panicln("expected mode = 0o400", mode) + } + // Finally, close it. if err = f.Close(); err != nil { log.Panicln(err) } + + // Revert to writeable + if err = syscall.Chmod(file1, 0o600); err != nil { + log.Panicln(err) + } + if stat, err := os.Stat(file1); err != nil { + log.Panicln(err) + } else if mode := stat.Mode() & fs.ModePerm; mode != 0o600 { + log.Panicln("expected mode = 0o600", mode) + } + + // Check the file was truncated. if bytes, err := os.ReadFile(file1); err != nil { log.Panicln(err) } else if string(bytes) != "wa" { diff --git a/internal/sysfs/adapter_test.go b/internal/sysfs/adapter_test.go index f13ab4001d..fcbcb16415 100644 --- a/internal/sysfs/adapter_test.go +++ b/internal/sysfs/adapter_test.go @@ -24,6 +24,13 @@ func TestAdapt_MkDir(t *testing.T) { require.Equal(t, syscall.ENOSYS, err) } +func TestAdapt_Chmod(t *testing.T) { + testFS := Adapt(os.DirFS(t.TempDir())) + + err := testFS.Chmod("chmod", fs.ModeDir) + require.Equal(t, syscall.ENOSYS, err) +} + func TestAdapt_Rename(t *testing.T) { tmpDir := t.TempDir() testFS := Adapt(os.DirFS(tmpDir)) @@ -110,6 +117,8 @@ func (dir hackFS) Open(name string) (fs.File, error) { return f, nil } else if errors.Is(err, syscall.EISDIR) { return os.OpenFile(path, os.O_RDONLY, 0) + } else if errors.Is(err, syscall.ENOENT) { + return os.OpenFile(path, os.O_RDONLY|os.O_CREATE, 0o444) } else { return nil, err } diff --git a/internal/sysfs/dirfs.go b/internal/sysfs/dirfs.go index 0a7b35b824..552588c47d 100644 --- a/internal/sysfs/dirfs.go +++ b/internal/sysfs/dirfs.go @@ -1,7 +1,6 @@ package sysfs import ( - "errors" "io/fs" "os" "syscall" @@ -51,11 +50,17 @@ func (d *dirFS) OpenFile(name string, flag int, perm fs.FileMode) (fs.File, erro } // Mkdir implements FS.Mkdir -func (d *dirFS) Mkdir(name string, perm fs.FileMode) error { - err := os.Mkdir(d.join(name), perm) - if errors.Is(err, syscall.ENOTDIR) { - return syscall.ENOENT +func (d *dirFS) Mkdir(name string, perm fs.FileMode) (err error) { + err = os.Mkdir(d.join(name), perm) + if err = UnwrapOSError(err); err == syscall.ENOTDIR { + err = syscall.ENOENT } + return +} + +// Chmod implements FS.Chmod +func (d *dirFS) Chmod(name string, perm fs.FileMode) error { + err := os.Chmod(d.join(name), perm) return UnwrapOSError(err) } diff --git a/internal/sysfs/dirfs_test.go b/internal/sysfs/dirfs_test.go index 888f240495..4ae5ec4e53 100644 --- a/internal/sysfs/dirfs_test.go +++ b/internal/sysfs/dirfs_test.go @@ -76,6 +76,45 @@ func TestDirFS_MkDir(t *testing.T) { err := testFS.Mkdir(filePath, fs.ModeDir) require.Equal(t, syscall.ENOENT, err) }) + + // Remove the path so that we can test creating it with perms. + require.NoError(t, os.Remove(realPath)) + + // Setting mode only applies to files on windows + if runtime.GOOS != "windows" { + t.Run("dir", func(t *testing.T) { + require.NoError(t, os.Mkdir(realPath, 0o444)) + defer os.RemoveAll(realPath) + testChmod(t, testFS, name) + }) + } + + t.Run("file", func(t *testing.T) { + require.NoError(t, os.WriteFile(realPath, nil, 0o444)) + defer os.RemoveAll(realPath) + testChmod(t, testFS, name) + }) +} + +func testChmod(t *testing.T, testFS FS, path string) { + // Test base case, using 0o444 not 0o400 for read-back on windows. + requireMode(t, testFS, path, 0o444) + + // Test adding write, using 0o666 not 0o600 for read-back on windows. + require.NoError(t, testFS.Chmod(path, 0o666)) + requireMode(t, testFS, path, 0o666) + + if runtime.GOOS != "windows" { + // Test clearing group and world, setting owner read+execute. + require.NoError(t, testFS.Chmod(path, 0o500)) + requireMode(t, testFS, path, 0o500) + } +} + +func requireMode(t *testing.T, testFS FS, path string, mode fs.FileMode) { + stat, err := StatPath(testFS, path) + require.NoError(t, err) + require.Equal(t, mode, stat.Mode()&fs.ModePerm) } func TestDirFS_Rename(t *testing.T) { @@ -371,7 +410,7 @@ func TestDirFS_Utimes(t *testing.T) { testUtimes(t, tmpDir, testFS) } -func TestDirFS_Open(t *testing.T) { +func TestDirFS_OpenFile(t *testing.T) { tmpDir := t.TempDir() // Create a subdirectory, so we can test reads outside the FS root. @@ -466,9 +505,9 @@ func TestDirFS_Truncate(t *testing.T) { require.NoError(t, os.Remove(realPath)) }) - t.Run("negative", func(t *testing.T) { - require.NoError(t, os.WriteFile(realPath, []byte{}, 0o600)) + require.NoError(t, os.WriteFile(realPath, []byte{}, 0o600)) + t.Run("negative", func(t *testing.T) { err := testFS.Truncate(name, -1) require.Equal(t, syscall.EINVAL, err) }) diff --git a/internal/sysfs/readfs_test.go b/internal/sysfs/readfs_test.go index 9a6a165b94..68c1dea9b5 100644 --- a/internal/sysfs/readfs_test.go +++ b/internal/sysfs/readfs_test.go @@ -41,6 +41,14 @@ func TestReadFS_MkDir(t *testing.T) { require.Equal(t, syscall.ENOSYS, err) } +func TestReadFS_Chmod(t *testing.T) { + writeable := NewDirFS(t.TempDir()) + testFS := NewReadFS(writeable) + + err := testFS.Chmod("chmod", fs.ModeDir) + require.Equal(t, syscall.ENOSYS, err) +} + func TestReadFS_Rename(t *testing.T) { tmpDir := t.TempDir() writeable := NewDirFS(tmpDir) diff --git a/internal/sysfs/sysfs.go b/internal/sysfs/sysfs.go index 554a17bcab..d0d3ed99f2 100644 --- a/internal/sysfs/sysfs.go +++ b/internal/sysfs/sysfs.go @@ -52,6 +52,12 @@ type FS interface { // fs.FS. While fs.FS is supported (Adapt), wazero cannot runtime enforce // open flags. Instead, we encourage good behavior and test our built-in // implementations. + // + // # Notes + // + // - flag are the same as OpenFile, for example, os.O_CREATE. + // - Implications of permissions when os.O_CREATE are described in Chmod + // notes. OpenFile(path string, flag int, perm fs.FileMode) (fs.File, error) // ^^ TODO: Consider syscall.Open, though this implies defining and // coercing flags and perms similar to what is done in os.OpenFile. @@ -66,10 +72,31 @@ type FS interface { // - syscall.EEXIST: `path` exists and is a directory. // - syscall.ENOTDIR: `path` exists and is a file. // + // # Notes + // + // - Implications of permissions are described in Chmod notes. Mkdir(path string, perm fs.FileMode) error // ^^ TODO: Consider syscall.Mkdir, though this implies defining and // coercing flags and perms similar to what is done in os.Mkdir. + // Chmod is similar to os.Chmod, except the path is relative to this file + // system, and syscall.Errno are returned instead of a os.PathError. + // + // # Errors + // + // The following errors are expected: + // - syscall.EINVAL: `path` is invalid. + // - syscall.ENOENT: `path` does not exist. + // + // # Notes + // + // - Windows ignores the execute bit, and any permissions come back as + // group and world. For example, chmod of 0400 reads back as 0444, and + // 0700 0666. Also, permissions on directories aren't supported at all. + Chmod(path string, perm fs.FileMode) error + // ^^ TODO: Consider syscall.Chmod, though this implies defining and + // coercing flags and perms similar to what is done in os.Chmod. + // Rename is similar to syscall.Rename, except the path is relative to this // file system. // @@ -171,7 +198,8 @@ type FS interface { // The following errors are expected: // - syscall.EINVAL: `path` is invalid or size is negative. // - syscall.ENOENT: `path` doesn't exist - Truncate(name string, size int64) error + // - syscall.EACCES: `path` doesn't have write access. + Truncate(path string, size int64) error // Utimes is similar to syscall.UtimesNano, except the path is relative to // this file system. @@ -221,6 +249,7 @@ type file interface { readFile io.Writer io.WriterAt // for pwrite + chmoder syncer truncater fder // for the number of links. @@ -228,6 +257,8 @@ type file interface { // The following interfaces are used until we finalize our own FD-scoped file. type ( + // chmoder is implemented by os.File in file_posix.go + chmoder interface{ Chmod(fs.FileMode) error } // syncer is implemented by os.File in file_posix.go syncer interface{ Sync() error } // truncater is implemented by os.File in file_posix.go diff --git a/internal/sysfs/sysfs_test.go b/internal/sysfs/sysfs_test.go index 49739f59b0..813ff57a72 100644 --- a/internal/sysfs/sysfs_test.go +++ b/internal/sysfs/sysfs_test.go @@ -45,6 +45,28 @@ func testOpen_O_RDWR(t *testing.T, tmpDir string, testFS FS) { b, err := os.ReadFile(realPath) require.NoError(t, err) require.Equal(t, fileContents, b) + + require.NoError(t, f.Close()) + + // re-create as read-only, using 0444 to allow read-back on windows. + require.NoError(t, os.Remove(realPath)) + f, err = testFS.OpenFile(file, os.O_RDONLY|os.O_CREATE, 0o444) + require.NoError(t, err) + defer f.Close() + + w, ok = f.(io.Writer) + require.True(t, ok) + + if runtime.GOOS != "windows" { + // If the read-only flag was honored, we should not be able to write! + _, err = w.Write(fileContents) + require.Equal(t, syscall.EBADF, UnwrapOSError(err)) + } + + // Verify stat on the file + stat, err := f.Stat() + require.NoError(t, err) + require.Equal(t, fs.FileMode(0o444), stat.Mode()&fs.ModePerm) } func testOpen_Read(t *testing.T, tmpDir string, testFS FS) { diff --git a/internal/sysfs/unsupported.go b/internal/sysfs/unsupported.go index e7ee48c97f..e9c73b6d4d 100644 --- a/internal/sysfs/unsupported.go +++ b/internal/sysfs/unsupported.go @@ -29,6 +29,11 @@ func (UnimplementedFS) Mkdir(path string, perm fs.FileMode) error { return syscall.ENOSYS } +// Chmod implements FS.Chmod +func (UnimplementedFS) Chmod(path string, perm fs.FileMode) error { + return syscall.ENOSYS +} + // Rename implements FS.Rename func (UnimplementedFS) Rename(from, to string) error { return syscall.ENOSYS diff --git a/internal/wasi_snapshot_preview1/errno.go b/internal/wasi_snapshot_preview1/errno.go index 838d96bec5..42e0ac96f0 100644 --- a/internal/wasi_snapshot_preview1/errno.go +++ b/internal/wasi_snapshot_preview1/errno.go @@ -268,8 +268,9 @@ var errnoToString = [...]string{ func ToErrno(err error) Errno { errno := sysfs.UnwrapOSError(err) - // The below Errno have references in existing WASI code. switch errno { + case syscall.EACCES: + return ErrnoAcces case syscall.EAGAIN: return ErrnoAgain case syscall.EBADF: diff --git a/internal/wasi_snapshot_preview1/errno_test.go b/internal/wasi_snapshot_preview1/errno_test.go index 808fc07ff6..a9e90d4712 100644 --- a/internal/wasi_snapshot_preview1/errno_test.go +++ b/internal/wasi_snapshot_preview1/errno_test.go @@ -13,6 +13,11 @@ func TestToErrno(t *testing.T) { input error expected Errno }{ + { + name: "syscall.EACCES", + input: syscall.EACCES, + expected: ErrnoAcces, + }, { name: "syscall.EAGAIN", input: syscall.EAGAIN,