diff --git a/imports/assemblyscript/assemblyscript.go b/imports/assemblyscript/assemblyscript.go index 554750b1..4e2833ec 100644 --- a/imports/assemblyscript/assemblyscript.go +++ b/imports/assemblyscript/assemblyscript.go @@ -165,8 +165,8 @@ func abortWithMessage(ctx context.Context, mod api.Module, stack []uint64) { columnNumber := uint32(stack[3]) // Don't panic if there was a problem reading the message - stderr := fsc.FdWriter(internalsys.FdStderr) - if msg, msgOk := readAssemblyScriptString(mem, message); msgOk { + stderr := internalsys.WriterForFile(fsc, internalsys.FdStderr) + if msg, msgOk := readAssemblyScriptString(mem, message); msgOk && stderr != nil { if fn, fnOk := readAssemblyScriptString(mem, fileName); fnOk { _, _ = fmt.Fprintf(stderr, "%s at %s:%d:%d\n", msg, fn, lineNumber, columnNumber) } @@ -200,8 +200,7 @@ var traceStdout = &wasm.HostFunc{ IsHostFunction: true, GoFunc: api.GoModuleFunc(func(_ context.Context, mod api.Module, stack []uint64) { fsc := mod.(*wasm.CallContext).Sys.FS() - stdout := fsc.FdWriter(internalsys.FdStdout) - traceTo(mod, stack, stdout) + traceTo(mod, stack, internalsys.WriterForFile(fsc, internalsys.FdStdout)) }), }, } @@ -209,8 +208,7 @@ var traceStdout = &wasm.HostFunc{ // traceStderr implements trace to the configured Stderr. var traceStderr = traceStdout.WithGoModuleFunc(func(_ context.Context, mod api.Module, stack []uint64) { fsc := mod.(*wasm.CallContext).Sys.FS() - stderr := fsc.FdWriter(internalsys.FdStderr) - traceTo(mod, stack, stderr) + traceTo(mod, stack, internalsys.WriterForFile(fsc, internalsys.FdStderr)) }) // traceTo implements the function "trace" in AssemblyScript. e.g. @@ -224,6 +222,9 @@ var traceStderr = traceStdout.WithGoModuleFunc(func(_ context.Context, mod api.M // // See https://github.com/AssemblyScript/assemblyscript/blob/fa14b3b03bd4607efa52aaff3132bea0c03a7989/std/assembly/wasi/index.ts#L61 func traceTo(mod api.Module, params []uint64, writer io.Writer) { + if writer == nil { + return // closed + } message := uint32(params[0]) nArgs := uint32(params[1]) arg0 := api.DecodeF64(params[2]) diff --git a/imports/wasi_snapshot_preview1/fs.go b/imports/wasi_snapshot_preview1/fs.go index 50fa306f..2bd28431 100644 --- a/imports/wasi_snapshot_preview1/fs.go +++ b/imports/wasi_snapshot_preview1/fs.go @@ -12,6 +12,7 @@ import ( "github.com/tetratelabs/wazero/api" "github.com/tetratelabs/wazero/internal/platform" "github.com/tetratelabs/wazero/internal/sys" + "github.com/tetratelabs/wazero/internal/syscallfs" . "github.com/tetratelabs/wazero/internal/wasi_snapshot_preview1" "github.com/tetratelabs/wazero/internal/wasm" ) @@ -57,8 +58,8 @@ func fdCloseFn(_ context.Context, mod api.Module, params []uint64) Errno { fsc := mod.(*wasm.CallContext).Sys.FS() fd := uint32(params[0]) - if ok := fsc.CloseFile(fd); !ok { - return ErrnoBadf + if err := fsc.CloseFile(fd); err != nil { + return ToErrno(err) } return ErrnoSuccess } @@ -121,20 +122,19 @@ func fdFdstatGetFn(_ context.Context, mod api.Module, params []uint64) Errno { return ErrnoFault } - stat, err := fsc.StatFile(fd) - if err != nil { - return ToErrno(err) - } - - filetype := getWasiFiletype(stat.Mode()) var fdflags uint16 - - // Determine if it is writeable - if w := fsc.FdWriter(fd); w != nil { + var stat fs.FileInfo + var err error + if f, ok := fsc.LookupFile(fd); !ok { + return ErrnoBadf + } else if stat, err = f.File.Stat(); err != nil { + return ToErrno(err) + } else if _, ok := f.File.(io.Writer); ok { // TODO: maybe cache flags to open instead fdflags = FD_APPEND } + filetype := getWasiFiletype(stat.Mode()) writeFdstat(buf, filetype, fdflags) return ErrnoSuccess @@ -233,7 +233,7 @@ func fdFilestatGetFunc(mod api.Module, fd, resultBuf uint32) Errno { return ErrnoFault } - stat, err := fsc.StatFile(fd) + stat, err := sys.StatFile(fsc, fd) if err != nil { return ToErrno(err) } @@ -358,14 +358,16 @@ func fdPrestatGetFn(_ context.Context, mod api.Module, params []uint64) Errno { return ErrnoBadf } - entry, ok := fsc.OpenedFile(fd) - if !ok { - return ErrnoBadf + var name string + if f, ok := fsc.LookupFile(fd); !ok { + return ErrnoBadf // closed + } else { + name = f.Name } // Upper 32-bits are zero because... // * Zero-value 8-bit tag, and 3-byte zero-value padding - prestat := uint64(len(entry.Name) << 32) + prestat := uint64(len(name) << 32) if !mod.Memory().WriteUint64Le(resultPrestat, prestat) { return ErrnoFault } @@ -417,17 +419,19 @@ func fdPrestatDirNameFn(_ context.Context, mod api.Module, params []uint64) Errn return ErrnoBadf } - f, ok := fsc.OpenedFile(fd) - if !ok { - return ErrnoBadf + var name string + if f, ok := fsc.LookupFile(fd); !ok { + return ErrnoBadf // closed + } else { + name = f.Name } // Some runtimes may have another semantics. See /RATIONALE.md - if uint32(len(f.Name)) < pathLen { + if uint32(len(name)) < pathLen { return ErrnoNametoolong } - if !mod.Memory().Write(path, []byte(f.Name)[:pathLen]) { + if !mod.Memory().Write(path, []byte(name)[:pathLen]) { return ErrnoFault } return ErrnoSuccess @@ -519,21 +523,21 @@ func fdReadOrPread(mod api.Module, params []uint64, isPread bool) Errno { resultNread = uint32(params[3]) } - r := fsc.FdReader(fd) - if r == nil { + r, ok := fsc.LookupFile(fd) + if !ok { return ErrnoBadf } - read := r.Read + read := r.File.Read if isPread { - if ra, ok := r.(io.ReaderAt); ok { + if ra, ok := r.File.(io.ReaderAt); ok { // ReadAt is the Go equivalent to pread. read = func(p []byte) (int, error) { n, err := ra.ReadAt(p, offset) offset += int64(n) return n, err } - } else if s, ok := r.(io.Seeker); ok { + } else if s, ok := r.File.(io.Seeker); ok { // Unfortunately, it is often not supported. // See /RATIONALE.md "fd_pread: io.Seeker fallback when io.ReaderAt is not supported" initialOffset, err := s.Seek(0, io.SeekCurrent) @@ -853,7 +857,7 @@ func writeDirent(buf []byte, dNext uint64, dNamlen uint32, dType bool) { // openedDir returns the directory and ErrnoSuccess if the fd points to a readable directory. func openedDir(fsc *sys.FSContext, fd uint32) (fs.ReadDirFile, *sys.ReadDir, Errno) { - if f, ok := fsc.OpenedFile(fd); !ok { + if f, ok := fsc.LookupFile(fd); !ok { return nil, nil, ErrnoBadf } else if d, ok := f.File.(fs.ReadDirFile); !ok { // fd_readdir docs don't indicate whether to return ErrnoNotdir or @@ -933,7 +937,7 @@ func fdSeekFn(_ context.Context, mod api.Module, params []uint64) Errno { var seeker io.Seeker // Check to see if the file descriptor is available - if f, ok := fsc.OpenedFile(fd); !ok { + if f, ok := fsc.LookupFile(fd); !ok { return ErrnoBadf // fs.FS doesn't declare io.Seeker, but implementations such as os.File implement it. } else if seeker, ok = f.File.(io.Seeker); !ok { @@ -1040,7 +1044,7 @@ func fdWriteFn(_ context.Context, mod api.Module, params []uint64) Errno { iovsCount := uint32(params[2]) resultNwritten := uint32(params[3]) - writer := fsc.FdWriter(fd) + writer := sys.WriterForFile(fsc, fd) if writer == nil { return ErrnoBadf } @@ -1118,10 +1122,8 @@ func pathCreateDirectoryFn(_ context.Context, mod api.Module, params []uint64) E return errno } - if fd, err := fsc.Mkdir(pathName, 0o700); err != nil { + if err := fsc.FS().Mkdir(pathName, 0o700); err != nil { return ToErrno(err) - } else { - _ = fsc.CloseFile(fd) } return ErrnoSuccess @@ -1183,20 +1185,23 @@ func pathFilestatGetFn(_ context.Context, mod api.Module, params []uint64) Errno } pathName := string(b) - // Prepend the path if necessary. - if dir, ok := fsc.OpenedFile(dirfd); !ok { - return ErrnoBadf - } else if _, ok := dir.File.(fs.ReadDirFile); !ok { - return ErrnoNotdir // TODO: cache filetype instead of poking. - } else { + // os.File implements ReadDirFile, so we have to check with stat. + stat, err := sys.StatFile(fsc, dirfd) + if err != nil { + return ToErrno(err) + } + + if !stat.IsDir() { + return ErrnoNotdir + } else { // prepend the name // TODO: consolidate "at" logic with path_open as same issues occur. - pathName = path.Join(dir.Name, pathName) + pathName = path.Join(stat.Name(), pathName) } // Stat the file without allocating a file descriptor - stat, errnoResult := statFile(fsc, pathName) - if errnoResult != ErrnoSuccess { - return errnoResult + stat, err = syscallfs.StatPath(fsc.FS(), pathName) + if err != nil { + return ToErrno(err) } // Write the stat result to memory @@ -1315,22 +1320,20 @@ func pathOpenFn(_ context.Context, mod api.Module, params []uint64) Errno { fileOpenFlags, isDir := openFlags(oflags, fdflags) - var newFD uint32 - var err error if isDir && oflags&O_CREAT != 0 { return ErrnoInval // use pathCreateDirectory! - } else { - newFD, err = fsc.OpenFile(pathName, fileOpenFlags, 0o600) } + newFD, err := fsc.OpenFile(pathName, fileOpenFlags, 0o600) if err != nil { return ToErrno(err) } // Check any flags that require the file to evaluate. if isDir { - if errno := failIfNotDirectory(fsc, newFD); errno != ErrnoSuccess { - return errno + if stat, err := sys.StatFile(fsc, newFD); err != nil || !stat.IsDir() { + _ = fsc.CloseFile(newFD) + return ErrnoNotdir } } @@ -1358,8 +1361,8 @@ func atPath(fsc *sys.FSContext, mem api.Memory, dirFd, path, pathLen uint32) (st // "/tmp/foo/bar" not "/bar". } - if _, ok := fsc.OpenedFile(dirFd); !ok { - return "", ErrnoBadf + if _, ok := fsc.LookupFile(dirFd); !ok { + return "", ErrnoBadf // closed } b, ok := mem.Read(path, pathLen) @@ -1392,17 +1395,6 @@ func openFlags(oflags, fdflags uint16) (openFlags int, isDir bool) { return } -func failIfNotDirectory(fsc *sys.FSContext, fd uint32) Errno { - // Lookup the previous file - if f, ok := fsc.OpenedFile(fd); !ok { - return ErrnoBadf - } else if _, ok := f.File.(fs.ReadDirFile); !ok { - _ = fsc.CloseFile(fd) - return ErrnoNotdir - } - return ErrnoSuccess -} - // pathReadlink is the WASI function named PathReadlinkName that reads the // contents of a symbolic link. // @@ -1453,7 +1445,7 @@ func pathRemoveDirectoryFn(_ context.Context, mod api.Module, params []uint64) E return errno } - if err := fsc.Rmdir(pathName); err != nil { + if err := fsc.FS().Rmdir(pathName); err != nil { return ToErrno(err) } @@ -1512,7 +1504,7 @@ func pathRenameFn(_ context.Context, mod api.Module, params []uint64) Errno { return errno } - if err := fsc.Rename(oldPathName, newPathName); err != nil { + if err := fsc.FS().Rename(oldPathName, newPathName); err != nil { return ToErrno(err) } @@ -1568,20 +1560,9 @@ func pathUnlinkFileFn(_ context.Context, mod api.Module, params []uint64) Errno return errno } - if err := fsc.Unlink(pathName); err != nil { + if err := fsc.FS().Unlink(pathName); err != nil { return ToErrno(err) } return ErrnoSuccess } - -// statFile attempts to stat the file at the given path. Errors coerce to WASI -// Errno. -func statFile(fsc *sys.FSContext, name string) (stat fs.FileInfo, errno Errno) { - var err error - stat, err = fsc.StatPath(name) - if err != nil { - errno = ToErrno(err) - } - return -} diff --git a/imports/wasi_snapshot_preview1/fs_test.go b/imports/wasi_snapshot_preview1/fs_test.go index 2433e72f..68288c4b 100644 --- a/imports/wasi_snapshot_preview1/fs_test.go +++ b/imports/wasi_snapshot_preview1/fs_test.go @@ -67,11 +67,11 @@ func Test_fdClose(t *testing.T) { `, "\n"+log.String()) // Verify fdToClose is closed and removed from the opened FDs. - _, ok := fsc.OpenedFile(fdToClose) + _, ok := fsc.LookupFile(fdToClose) require.False(t, ok) // Verify fdToKeep is not closed - _, ok = fsc.OpenedFile(fdToKeep) + _, ok = fsc.LookupFile(fdToKeep) require.True(t, ok) log.Reset() @@ -104,10 +104,10 @@ func Test_fdFdstatGet(t *testing.T) { // open both paths without using WASI fsc := mod.(*wasm.CallContext).Sys.FS() - fileFd, err := fsc.OpenFile(file, os.O_RDONLY, 0) + fileFD, err := fsc.OpenFile(file, os.O_RDONLY, 0) require.NoError(t, err) - dirFd, err := fsc.OpenFile(dir, os.O_RDONLY, 0) + dirFD, err := fsc.OpenFile(dir, os.O_RDONLY, 0) require.NoError(t, err) tests := []struct { @@ -175,7 +175,7 @@ func Test_fdFdstatGet(t *testing.T) { }, { name: "file", - fd: fileFd, + fd: fileFD, expectedMemory: []byte{ 4, 0, // fs_filetype 0, 0, 0, 0, 0, 0, // fs_flags @@ -189,7 +189,7 @@ func Test_fdFdstatGet(t *testing.T) { }, { name: "dir", - fd: dirFd, + fd: dirFD, expectedMemory: []byte{ 3, 0, // fs_filetype 0, 0, 0, 0, 0, 0, // fs_flags @@ -212,7 +212,7 @@ func Test_fdFdstatGet(t *testing.T) { }, { name: "resultFdstat exceeds the maximum valid address by 1", - fd: dirFd, + fd: dirFD, resultFdstat: memorySize - 24 + 1, expectedErrno: ErrnoFault, expectedLog: ` @@ -273,10 +273,10 @@ func Test_fdFilestatGet(t *testing.T) { // open both paths without using WASI fsc := mod.(*wasm.CallContext).Sys.FS() - fileFd, err := fsc.OpenFile(file, os.O_RDONLY, 0) + fileFD, err := fsc.OpenFile(file, os.O_RDONLY, 0) require.NoError(t, err) - dirFd, err := fsc.OpenFile(dir, os.O_RDONLY, 0) + dirFD, err := fsc.OpenFile(dir, os.O_RDONLY, 0) require.NoError(t, err) tests := []struct { @@ -363,7 +363,7 @@ func Test_fdFilestatGet(t *testing.T) { }, { name: "file", - fd: fileFd, + fd: fileFD, expectedMemory: []byte{ 0, 0, 0, 0, 0, 0, 0, 0, // dev 0, 0, 0, 0, 0, 0, 0, 0, // ino @@ -381,7 +381,7 @@ func Test_fdFilestatGet(t *testing.T) { }, { name: "dir", - fd: dirFd, + fd: dirFD, expectedMemory: []byte{ 0, 0, 0, 0, 0, 0, 0, 0, // dev 0, 0, 0, 0, 0, 0, 0, 0, // ino @@ -408,7 +408,7 @@ func Test_fdFilestatGet(t *testing.T) { }, { name: "resultFilestat exceeds the maximum valid address by 1", - fd: dirFd, + fd: dirFD, resultFilestat: memorySize - 64 + 1, expectedErrno: ErrnoFault, expectedLog: ` @@ -1059,7 +1059,7 @@ func Test_fdRead_Errors(t *testing.T) { var ( fdReadDirFs = fstest.MapFS{ - "notdir": {}, + "file": {}, "emptydir": {Mode: fs.ModeDir}, "dir": {Mode: fs.ModeDir}, "dir/-": {}, // len = 24+1 = 25 @@ -1341,7 +1341,7 @@ func Test_fdReaddir(t *testing.T) { defer log.Reset() // Assign the state we are testing - file, ok := fsc.OpenedFile(fd) + file, ok := fsc.LookupFile(fd) require.True(t, ok) dir := tc.dir() defer dir.File.Close() @@ -1383,10 +1383,10 @@ func Test_fdReaddir_Errors(t *testing.T) { fsc := mod.(*wasm.CallContext).Sys.FS() - dirFD, err := fsc.OpenFile("dir", os.O_RDONLY, 0) + fileFD, err := fsc.OpenFile("file", os.O_RDONLY, 0) require.NoError(t, err) - fileFD, err := fsc.OpenFile("notdir", os.O_RDONLY, 0) + dirFD, err := fsc.OpenFile("dir", os.O_RDONLY, 0) require.NoError(t, err) tests := []struct { @@ -1405,7 +1405,7 @@ func Test_fdReaddir_Errors(t *testing.T) { bufLen: 1000, expectedErrno: ErrnoFault, expectedLog: ` -==> wasi_snapshot_preview1.fd_readdir(fd=4,buf=65536,buf_len=1000,cookie=0,result.bufused=0) +==> wasi_snapshot_preview1.fd_readdir(fd=5,buf=65536,buf_len=1000,cookie=0,result.bufused=0) <== errno=EFAULT `, }, @@ -1427,19 +1427,8 @@ func Test_fdReaddir_Errors(t *testing.T) { resultBufused: 1000, // arbitrary expectedErrno: ErrnoBadf, expectedLog: ` -==> wasi_snapshot_preview1.fd_readdir(fd=5,buf=0,buf_len=24,cookie=0,result.bufused=1000) +==> wasi_snapshot_preview1.fd_readdir(fd=4,buf=0,buf_len=24,cookie=0,result.bufused=1000) <== errno=EBADF -`, - }, - { - name: "out-of-memory reading buf", - fd: dirFD, - buf: memLen, - bufLen: 1000, - expectedErrno: ErrnoFault, - expectedLog: ` -==> wasi_snapshot_preview1.fd_readdir(fd=4,buf=65536,buf_len=1000,cookie=0,result.bufused=0) -<== errno=EFAULT `, }, { @@ -1449,7 +1438,7 @@ func Test_fdReaddir_Errors(t *testing.T) { bufLen: 1000, expectedErrno: ErrnoFault, expectedLog: ` -==> wasi_snapshot_preview1.fd_readdir(fd=4,buf=65535,buf_len=1000,cookie=0,result.bufused=0) +==> wasi_snapshot_preview1.fd_readdir(fd=5,buf=65535,buf_len=1000,cookie=0,result.bufused=0) <== errno=EFAULT `, }, @@ -1460,7 +1449,7 @@ func Test_fdReaddir_Errors(t *testing.T) { resultBufused: 1000, expectedErrno: ErrnoInval, expectedLog: ` -==> wasi_snapshot_preview1.fd_readdir(fd=4,buf=0,buf_len=1,cookie=0,result.bufused=1000) +==> wasi_snapshot_preview1.fd_readdir(fd=5,buf=0,buf_len=1,cookie=0,result.bufused=1000) <== errno=EINVAL `, }, @@ -1472,19 +1461,7 @@ func Test_fdReaddir_Errors(t *testing.T) { resultBufused: 2000, expectedErrno: ErrnoInval, expectedLog: ` -==> wasi_snapshot_preview1.fd_readdir(fd=4,buf=0,buf_len=1000,cookie=1,result.bufused=2000) -<== errno=EINVAL -`, - }, - { - name: "cookie invalid when no prior state", - fd: dirFD, - buf: 0, bufLen: 1000, - cookie: 1, - resultBufused: 2000, - expectedErrno: ErrnoInval, - expectedLog: ` -==> wasi_snapshot_preview1.fd_readdir(fd=4,buf=0,buf_len=1000,cookie=1,result.bufused=2000) +==> wasi_snapshot_preview1.fd_readdir(fd=5,buf=0,buf_len=1000,cookie=1,result.bufused=2000) <== errno=EINVAL `, }, @@ -1497,7 +1474,7 @@ func Test_fdReaddir_Errors(t *testing.T) { resultBufused: 2000, expectedErrno: ErrnoInval, expectedLog: ` -==> wasi_snapshot_preview1.fd_readdir(fd=4,buf=0,buf_len=1000,cookie=-1,result.bufused=2000) +==> wasi_snapshot_preview1.fd_readdir(fd=5,buf=0,buf_len=1000,cookie=-1,result.bufused=2000) <== errno=EINVAL `, }, @@ -1509,7 +1486,7 @@ func Test_fdReaddir_Errors(t *testing.T) { defer log.Reset() // Reset the directory so that tests don't taint each other. - if file, ok := fsc.OpenedFile(tc.fd); ok && tc.fd == dirFD { + if file, ok := fsc.LookupFile(tc.fd); ok && tc.fd == dirFD { dir, err := fdReadDirFs.Open("dir") require.NoError(t, err) defer dir.Close() @@ -1604,7 +1581,7 @@ func Test_fdSeek(t *testing.T) { // Since we initialized this file, we know it is a seeker (because it is a MapFile) fsc := mod.(*wasm.CallContext).Sys.FS() - f, ok := fsc.OpenedFile(fd) + f, ok := fsc.LookupFile(fd) require.True(t, ok) seeker := f.File.(io.Seeker) @@ -1961,17 +1938,6 @@ func Test_pathCreateDirectory_Errors(t *testing.T) { expectedLog: ` ==> wasi_snapshot_preview1.path_create_directory(fd=3,path=OOM(65536,1)) <== errno=EFAULT -`, - }, - { - name: "path invalid", - fd: sys.FdRoot, - pathName: "../foo", - pathLen: 6, - expectedErrno: ErrnoInval, - expectedLog: ` -==> wasi_snapshot_preview1.path_create_directory(fd=3,path=../foo) -<== errno=EINVAL `, }, { @@ -2043,10 +2009,10 @@ func Test_pathFilestatGet(t *testing.T) { // open both paths without using WASI fsc := mod.(*wasm.CallContext).Sys.FS() - fileFd, err := fsc.OpenFile(file, os.O_RDONLY, 0) + fileFD, err := fsc.OpenFile(file, os.O_RDONLY, 0) require.NoError(t, err) - dirFd, err := fsc.OpenFile(dir, os.O_RDONLY, 0) + dirFD, err := fsc.OpenFile(dir, os.O_RDONLY, 0) require.NoError(t, err) tests := []struct { @@ -2080,7 +2046,7 @@ func Test_pathFilestatGet(t *testing.T) { }, { name: "file under dir", - fd: dirFd, // root + fd: dirFD, // root memory: initialMemoryFile, pathLen: 1, resultFilestat: 2, @@ -2133,7 +2099,7 @@ func Test_pathFilestatGet(t *testing.T) { }, { name: "bad FD - not dir", - fd: fileFd, + fd: fileFD, memory: initialMemoryFile, pathLen: 1, resultFilestat: 2, @@ -2157,7 +2123,7 @@ func Test_pathFilestatGet(t *testing.T) { }, { name: "path under dir doesn't exist", - fd: dirFd, + fd: dirFD, memory: initialMemoryNotExists, pathLen: 1, resultFilestat: 2, @@ -2169,7 +2135,7 @@ func Test_pathFilestatGet(t *testing.T) { }, { name: "path invalid", - fd: dirFd, + fd: dirFD, memory: []byte("?../foo"), pathLen: 6, resultFilestat: 7, @@ -2321,9 +2287,9 @@ func Test_pathOpen(t *testing.T) { fdflags: FD_APPEND, expected: func(t *testing.T, fsc *sys.FSContext) { contents := []byte("hello") - _, err := fsc.FdWriter(expectedOpenedFd).Write(contents) + _, err := sys.WriterForFile(fsc, expectedOpenedFd).Write(contents) require.NoError(t, err) - require.True(t, fsc.CloseFile(expectedOpenedFd)) + require.NoError(t, fsc.CloseFile(expectedOpenedFd)) // verify the contents were appended b := readFile(t, dir, appendName) @@ -2353,9 +2319,9 @@ func Test_pathOpen(t *testing.T) { expected: func(t *testing.T, fsc *sys.FSContext) { // expect to create a new file contents := []byte("hello") - _, err := fsc.FdWriter(expectedOpenedFd).Write(contents) + _, err := sys.WriterForFile(fsc, expectedOpenedFd).Write(contents) require.NoError(t, err) - require.True(t, fsc.CloseFile(expectedOpenedFd)) + require.NoError(t, fsc.CloseFile(expectedOpenedFd)) // verify the contents were written b := readFile(t, dir, "creat") @@ -2385,9 +2351,9 @@ func Test_pathOpen(t *testing.T) { expected: func(t *testing.T, fsc *sys.FSContext) { // expect to create a new file contents := []byte("hello") - _, err := fsc.FdWriter(expectedOpenedFd).Write(contents) + _, err := sys.WriterForFile(fsc, expectedOpenedFd).Write(contents) require.NoError(t, err) - require.True(t, fsc.CloseFile(expectedOpenedFd)) + require.NoError(t, fsc.CloseFile(expectedOpenedFd)) // verify the contents were written b := readFile(t, dir, path.Join(dirName, "O_CREAT-O_TRUNC")) @@ -2404,7 +2370,7 @@ func Test_pathOpen(t *testing.T) { oflags: O_DIRECTORY, path: func(*testing.T) string { return dirName }, expected: func(t *testing.T, fsc *sys.FSContext) { - stat, err := fsc.StatFile(expectedOpenedFd) + stat, err := sys.StatFile(fsc, expectedOpenedFd) require.NoError(t, err) require.True(t, stat.IsDir()) }, @@ -2419,7 +2385,7 @@ func Test_pathOpen(t *testing.T) { path: func(*testing.T) string { return dirName }, oflags: O_DIRECTORY, expected: func(t *testing.T, fsc *sys.FSContext) { - stat, err := fsc.StatFile(expectedOpenedFd) + stat, err := sys.StatFile(fsc, expectedOpenedFd) require.NoError(t, err) require.True(t, stat.IsDir()) }, @@ -2446,9 +2412,9 @@ func Test_pathOpen(t *testing.T) { oflags: O_TRUNC, expected: func(t *testing.T, fsc *sys.FSContext) { contents := []byte("hello") - _, err := fsc.FdWriter(expectedOpenedFd).Write(contents) + _, err := sys.WriterForFile(fsc, expectedOpenedFd).Write(contents) require.NoError(t, err) - require.True(t, fsc.CloseFile(expectedOpenedFd)) + require.NoError(t, fsc.CloseFile(expectedOpenedFd)) // verify the contents were truncated b := readFile(t, dir, "trunc") @@ -2499,12 +2465,12 @@ func Test_pathOpen(t *testing.T) { func requireContents(t *testing.T, fsc *sys.FSContext, expectedOpenedFd uint32, fileName string, fileContents []byte) { // verify the file was actually opened - f, ok := fsc.OpenedFile(expectedOpenedFd) + f, ok := fsc.LookupFile(expectedOpenedFd) require.True(t, ok) require.Equal(t, fileName, f.Name) // verify the contents are readable - b, err := io.ReadAll(fsc.FdReader(expectedOpenedFd)) + b, err := io.ReadAll(f.File) require.NoError(t, err) require.Equal(t, fileContents, b) } @@ -2528,7 +2494,7 @@ func writeFile(t *testing.T, tmpDir, file string, contents []byte) { func Test_pathOpen_Errors(t *testing.T) { validFD := uint32(3) // arbitrary valid fd after 0, 1, and 2, that are stdin/out/err dirName := "wazero" - fileName := "notdir" // name length as wazero + fileName := "file" // name length as wazero testFS := fstest.MapFS{ dirName: &fstest.MapFile{Mode: os.ModeDir}, fileName: &fstest.MapFile{}, @@ -2563,18 +2529,6 @@ func Test_pathOpen_Errors(t *testing.T) { expectedLog: ` ==> wasi_snapshot_preview1.path_open(fd=3,dirflags=,path=OOM(65536,6),oflags=,fs_rights_base=,fs_rights_inheriting=,fdflags=) <== (opened_fd=,errno=EFAULT) -`, - }, - { - name: "path invalid", - fd: validFD, - pathName: "../foo", - pathLen: 6, - // fstest.MapFS returns file not found instead of invalid on invalid path - expectedErrno: ErrnoNoent, - expectedLog: ` -==> wasi_snapshot_preview1.path_open(fd=3,dirflags=,path=../foo,oflags=,fs_rights_base=,fs_rights_inheriting=,fdflags=) -<== (opened_fd=,errno=ENOENT) `, }, { @@ -2619,10 +2573,10 @@ func Test_pathOpen_Errors(t *testing.T) { fd: validFD, pathName: fileName, path: validPath, - pathLen: validPathLen, + pathLen: uint32(len(fileName)), expectedErrno: ErrnoNotdir, expectedLog: ` -==> wasi_snapshot_preview1.path_open(fd=3,dirflags=,path=notdir,oflags=DIRECTORY,fs_rights_base=,fs_rights_inheriting=,fdflags=) +==> wasi_snapshot_preview1.path_open(fd=3,dirflags=,path=file,oflags=DIRECTORY,fs_rights_base=,fs_rights_inheriting=,fdflags=) <== (opened_fd=,errno=ENOTDIR) `, }, @@ -2632,10 +2586,10 @@ func Test_pathOpen_Errors(t *testing.T) { fd: validFD, pathName: fileName, path: validPath, - pathLen: validPathLen, + pathLen: uint32(len(fileName)), expectedErrno: ErrnoInval, expectedLog: ` -==> wasi_snapshot_preview1.path_open(fd=3,dirflags=,path=notdir,oflags=CREAT|DIRECTORY,fs_rights_base=,fs_rights_inheriting=,fdflags=) +==> wasi_snapshot_preview1.path_open(fd=3,dirflags=,path=file,oflags=CREAT|DIRECTORY,fs_rights_base=,fs_rights_inheriting=,fdflags=) <== (opened_fd=,errno=EINVAL) `, }, @@ -2741,17 +2695,6 @@ func Test_pathRemoveDirectory_Errors(t *testing.T) { expectedLog: ` ==> wasi_snapshot_preview1.path_remove_directory(fd=3,path=OOM(65536,1)) <== errno=EFAULT -`, - }, - { - name: "path invalid", - fd: sys.FdRoot, - pathName: "../foo", - pathLen: 6, - expectedErrno: ErrnoInval, - expectedLog: ` -==> wasi_snapshot_preview1.path_remove_directory(fd=3,path=../foo) -<== errno=EINVAL `, }, { @@ -2945,32 +2888,6 @@ func Test_pathRename_Errors(t *testing.T) { expectedLog: ` ==> wasi_snapshot_preview1.path_rename(fd=3,old_path=a,new_fd=3,new_path=OOM(65536,1)) <== errno=EFAULT -`, - }, - { - name: "old path invalid", - oldFd: sys.FdRoot, - newFd: sys.FdRoot, - oldPathName: "../foo", - oldPathLen: 6, - expectedErrno: ErrnoInval, - expectedLog: ` -==> wasi_snapshot_preview1.path_rename(fd=3,old_path=../foo,new_fd=3,new_path=) -<== errno=EINVAL -`, - }, - { - name: "new path invalid", - oldFd: sys.FdRoot, - newFd: sys.FdRoot, - oldPathName: file, - oldPathLen: uint32(len(file)), - newPathName: "../foo", - newPathLen: 6, - expectedErrno: ErrnoInval, - expectedLog: ` -==> wasi_snapshot_preview1.path_rename(fd=3,old_path=../f,new_fd=3,new_path=../foo) -<== errno=EINVAL `, }, { @@ -3120,17 +3037,6 @@ func Test_pathUnlinkFile_Errors(t *testing.T) { expectedLog: ` ==> wasi_snapshot_preview1.path_unlink_file(fd=3,path=OOM(65536,1)) <== errno=EFAULT -`, - }, - { - name: "path invalid", - fd: sys.FdRoot, - pathName: "../foo", - pathLen: 6, - expectedErrno: ErrnoInval, - expectedLog: ` -==> wasi_snapshot_preview1.path_unlink_file(fd=3,path=../foo) -<== errno=EINVAL `, }, { @@ -3191,6 +3097,7 @@ func requireOpenFile(t *testing.T, pathName string, data []byte) (api.Module, ui testFS := fstest.MapFS{pathName[1:]: mapFile} // strip the leading slash mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(testFS)) fsc := mod.(*wasm.CallContext).Sys.FS() + fd, err := fsc.OpenFile(pathName, os.O_RDONLY, 0) require.NoError(t, err) return mod, fd, log, r @@ -3198,29 +3105,17 @@ func requireOpenFile(t *testing.T, pathName string, data []byte) (api.Module, ui // requireOpenWritableFile is temporary until we add the ability to open files for writing. func requireOpenWritableFile(t *testing.T, tmpDir string, pathName string) (api.Module, uint32, *bytes.Buffer, api.Closer) { - writeable, testFS := createWriteableFile(t, tmpDir, pathName, []byte{}) - mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(testFS)) - fsc := mod.(*wasm.CallContext).Sys.FS() - fd, err := fsc.OpenFile(pathName, os.O_RDWR, 0) + absolutePath := path.Join(tmpDir, pathName) + require.NoError(t, os.WriteFile(absolutePath, []byte{}, 0o600)) + + writeFS, err := syscallfs.NewDirFS(tmpDir) require.NoError(t, err) - // Swap the read-only file with a writeable one until #390 - f, ok := fsc.OpenedFile(fd) - require.True(t, ok) - f.File.Close() - f.File = writeable + mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(writeFS)) + fsc := mod.(*wasm.CallContext).Sys.FS() + + fd, err := fsc.OpenFile(pathName, os.O_RDWR, 0o600) + require.NoError(t, err) return mod, fd, log, r } - -// createWriteableFile uses real files when io.Writer tests are needed. -func createWriteableFile(t *testing.T, tmpDir string, pathName string, data []byte) (fs.File, fs.FS) { - require.NotNil(t, data) - absolutePath := path.Join(tmpDir, pathName) - require.NoError(t, os.WriteFile(absolutePath, data, 0o600)) - - // open the file for writing in a custom way until #390 - f, err := os.OpenFile(absolutePath, os.O_RDWR, 0o600) - require.NoError(t, err) - return f, os.DirFS(tmpDir) -} diff --git a/imports/wasi_snapshot_preview1/poll.go b/imports/wasi_snapshot_preview1/poll.go index 367c517d..e3abb924 100644 --- a/imports/wasi_snapshot_preview1/poll.go +++ b/imports/wasi_snapshot_preview1/poll.go @@ -4,6 +4,7 @@ import ( "context" "github.com/tetratelabs/wazero/api" + internalsys "github.com/tetratelabs/wazero/internal/sys" . "github.com/tetratelabs/wazero/internal/wasi_snapshot_preview1" "github.com/tetratelabs/wazero/internal/wasm" ) @@ -132,9 +133,11 @@ func processFDEvent(mod api.Module, eventType byte, inBuf []byte) Errno { // Choose the best error, which falls back to unsupported, until we support // files. errno := ErrnoNotsup - if eventType == EventTypeFdRead && fsc.FdReader(fd) == nil { - errno = ErrnoBadf - } else if eventType == EventTypeFdWrite && fsc.FdWriter(fd) == nil { + if eventType == EventTypeFdRead { + if _, ok := fsc.LookupFile(fd); !ok { + errno = ErrnoBadf + } + } else if eventType == EventTypeFdWrite && internalsys.WriterForFile(fsc, fd) == nil { errno = ErrnoBadf } diff --git a/imports/wasi_snapshot_preview1/wasi_bench_test.go b/imports/wasi_snapshot_preview1/wasi_bench_test.go index bfebf86d..2de336bb 100644 --- a/imports/wasi_snapshot_preview1/wasi_bench_test.go +++ b/imports/wasi_snapshot_preview1/wasi_bench_test.go @@ -178,11 +178,11 @@ func Benchmark_fdReaddir(b *testing.B) { if err != nil { b.Fatal(err) } - f, ok := fsc.OpenedFile(fd) + f, ok := fsc.LookupFile(fd) if !ok { b.Fatal("couldn't open fd ", fd) } - defer fsc.CloseFile(fd) + defer fsc.CloseFile(fd) //nolint b.ResetTimer() b.ReportAllocs() @@ -284,7 +284,7 @@ func Benchmark_pathFilestat(b *testing.B) { if err != nil { b.Fatal(err) } - defer fsc.CloseFile(fd) + defer fsc.CloseFile(fd) //nolint } fn := mod.ExportedFunction(PathFilestatGetName) diff --git a/internal/gojs/compiler_test.go b/internal/gojs/compiler_test.go index 94827b22..cf1a64d9 100644 --- a/internal/gojs/compiler_test.go +++ b/internal/gojs/compiler_test.go @@ -22,6 +22,7 @@ import ( gojs "github.com/tetratelabs/wazero/imports/go" internalgojs "github.com/tetratelabs/wazero/internal/gojs" "github.com/tetratelabs/wazero/internal/gojs/run" + "github.com/tetratelabs/wazero/internal/syscallfs" ) func compileAndRun(ctx context.Context, arg string, config wazero.ModuleConfig) (stdout, stderr string, err error) { @@ -65,12 +66,12 @@ var testBin []byte // testCtx is configured in TestMain to re-use wazero's compilation cache. var ( testCtx context.Context - testFS = fstest.MapFS{ + testFS = syscallfs.Adapt(fstest.MapFS{ "empty.txt": {}, "test.txt": {Data: []byte("animals\n"), Mode: 0o644}, "sub": {Mode: fs.ModeDir | 0o755}, "sub/test.txt": {Data: []byte("greet sub dir\n"), Mode: 0o444}, - } + }) rt wazero.Runtime ) diff --git a/internal/gojs/fs.go b/internal/gojs/fs.go index 795e199a..8c9bfb25 100644 --- a/internal/gojs/fs.go +++ b/internal/gojs/fs.go @@ -13,6 +13,7 @@ import ( "github.com/tetratelabs/wazero/internal/gojs/goos" "github.com/tetratelabs/wazero/internal/platform" internalsys "github.com/tetratelabs/wazero/internal/sys" + "github.com/tetratelabs/wazero/internal/syscallfs" "github.com/tetratelabs/wazero/internal/wasm" ) @@ -93,6 +94,7 @@ func (jsfsOpen) invoke(ctx context.Context, mod api.Module, args ...interface{}) callback := args[3].(funcWrapper) fsc := mod.(*wasm.CallContext).Sys.FS() + fd, err := fsc.OpenFile(path, int(flags), fs.FileMode(perm)) return callback.invoke(ctx, mod, goos.RefJsfs, err, fd) // note: error first @@ -112,13 +114,13 @@ func (jsfsStat) invoke(ctx context.Context, mod api.Module, args ...interface{}) } // syscallStat is like syscall.Stat -func syscallStat(mod api.Module, name string) (*jsSt, error) { +func syscallStat(mod api.Module, path string) (*jsSt, error) { fsc := mod.(*wasm.CallContext).Sys.FS() - if fd, err := fsc.OpenFile(name, os.O_RDONLY, 0); err != nil { + + if stat, err := syscallfs.StatPath(fsc.FS(), path); err != nil { return nil, err } else { - defer fsc.CloseFile(fd) - return syscallFstat(fsc, fd) + return newJsSt(stat), nil } } @@ -168,21 +170,23 @@ const ( // syscallFstat is like syscall.Fstat func syscallFstat(fsc *internalsys.FSContext, fd uint32) (*jsSt, error) { - if f, ok := fsc.OpenedFile(fd); !ok { - return nil, syscall.EBADF - } else if stat, err := f.File.Stat(); err != nil { + stat, err := internalsys.StatFile(fsc, fd) + if err != nil { return nil, err - } else { - ret := &jsSt{} - ret.isDir = stat.IsDir() - ret.mode = getJsMode(stat.Mode()) - ret.size = stat.Size() - atimeNsec, mtimeNsec, ctimeNsec := platform.StatTimes(stat) - ret.atimeMs = atimeNsec / 1e6 - ret.mtimeMs = mtimeNsec / 1e6 - ret.ctimeMs = ctimeNsec / 1e6 - return ret, nil } + return newJsSt(stat), nil +} + +func newJsSt(stat fs.FileInfo) *jsSt { + ret := &jsSt{} + ret.isDir = stat.IsDir() + ret.mode = getJsMode(stat.Mode()) + ret.size = stat.Size() + atimeNsec, mtimeNsec, ctimeNsec := platform.StatTimes(stat) + ret.atimeMs = atimeNsec / 1e6 + ret.mtimeMs = mtimeNsec / 1e6 + ret.ctimeMs = ctimeNsec / 1e6 + return ret } // getJsMode is required because the mode property read in `GOOS=js` is @@ -230,10 +234,7 @@ func (jsfsClose) invoke(ctx context.Context, mod api.Module, args ...interface{} fd := toUint32(args[0]) callback := args[1].(funcWrapper) - var err error - if ok := fsc.CloseFile(fd); !ok { - err = syscall.EBADF // already closed - } + err := fsc.CloseFile(fd) return jsfsInvoke(ctx, mod, callback, err) } @@ -263,13 +264,13 @@ func (jsfsRead) invoke(ctx context.Context, mod api.Module, args ...interface{}) func syscallRead(mod api.Module, fd uint32, offset interface{}, p []byte) (n uint32, err error) { fsc := mod.(*wasm.CallContext).Sys.FS() - r := fsc.FdReader(fd) - if r == nil { + f, ok := fsc.LookupFile(fd) + if !ok { err = syscall.EBADF } if offset != nil { - if s, ok := r.(io.Seeker); ok { + if s, ok := f.File.(io.Seeker); ok { if _, err := s.Seek(toInt64(offset), io.SeekStart); err != nil { return 0, err } @@ -278,7 +279,7 @@ func syscallRead(mod api.Module, fd uint32, offset interface{}, p []byte) (n uin } } - if nRead, e := r.Read(p); e == nil || e == io.EOF { + if nRead, e := f.File.Read(p); e == nil || e == io.EOF { // fs_js.go cannot parse io.EOF so coerce it to nil. // See https://github.com/golang/go/issues/43913 n = uint32(nRead) @@ -317,7 +318,7 @@ func (jsfsWrite) invoke(ctx context.Context, mod api.Module, args ...interface{} func syscallWrite(mod api.Module, fd uint32, offset interface{}, p []byte) (n uint32, err error) { fsc := mod.(*wasm.CallContext).Sys.FS() - if writer := fsc.FdWriter(fd); writer == nil { + if writer := internalsys.WriterForFile(fsc, fd); writer == nil { err = syscall.EBADF } else if nWritten, e := writer.Write(p); e == nil || e == io.EOF { // fs_js.go cannot parse io.EOF so coerce it to nil. @@ -346,15 +347,14 @@ func (jsfsReaddir) invoke(ctx context.Context, mod api.Module, args ...interface func syscallReaddir(_ context.Context, mod api.Module, name string) (*objectArray, error) { fsc := mod.(*wasm.CallContext).Sys.FS() - fd, err := fsc.OpenFile(name, os.O_RDONLY, 0) + // don't allocate a file descriptor + f, err := fsc.FS().OpenFile(name, os.O_RDONLY, 0) if err != nil { return nil, err } - defer fsc.CloseFile(fd) + defer f.Close() //nolint - if f, ok := fsc.OpenedFile(fd); !ok { - return nil, syscall.EBADF - } else if d, ok := f.File.(fs.ReadDirFile); !ok { + if d, ok := f.(fs.ReadDirFile); !ok { return nil, syscall.ENOTDIR } else if l, err := d.ReadDir(-1); err != nil { return nil, err @@ -399,20 +399,11 @@ func (processCwd) invoke(ctx context.Context, _ api.Module, _ ...interface{}) (i type processChdir struct{} func (processChdir) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) { - fsc := mod.(*wasm.CallContext).Sys.FS() - path := args[0].(string) - // TODO: refactor so that sys has path-based ops, also needed in WASI. - if fd, err := fsc.OpenFile(path, os.O_RDONLY, 0); err != nil { - return nil, syscall.ENOENT - } else if f, ok := fsc.OpenedFile(fd); !ok { - return nil, syscall.ENOENT - } else if s, err := f.File.Stat(); err != nil { - fsc.CloseFile(fd) - return nil, syscall.ENOENT - } else if !s.IsDir() { - fsc.CloseFile(fd) + if s, err := syscallStat(mod, path); err != nil { + return nil, mapJSError(err) + } else if !s.isDir { return nil, syscall.ENOTDIR } else { getState(ctx).cwd = path @@ -431,7 +422,12 @@ func (jsfsMkdir) invoke(ctx context.Context, mod api.Module, args ...interface{} callback := args[2].(funcWrapper) fsc := mod.(*wasm.CallContext).Sys.FS() - fd, err := fsc.Mkdir(path, fs.FileMode(perm)) + + var fd uint32 + var err error + if err = fsc.FS().Mkdir(path, fs.FileMode(perm)); err == nil { + fd, err = fsc.OpenFile(path, os.O_RDONLY, 0) + } return callback.invoke(ctx, mod, goos.RefJsfs, err, fd) // note: error first } @@ -446,7 +442,7 @@ func (jsfsRmdir) invoke(ctx context.Context, mod api.Module, args ...interface{} callback := args[1].(funcWrapper) fsc := mod.(*wasm.CallContext).Sys.FS() - err := fsc.Rmdir(path) + err := fsc.FS().Rmdir(path) return jsfsInvoke(ctx, mod, callback, err) } @@ -462,7 +458,7 @@ func (jsfsRename) invoke(ctx context.Context, mod api.Module, args ...interface{ callback := args[2].(funcWrapper) fsc := mod.(*wasm.CallContext).Sys.FS() - err := fsc.Rename(from, to) + err := fsc.FS().Rename(from, to) return jsfsInvoke(ctx, mod, callback, err) } @@ -477,7 +473,7 @@ func (jsfsUnlink) invoke(ctx context.Context, mod api.Module, args ...interface{ callback := args[1].(funcWrapper) fsc := mod.(*wasm.CallContext).Sys.FS() - err := fsc.Unlink(path) + err := fsc.FS().Unlink(path) return jsfsInvoke(ctx, mod, callback, err) } @@ -494,7 +490,7 @@ func (jsfsUtimes) invoke(ctx context.Context, mod api.Module, args ...interface{ callback := args[3].(funcWrapper) fsc := mod.(*wasm.CallContext).Sys.FS() - err := fsc.Utimes(path, atimeSec*1e9, mtimeSec*1e9) + err := fsc.FS().Utimes(path, atimeSec*1e9, mtimeSec*1e9) return jsfsInvoke(ctx, mod, callback, err) } diff --git a/internal/gojs/runtime.go b/internal/gojs/runtime.go index 06b6b1c0..213de99c 100644 --- a/internal/gojs/runtime.go +++ b/internal/gojs/runtime.go @@ -8,6 +8,7 @@ import ( "github.com/tetratelabs/wazero/api" "github.com/tetratelabs/wazero/internal/gojs/custom" "github.com/tetratelabs/wazero/internal/gojs/goarch" + internalsys "github.com/tetratelabs/wazero/internal/sys" "github.com/tetratelabs/wazero/internal/wasm" ) @@ -39,12 +40,9 @@ func wasmWrite(_ context.Context, mod api.Module, stack goarch.Stack) { p := stack.ParamBytes(mod.Memory(), 1 /*, 2 */) fsc := mod.(*wasm.CallContext).Sys.FS() - writer := fsc.FdWriter(fd) - if writer == nil { - panic(fmt.Errorf("unexpected fd %d", fd)) - } - - if _, err := writer.Write(p); err != nil { + if writer := internalsys.WriterForFile(fsc, fd); writer == nil { + panic(fmt.Errorf("fd %d invalid", fd)) + } else if _, err := writer.Write(p); err != nil { panic(fmt.Errorf("error writing p: %w", err)) } } diff --git a/internal/sys/fs.go b/internal/sys/fs.go index 87853977..e3d2142c 100644 --- a/internal/sys/fs.go +++ b/internal/sys/fs.go @@ -38,22 +38,6 @@ const ( modeCharDevice = uint32(fs.ModeCharDevice | 0o640) ) -// EmptyFS is exported to special-case an empty file system. -var EmptyFS = &emptyFS{} - -type emptyFS struct{} - -// compile-time check to ensure emptyFS implements fs.FS -var _ fs.FS = &emptyFS{} - -// Open implements the same method as documented on fs.FS. -func (f *emptyFS) Open(name string) (fs.File, error) { - if !fs.ValidPath(name) { - return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid} - } - return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist} -} - // An emptyRootDir is a fake "/" directory. type emptyRootDir struct{} @@ -175,7 +159,7 @@ type ReadDir struct { type FSContext struct { // fs is the root ("/") mount. - fs fs.FS + fs syscallfs.FS // openedFiles is a map of file descriptor numbers (>=FdRoot) to open files // (or directories) and defaults to empty. @@ -190,27 +174,18 @@ var errNotDir = errors.New("not a directory") // Otherwise, `root` is assigned file descriptor FdRoot and the returned // context can open files in that file system. Any error on opening "." is // returned. -func NewFSContext(stdin io.Reader, stdout, stderr io.Writer, root fs.FS) (fsc *FSContext, err error) { +func NewFSContext(stdin io.Reader, stdout, stderr io.Writer, root syscallfs.FS) (fsc *FSContext, err error) { fsc = &FSContext{fs: root} fsc.openedFiles.Insert(stdinReader(stdin)) fsc.openedFiles.Insert(stdioWriter(stdout, noopStdoutStat)) fsc.openedFiles.Insert(stdioWriter(stderr, noopStderrStat)) - if root == EmptyFS { + if root == syscallfs.EmptyFS { return fsc, nil } - // Open the root directory by using "." as "/" is not relevant in fs.FS. - // This not only validates the file system, but also allows us to test if - // this is a real file or not. ex. `file.(*os.File)`. - // - // Note: We don't use fs.ReadDirFS as this isn't implemented by os.DirFS. - var rootDir fs.File - if sfs, ok := root.(syscallfs.FS); ok { - rootDir, err = sfs.OpenFile(".", os.O_RDONLY, 0) - } else { - rootDir, err = root.Open(".") - } + // Test if this is a real file or not. ex. `file.(*os.File)`. + rootDir, err := root.OpenFile(".", os.O_RDONLY, 0) if err != nil { // This could fail because someone made a special-purpose file system, // which only passes certain filenames and not ".". @@ -255,19 +230,6 @@ func stdioStat(f interface{}, defaultStat stdioFileInfo) fs.FileInfo { return defaultStat } -// OpenedFile returns a file and true if it was opened or nil and false, if syscall.EBADF. -func (c *FSContext) OpenedFile(fd uint32) (*FileEntry, bool) { - return c.openedFiles.Lookup(fd) -} - -func (c *FSContext) StatFile(fd uint32) (fs.FileInfo, error) { - f, ok := c.openedFiles.Lookup(fd) - if !ok { - return nil, syscall.EBADF - } - return f.File.Stat() -} - // fileModeStat is a fake fs.FileInfo which only returns its mode. // This is used for character devices. type fileModeStat fs.FileMode @@ -281,156 +243,37 @@ func (s fileModeStat) Sys() interface{} { return nil } func (s fileModeStat) Name() string { return "" } func (s fileModeStat) IsDir() bool { return false } -// Mkdir is like syscall.Mkdir and returns the file descriptor of the new -// directory or an error. -func (c *FSContext) Mkdir(name string, perm fs.FileMode) (newFD uint32, err error) { - name = c.cleanPath(name) - if wfs, ok := c.fs.(syscallfs.FS); ok { - if err = wfs.Mkdir(name, perm); err != nil { - return - } - // TODO: Determine how to handle when a directory already - // exists or is a file. - return c.OpenFile(name, os.O_RDONLY, perm) - } - err = syscall.ENOSYS - return +// FS returns the underlying filesystem. Any files that should be added to the +// table should be inserted via InsertFile. +func (c *FSContext) FS() syscallfs.FS { + return c.fs } -// OpenFile is like syscall.Open and returns the file descriptor of the new file or an error. -func (c *FSContext) OpenFile(path string, flags int, perm fs.FileMode) (newFD uint32, err error) { - var f fs.File - if wfs, ok := c.fs.(syscallfs.FS); ok { - path = c.cleanPath(path) - f, err = wfs.OpenFile(path, flags, perm) - } else { - // While os.Open says it is read-only, in reality the files returned - // are often writable. Fail only on the flags which won't work. - switch { - case flags&os.O_APPEND != 0: - fallthrough - case flags&os.O_CREATE != 0: - fallthrough - case flags&os.O_TRUNC != 0: - return 0, syscall.ENOSYS - default: - // only time fs.FS is used - f, err = c.fs.Open(c.cleanPath(path)) - } - } - - if err != nil { +// OpenFile opens the file into the table and returns its file descriptor. +// The result must be closed by CloseFile or Close. +func (c *FSContext) OpenFile(path string, flag int, perm fs.FileMode) (uint32, error) { + if f, err := c.fs.OpenFile(path, flag, perm); err != nil { return 0, err - } - - newFD = c.openedFiles.Insert(&FileEntry{Name: pathutil.Base(path), File: f}) - return newFD, nil -} - -// Rmdir is like syscall.Rmdir. -func (c *FSContext) Rmdir(path string) (err error) { - if wfs, ok := c.fs.(syscallfs.FS); ok { - path = c.cleanPath(path) - return wfs.Rmdir(path) - } - err = syscall.ENOSYS - return -} - -func (c *FSContext) StatPath(path string) (fs.FileInfo, error) { - fd, err := c.OpenFile(path, os.O_RDONLY, 0) - if err != nil { - return nil, err - } - defer c.CloseFile(fd) - return c.StatFile(fd) -} - -// Rename is like syscall.Rename. -func (c *FSContext) Rename(from, to string) (err error) { - if wfs, ok := c.fs.(syscallfs.FS); ok { - from = c.cleanPath(from) - to = c.cleanPath(to) - return wfs.Rename(from, to) - } - err = syscall.ENOSYS - return -} - -// Unlink is like syscall.Unlink. -func (c *FSContext) Unlink(path string) (err error) { - if wfs, ok := c.fs.(syscallfs.FS); ok { - path = c.cleanPath(path) - return wfs.Unlink(path) - } - err = syscall.ENOSYS - return -} - -// Utimes is like syscall.Utimes. -func (c *FSContext) Utimes(path string, atimeNsec, mtimeNsec int64) (err error) { - if wfs, ok := c.fs.(syscallfs.FS); ok { - path = c.cleanPath(path) - return wfs.Utimes(path, atimeNsec, mtimeNsec) - } - err = syscall.ENOSYS - return -} - -func (c *FSContext) cleanPath(path string) string { - if len(path) == 0 { - return path - } - // fs.ValidFile cannot be rooted (start with '/') - cleaned := path - if path[0] == '/' { - cleaned = path[1:] - } - cleaned = pathutil.Clean(cleaned) // e.g. "sub/." -> "sub" - return cleaned -} - -// FdWriter returns a valid writer for the given file descriptor or nil if syscall.EBADF. -func (c *FSContext) FdWriter(fd uint32) io.Writer { - // Check to see if the file descriptor is available - if f, ok := c.openedFiles.Lookup(fd); !ok { - return nil - } else if writer, ok := f.File.(io.Writer); !ok { - // Go's syscall.Write also returns EBADF if the FD is present, but not writeable - return nil } else { - return writer + newFD := c.openedFiles.Insert(&FileEntry{Name: pathutil.Base(path), File: f}) + return newFD, nil } } -// FdReader returns a valid reader for the given file descriptor or nil if syscall.EBADF. -func (c *FSContext) FdReader(fd uint32) io.Reader { - switch fd { - case FdStdout, FdStderr: - return nil // writer, not a readable file. - case FdRoot: - return nil // directory, not a readable file. - } - - if f, ok := c.openedFiles.Lookup(fd); !ok { - return nil // TODO: could be a directory not a file. - } else { - return f.File - } +// LookupFile returns a file if it is in the table. +func (c *FSContext) LookupFile(fd uint32) (*FileEntry, bool) { + f, ok := c.openedFiles.Lookup(fd) + return f, ok } -// CloseFile returns true if a file was opened and closed without error, or false if syscall.EBADF. -func (c *FSContext) CloseFile(fd uint32) bool { +// CloseFile returns any error closing the existing file. +func (c *FSContext) CloseFile(fd uint32) error { f, ok := c.openedFiles.Lookup(fd) if !ok { - return false + return syscall.EBADF } c.openedFiles.Delete(fd) - - if err := f.File.Close(); err != nil { - return false - } - return true + return f.File.Close() } // Close implements api.Closer @@ -447,3 +290,23 @@ func (c *FSContext) Close(context.Context) (err error) { c.openedFiles = FileTable{} return } + +// StatFile is a convenience that calls FSContext.LookupFile then fs.File Stat. +// syscall.EBADF is returned on lookup failure. +func StatFile(fsc *FSContext, fd uint32) (stat fs.FileInfo, err error) { + if f, ok := fsc.LookupFile(fd); !ok { + err = syscall.EBADF + } else { + stat, err = f.File.Stat() + } + return +} + +// WriterForFile returns a writer for the given file descriptor or nil if not +// opened or not writeable (e.g. a directory or a file not opened for writes). +func WriterForFile(fsc *FSContext, fd uint32) (writer io.Writer) { + if f, ok := fsc.LookupFile(fd); ok { + writer = f.File.(io.Writer) + } + return +} diff --git a/internal/sys/fs_test.go b/internal/sys/fs_test.go index 7ef2890b..f80f87bd 100644 --- a/internal/sys/fs_test.go +++ b/internal/sys/fs_test.go @@ -7,10 +7,10 @@ import ( "io" "io/fs" "os" - "path" "testing" "testing/fstest" + "github.com/tetratelabs/wazero/internal/syscallfs" testfs "github.com/tetratelabs/wazero/internal/testing/fs" "github.com/tetratelabs/wazero/internal/testing/require" ) @@ -31,26 +31,31 @@ func TestNewFSContext(t *testing.T) { embedFS, err := fs.Sub(testdata, "testdata") require.NoError(t, err) + dirfs, err := syscallfs.NewDirFS(".") + require.NoError(t, err) + // Test various usual configuration for the file system. tests := []struct { - name string - fs fs.FS - expectOsFile bool + name string + fs syscallfs.FS }{ { name: "embed.FS", - fs: embedFS, + fs: syscallfs.Adapt(embedFS), }, { - name: "os.DirFS", + name: "syscallfs.NewDirFS", // Don't use "testdata" because it may not be present in // cross-architecture (a.k.a. scratch) build containers. - fs: os.DirFS("."), - expectOsFile: true, + fs: dirfs, + }, + { + name: "syscallfs.NewReadFS", + fs: syscallfs.NewReadFS(dirfs), }, { name: "fstest.MapFS", - fs: fstest.MapFS{}, + fs: syscallfs.Adapt(fstest.MapFS{}), }, } @@ -67,17 +72,11 @@ func TestNewFSContext(t *testing.T) { require.NotNil(t, rootFile) require.Equal(t, "/", rootFile.Name) - _, osFile := rootFile.File.(*os.File) - require.Equal(t, tc.expectOsFile, osFile) - - f0, err := fsc.OpenFile("/", 0, 0) - require.NoError(t, err) - // Verify that each call to OpenFile returns a different file // descriptor. f1, err := fsc.OpenFile("/", 0, 0) require.NoError(t, err) - require.NotEqual(t, f0, f1) + require.NotEqual(t, FdRoot, f1) // Verify that file descriptors are reused. // @@ -87,119 +86,38 @@ func TestNewFSContext(t *testing.T) { // test to ensure that our implementation properly reuses descriptor // numbers but if we were to change the reuse strategy, this test // would likely break and need to be updated. - require.True(t, fsc.CloseFile(f0)) + require.NoError(t, fsc.CloseFile(f1)) f2, err := fsc.OpenFile("/", 0, 0) require.NoError(t, err) - require.Equal(t, f0, f2) + require.Equal(t, f1, f2) }) } } -func TestEmptyFS(t *testing.T) { - testFS := EmptyFS - - t.Run("validates path", func(t *testing.T) { - f, err := testFS.Open("/foo.txt") - require.Nil(t, f) - require.EqualError(t, err, "open /foo.txt: invalid argument") - }) - - t.Run("path not found", func(t *testing.T) { - f, err := testFS.Open("foo.txt") - require.Nil(t, f) - require.EqualError(t, err, "open foo.txt: file does not exist") - }) -} - func TestEmptyFSContext(t *testing.T) { - testFS, err := NewFSContext(nil, nil, nil, EmptyFS) + testFS, err := NewFSContext(nil, nil, nil, syscallfs.EmptyFS) require.NoError(t, err) - expected := &FSContext{fs: EmptyFS} + expected := &FSContext{fs: syscallfs.EmptyFS} expected.openedFiles.Insert(noopStdin) expected.openedFiles.Insert(noopStdout) expected.openedFiles.Insert(noopStderr) - t.Run("OpenFile doesn't affect state", func(t *testing.T) { - fd, err := testFS.OpenFile("foo.txt", os.O_RDONLY, 0) - require.Zero(t, fd) - require.EqualError(t, err, "open foo.txt: file does not exist") - - // Ensure this didn't modify state - require.Equal(t, expected, testFS) - }) - t.Run("Close closes", func(t *testing.T) { err := testFS.Close(testCtx) require.NoError(t, err) // Closes opened files - require.Equal(t, &FSContext{fs: EmptyFS}, testFS) + require.Equal(t, &FSContext{fs: syscallfs.EmptyFS}, testFS) }) } -func TestContext_File(t *testing.T) { - embedFS, err := fs.Sub(testdata, "testdata") - require.NoError(t, err) - - fsc, err := NewFSContext(nil, nil, nil, embedFS) - require.NoError(t, err) - defer fsc.Close(testCtx) - - tests := []struct { - name string - expected string - }{ - { - name: "empty.txt", - }, - { - name: "test.txt", - expected: "animals\n", - }, - { - name: "sub/test.txt", - expected: "greet sub dir\n", - }, - { - name: "sub/sub/test.txt", - expected: "greet sub sub dir\n", - }, - } - - for _, tt := range tests { - tc := tt - - t.Run(tc.name, func(b *testing.T) { - fd, err := fsc.OpenFile(tc.name, os.O_RDONLY, 0) - require.NoError(t, err) - defer fsc.CloseFile(fd) - - f, ok := fsc.OpenedFile(fd) - require.True(t, ok) - - stat, err := f.File.Stat() - require.NoError(t, err) - - // Ensure the name is the basename and matches the stat name. - require.Equal(t, path.Base(tc.name), f.Name) - require.Equal(t, f.Name, stat.Name()) - - buf := make([]byte, stat.Size()) - size, err := f.File.Read(buf) - if err != nil { - require.Equal(t, io.EOF, err) - } - require.Equal(t, stat.Size(), int64(size)) - - require.Equal(t, tc.expected, string(buf[:size])) - }) - } -} - func TestContext_Close(t *testing.T) { - fsc, err := NewFSContext(nil, nil, nil, testfs.FS{"foo": &testfs.File{}}) + testFS := syscallfs.Adapt(testfs.FS{"foo": &testfs.File{}}) + + fsc, err := NewFSContext(nil, nil, nil, testFS) require.NoError(t, err) + // Verify base case require.Equal(t, 1+FdRoot, uint32(fsc.openedFiles.Len())) @@ -219,7 +137,10 @@ func TestContext_Close(t *testing.T) { func TestContext_Close_Error(t *testing.T) { file := &testfs.File{CloseErr: errors.New("error closing")} - fsc, err := NewFSContext(nil, nil, nil, testfs.FS{"foo": file}) + + testFS := syscallfs.Adapt(testfs.FS{"foo": file}) + + fsc, err := NewFSContext(nil, nil, nil, testFS) require.NoError(t, err) // open another file diff --git a/internal/sys/sys.go b/internal/sys/sys.go index c678f801..2b19976e 100644 --- a/internal/sys/sys.go +++ b/internal/sys/sys.go @@ -8,6 +8,7 @@ import ( "time" "github.com/tetratelabs/wazero/internal/platform" + "github.com/tetratelabs/wazero/internal/syscallfs" "github.com/tetratelabs/wazero/sys" ) @@ -181,9 +182,9 @@ func NewContext( } if fs != nil { - sysCtx.fsc, err = NewFSContext(stdin, stdout, stderr, fs) + sysCtx.fsc, err = NewFSContext(stdin, stdout, stderr, syscallfs.Adapt(fs)) } else { - sysCtx.fsc, err = NewFSContext(stdin, stdout, stderr, EmptyFS) + sysCtx.fsc, err = NewFSContext(stdin, stdout, stderr, syscallfs.EmptyFS) } return diff --git a/internal/sys/sys_test.go b/internal/sys/sys_test.go index 81f38a08..e4e8114d 100644 --- a/internal/sys/sys_test.go +++ b/internal/sys/sys_test.go @@ -6,15 +6,16 @@ import ( "time" "github.com/tetratelabs/wazero/internal/platform" + "github.com/tetratelabs/wazero/internal/syscallfs" testfs "github.com/tetratelabs/wazero/internal/testing/fs" "github.com/tetratelabs/wazero/internal/testing/require" "github.com/tetratelabs/wazero/sys" ) func TestContext_FS(t *testing.T) { - sysCtx := DefaultContext(testfs.FS{}) + sysCtx := DefaultContext(syscallfs.EmptyFS) - fsc, err := NewFSContext(nil, nil, nil, testfs.FS{}) + fsc, err := NewFSContext(nil, nil, nil, syscallfs.EmptyFS) require.NoError(t, err) require.Equal(t, fsc, sysCtx.FS()) @@ -50,7 +51,8 @@ func TestDefaultSysContext(t *testing.T) { require.Equal(t, &ns, sysCtx.nanosleep) require.Equal(t, platform.NewFakeRandSource(), sysCtx.RandSource()) - expectedFS, _ := NewFSContext(nil, nil, nil, testfs.FS{}) + testFS := syscallfs.Adapt(testfs.FS{}) + expectedFS, _ := NewFSContext(nil, nil, nil, testFS) expectedOpenedFiles := FileTable{} expectedOpenedFiles.Insert(noopStdin) diff --git a/internal/syscallfs/adapter.go b/internal/syscallfs/adapter.go new file mode 100644 index 00000000..fc129acf --- /dev/null +++ b/internal/syscallfs/adapter.go @@ -0,0 +1,77 @@ +package syscallfs + +import ( + "fmt" + "io/fs" + "os" + pathutil "path" + "syscall" +) + +// Adapt returns a read-only FS unless the input is already one. +func Adapt(fs fs.FS) FS { + if sys, ok := fs.(FS); ok { + return sys + } + return &adapter{fs} +} + +type adapter struct{ fs fs.FS } + +// Open implements the same method as documented on fs.FS +func (ro *adapter) Open(name string) (fs.File, error) { + panic(fmt.Errorf("unexpected to call fs.FS.Open(%s)", name)) +} + +// OpenFile implements FS.OpenFile +func (ro *adapter) OpenFile(path string, flag int, perm fs.FileMode) (fs.File, error) { + if flag != 0 && flag != os.O_RDONLY { + return nil, syscall.ENOSYS + } + + path = cleanPath(path) + f, err := ro.fs.Open(path) + if err != nil { + // wrapped is fine while FS.OpenFile emulates os.OpenFile vs syscall.OpenFile. + return nil, err + } + return maskForReads(f), nil +} + +func cleanPath(name string) string { + if len(name) == 0 { + return name + } + // fs.ValidFile cannot be rooted (start with '/') + cleaned := name + if name[0] == '/' { + cleaned = name[1:] + } + cleaned = pathutil.Clean(cleaned) // e.g. "sub/." -> "sub" + return cleaned +} + +// Mkdir implements FS.Mkdir +func (ro *adapter) Mkdir(path string, perm fs.FileMode) error { + return syscall.ENOSYS +} + +// Rename implements FS.Rename +func (ro *adapter) Rename(from, to string) error { + return syscall.ENOSYS +} + +// Rmdir implements FS.Rmdir +func (ro *adapter) Rmdir(path string) error { + return syscall.ENOSYS +} + +// Unlink implements FS.Unlink +func (ro *adapter) Unlink(path string) error { + return syscall.ENOSYS +} + +// Utimes implements FS.Utimes +func (ro *adapter) Utimes(path string, atimeNsec, mtimeNsec int64) error { + return syscall.ENOSYS +} diff --git a/internal/syscallfs/adapter_test.go b/internal/syscallfs/adapter_test.go new file mode 100644 index 00000000..bc4d5abf --- /dev/null +++ b/internal/syscallfs/adapter_test.go @@ -0,0 +1,99 @@ +package syscallfs + +import ( + "errors" + "io/fs" + "os" + pathutil "path" + "syscall" + "testing" + + "github.com/tetratelabs/wazero/internal/testing/require" +) + +func TestAdapt_MkDir(t *testing.T) { + dir := t.TempDir() + + testFS := Adapt(os.DirFS(dir)) + + err := testFS.Mkdir("mkdir", fs.ModeDir) + require.Equal(t, syscall.ENOSYS, err) +} + +func TestAdapt_Rename(t *testing.T) { + tmpDir := t.TempDir() + testFS := Adapt(os.DirFS(tmpDir)) + + file1 := "file1" + file1Path := pathutil.Join(tmpDir, file1) + file1Contents := []byte{1} + err := os.WriteFile(file1Path, file1Contents, 0o600) + require.NoError(t, err) + + file2 := "file2" + file2Path := pathutil.Join(tmpDir, file2) + file2Contents := []byte{2} + err = os.WriteFile(file2Path, file2Contents, 0o600) + require.NoError(t, err) + + err = testFS.Rename(file1, file2) + require.Equal(t, syscall.ENOSYS, err) +} + +func TestAdapt_Rmdir(t *testing.T) { + dir := t.TempDir() + + testFS := Adapt(os.DirFS(dir)) + + path := "rmdir" + realPath := pathutil.Join(dir, path) + require.NoError(t, os.Mkdir(realPath, 0o700)) + + err := testFS.Rmdir(path) + require.Equal(t, syscall.ENOSYS, err) +} + +func TestAdapt_Unlink(t *testing.T) { + dir := t.TempDir() + + testFS := Adapt(os.DirFS(dir)) + + path := "unlink" + realPath := pathutil.Join(dir, path) + require.NoError(t, os.WriteFile(realPath, []byte{}, 0o600)) + + err := testFS.Unlink(path) + require.Equal(t, syscall.ENOSYS, err) +} + +func TestAdapt_Utimes(t *testing.T) { + dir := t.TempDir() + + testFS := Adapt(os.DirFS(dir)) + + path := "utimes" + realPath := pathutil.Join(dir, path) + require.NoError(t, os.WriteFile(realPath, []byte{}, 0o600)) + + err := testFS.Utimes(path, 1, 1) + require.Equal(t, syscall.ENOSYS, err) +} + +func TestAdapt_Open_Read(t *testing.T) { + tmpDir := t.TempDir() + + // Create a subdirectory, so we can test reads outside the FS root. + tmpDir = pathutil.Join(tmpDir, t.Name()) + require.NoError(t, os.Mkdir(tmpDir, 0o700)) + + testFS := Adapt(os.DirFS(tmpDir)) + + testFS_Open_Read(t, tmpDir, testFS) + + t.Run("path outside root invalid", func(t *testing.T) { + _, err := testFS.OpenFile("../foo", os.O_RDONLY, 0) + + // fs.FS doesn't allow relative path lookups + require.True(t, errors.Is(err, fs.ErrInvalid)) + }) +} diff --git a/internal/syscallfs/dirfs.go b/internal/syscallfs/dirfs.go index 1ed4f7b1..150be6a8 100644 --- a/internal/syscallfs/dirfs.go +++ b/internal/syscallfs/dirfs.go @@ -29,10 +29,6 @@ func (dir dirFS) Open(name string) (fs.File, error) { // OpenFile implements FS.OpenFile func (dir dirFS) OpenFile(name string, flag int, perm fs.FileMode) (fs.File, error) { - if !fs.ValidPath(name) { - return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid} - } - f, err := os.OpenFile(path.Join(string(dir), name), flag, perm) if err != nil { return nil, err @@ -46,23 +42,12 @@ func (dir dirFS) OpenFile(name string, flag int, perm fs.FileMode) (fs.File, err // Mkdir implements FS.Mkdir func (dir dirFS) Mkdir(name string, perm fs.FileMode) error { - if !fs.ValidPath(name) { - return &fs.PathError{Op: "mkdir", Path: name, Err: fs.ErrInvalid} - } - err := os.Mkdir(path.Join(string(dir), name), perm) - return adjustMkdirError(err) } // Rename implements FS.Rename func (dir dirFS) Rename(from, to string) error { - if !fs.ValidPath(from) { - return syscall.EINVAL - } - if !fs.ValidPath(to) { - return syscall.EINVAL - } if from == to { return nil } @@ -71,31 +56,18 @@ func (dir dirFS) Rename(from, to string) error { // Rmdir implements FS.Rmdir func (dir dirFS) Rmdir(name string) error { - if !fs.ValidPath(name) { - return syscall.EINVAL - } - err := syscall.Rmdir(path.Join(string(dir), name)) - return adjustRmdirError(err) } // Unlink implements FS.Unlink func (dir dirFS) Unlink(name string) error { - if !fs.ValidPath(name) { - return syscall.EINVAL - } - err := syscall.Unlink(path.Join(string(dir), name)) - return adjustUnlinkError(err) } // Utimes implements FS.Utimes func (dir dirFS) Utimes(name string, atimeNsec, mtimeNsec int64) error { - if !fs.ValidPath(name) { - return syscall.EINVAL - } return syscall.UtimesNano(path.Join(string(dir), name), []syscall.Timespec{ syscall.NsecToTimespec(atimeNsec), syscall.NsecToTimespec(mtimeNsec), diff --git a/internal/syscallfs/dirfs_test.go b/internal/syscallfs/dirfs_test.go index 4c09e590..86178ab5 100644 --- a/internal/syscallfs/dirfs_test.go +++ b/internal/syscallfs/dirfs_test.go @@ -4,7 +4,7 @@ import ( "errors" "io/fs" "os" - "path" + pathutil "path" "runtime" "syscall" "testing" @@ -18,7 +18,7 @@ func TestDirFS_MkDir(t *testing.T) { testFS := dirFS(dir) name := "mkdir" - realPath := path.Join(dir, name) + realPath := pathutil.Join(dir, name) t.Run("doesn't exist", func(t *testing.T) { require.NoError(t, testFS.Mkdir(name, fs.ModeDir)) @@ -48,7 +48,7 @@ func TestDirFS_Rename(t *testing.T) { testFS := dirFS(tmpDir) file1 := "file1" - file1Path := path.Join(tmpDir, file1) + file1Path := pathutil.Join(tmpDir, file1) err := os.WriteFile(file1Path, []byte{1}, 0o600) require.NoError(t, err) @@ -60,13 +60,13 @@ func TestDirFS_Rename(t *testing.T) { testFS := dirFS(tmpDir) file1 := "file1" - file1Path := path.Join(tmpDir, file1) + file1Path := pathutil.Join(tmpDir, file1) file1Contents := []byte{1} err := os.WriteFile(file1Path, file1Contents, 0o600) require.NoError(t, err) file2 := "file2" - file2Path := path.Join(tmpDir, file2) + file2Path := pathutil.Join(tmpDir, file2) err = testFS.Rename(file1, file2) require.NoError(t, err) @@ -83,11 +83,11 @@ func TestDirFS_Rename(t *testing.T) { testFS := dirFS(tmpDir) dir1 := "dir1" - dir1Path := path.Join(tmpDir, dir1) + dir1Path := pathutil.Join(tmpDir, dir1) require.NoError(t, os.Mkdir(dir1Path, 0o700)) dir2 := "dir2" - dir2Path := path.Join(tmpDir, dir2) + dir2Path := pathutil.Join(tmpDir, dir2) err := testFS.Rename(dir1, dir2) require.NoError(t, err) @@ -104,11 +104,11 @@ func TestDirFS_Rename(t *testing.T) { testFS := dirFS(tmpDir) dir1 := "dir1" - dir1Path := path.Join(tmpDir, dir1) + dir1Path := pathutil.Join(tmpDir, dir1) require.NoError(t, os.Mkdir(dir1Path, 0o700)) dir2 := "dir2" - dir2Path := path.Join(tmpDir, dir2) + dir2Path := pathutil.Join(tmpDir, dir2) // write a file to that path err := os.WriteFile(dir2Path, []byte{2}, 0o600) @@ -131,13 +131,13 @@ func TestDirFS_Rename(t *testing.T) { testFS := dirFS(tmpDir) file1 := "file1" - file1Path := path.Join(tmpDir, file1) + file1Path := pathutil.Join(tmpDir, file1) file1Contents := []byte{1} err := os.WriteFile(file1Path, file1Contents, 0o600) require.NoError(t, err) dir1 := "dir1" - dir1Path := path.Join(tmpDir, dir1) + dir1Path := pathutil.Join(tmpDir, dir1) require.NoError(t, os.Mkdir(dir1Path, 0o700)) err = testFS.Rename(file1, dir1) @@ -148,18 +148,18 @@ func TestDirFS_Rename(t *testing.T) { testFS := dirFS(tmpDir) dir1 := "dir1" - dir1Path := path.Join(tmpDir, dir1) + dir1Path := pathutil.Join(tmpDir, dir1) require.NoError(t, os.Mkdir(dir1Path, 0o700)) // add a file to that directory file1 := "file1" - file1Path := path.Join(dir1Path, file1) + file1Path := pathutil.Join(dir1Path, file1) file1Contents := []byte{1} err := os.WriteFile(file1Path, file1Contents, 0o600) require.NoError(t, err) dir2 := "dir2" - dir2Path := path.Join(tmpDir, dir2) + dir2Path := pathutil.Join(tmpDir, dir2) require.NoError(t, os.Mkdir(dir2Path, 0o700)) err = testFS.Rename(dir1, dir2) @@ -175,7 +175,7 @@ func TestDirFS_Rename(t *testing.T) { require.Equal(t, syscall.ENOENT, errors.Unwrap(err)) // Show the file inside that directory moved - s, err := os.Stat(path.Join(dir2Path, file1)) + s, err := os.Stat(pathutil.Join(dir2Path, file1)) require.NoError(t, err) require.False(t, s.IsDir()) }) @@ -184,13 +184,13 @@ func TestDirFS_Rename(t *testing.T) { testFS := dirFS(tmpDir) file1 := "file1" - file1Path := path.Join(tmpDir, file1) + file1Path := pathutil.Join(tmpDir, file1) file1Contents := []byte{1} err := os.WriteFile(file1Path, file1Contents, 0o600) require.NoError(t, err) file2 := "file2" - file2Path := path.Join(tmpDir, file2) + file2Path := pathutil.Join(tmpDir, file2) file2Contents := []byte{2} err = os.WriteFile(file2Path, file2Contents, 0o600) require.NoError(t, err) @@ -212,7 +212,7 @@ func TestDirFS_Rename(t *testing.T) { testFS := dirFS(tmpDir) dir1 := "dir1" - dir1Path := path.Join(tmpDir, dir1) + dir1Path := pathutil.Join(tmpDir, dir1) require.NoError(t, os.Mkdir(dir1Path, 0o700)) err := testFS.Rename(dir1, dir1) @@ -227,7 +227,7 @@ func TestDirFS_Rename(t *testing.T) { testFS := dirFS(tmpDir) file1 := "file1" - file1Path := path.Join(tmpDir, file1) + file1Path := pathutil.Join(tmpDir, file1) file1Contents := []byte{1} err := os.WriteFile(file1Path, file1Contents, 0o600) require.NoError(t, err) @@ -247,7 +247,7 @@ func TestDirFS_Rmdir(t *testing.T) { testFS := dirFS(dir) name := "rmdir" - realPath := path.Join(dir, name) + realPath := pathutil.Join(dir, name) t.Run("doesn't exist", func(t *testing.T) { err := testFS.Rmdir(name) @@ -256,7 +256,7 @@ func TestDirFS_Rmdir(t *testing.T) { t.Run("dir not empty", func(t *testing.T) { require.NoError(t, os.Mkdir(realPath, 0o700)) - fileInDir := path.Join(realPath, "file") + fileInDir := pathutil.Join(realPath, "file") require.NoError(t, os.WriteFile(fileInDir, []byte{}, 0o600)) err := testFS.Rmdir(name) @@ -287,7 +287,7 @@ func TestDirFS_Unlink(t *testing.T) { testFS := dirFS(dir) name := "unlink" - realPath := path.Join(dir, name) + realPath := pathutil.Join(dir, name) t.Run("doesn't exist", func(t *testing.T) { err := testFS.Unlink(name) @@ -323,7 +323,18 @@ func TestDirFS_Utimes(t *testing.T) { func TestDirFS_Open_Read(t *testing.T) { tmpDir := t.TempDir() - testFS := dirFS(tmpDir) + // Create a subdirectory, so we can test reads outside the FS root. + tmpDir = pathutil.Join(tmpDir, t.Name()) + require.NoError(t, os.Mkdir(tmpDir, 0o700)) + + testFS := Adapt(dirFS(tmpDir)) testFS_Open_Read(t, tmpDir, testFS) + + t.Run("path outside root valid", func(t *testing.T) { + _, err := testFS.OpenFile("../foo", os.O_RDONLY, 0) + + // syscall.FS allows relative path lookups + require.True(t, errors.Is(err, fs.ErrNotExist)) + }) } diff --git a/internal/syscallfs/empty.go b/internal/syscallfs/empty.go new file mode 100644 index 00000000..14fb233e --- /dev/null +++ b/internal/syscallfs/empty.go @@ -0,0 +1,48 @@ +package syscallfs + +import ( + "fmt" + "io/fs" + "syscall" +) + +// EmptyFS is an FS that returns syscall.ENOENT for all read functions, and +// syscall.ENOSYS otherwise. +var EmptyFS FS = unsupported{} + +type unsupported struct{} + +// Open implements the same method as documented on fs.FS +func (unsupported) Open(name string) (fs.File, error) { + panic(fmt.Errorf("unexpected to call fs.FS.Open(%s)", name)) +} + +// OpenFile implements FS.OpenFile +func (unsupported) OpenFile(path string, flag int, perm fs.FileMode) (fs.File, error) { + return nil, &fs.PathError{Op: "open", Path: path, Err: syscall.ENOENT} +} + +// Mkdir implements FS.Mkdir +func (unsupported) Mkdir(path string, perm fs.FileMode) error { + return syscall.ENOSYS +} + +// Rename implements FS.Rename +func (unsupported) Rename(from, to string) error { + return syscall.ENOSYS +} + +// Rmdir implements FS.Rmdir +func (unsupported) Rmdir(path string) error { + return syscall.ENOSYS +} + +// Unlink implements FS.Unlink +func (unsupported) Unlink(path string) error { + return syscall.ENOSYS +} + +// Utimes implements FS.Utimes +func (unsupported) Utimes(path string, atimeNsec, mtimeNsec int64) error { + return syscall.ENOSYS +} diff --git a/internal/syscallfs/readfs.go b/internal/syscallfs/readfs.go index 5d3a599c..3d9a4626 100644 --- a/internal/syscallfs/readfs.go +++ b/internal/syscallfs/readfs.go @@ -22,15 +22,15 @@ func (ro *readFS) Open(name string) (fs.File, error) { } // OpenFile implements FS.OpenFile -func (ro *readFS) OpenFile(name string, flag int, perm fs.FileMode) (fs.File, error) { +func (ro *readFS) OpenFile(path string, flag int, perm fs.FileMode) (fs.File, error) { if flag == 0 || flag == os.O_RDONLY { - return ro.fs.OpenFile(name, flag, perm) + return ro.fs.OpenFile(path, flag, perm) } return nil, syscall.ENOSYS } // Mkdir implements FS.Mkdir -func (ro *readFS) Mkdir(name string, perm fs.FileMode) error { +func (ro *readFS) Mkdir(path string, perm fs.FileMode) error { return syscall.ENOSYS } @@ -40,16 +40,16 @@ func (ro *readFS) Rename(from, to string) error { } // Rmdir implements FS.Rmdir -func (ro *readFS) Rmdir(name string) error { +func (ro *readFS) Rmdir(path string) error { return syscall.ENOSYS } // Unlink implements FS.Unlink -func (ro *readFS) Unlink(name string) error { +func (ro *readFS) Unlink(path string) error { return syscall.ENOSYS } // Utimes implements FS.Utimes -func (ro *readFS) Utimes(name string, atimeNsec, mtimeNsec int64) error { - return ro.fs.Utimes(name, atimeNsec, mtimeNsec) +func (ro *readFS) Utimes(path string, atimeNsec, mtimeNsec int64) error { + return syscall.ENOSYS } diff --git a/internal/syscallfs/readfs_test.go b/internal/syscallfs/readfs_test.go index e08d4b9f..34c22a11 100644 --- a/internal/syscallfs/readfs_test.go +++ b/internal/syscallfs/readfs_test.go @@ -3,7 +3,7 @@ package syscallfs import ( "io/fs" "os" - "path" + pathutil "path" "syscall" "testing" @@ -24,13 +24,13 @@ func TestReadFS_Rename(t *testing.T) { testFS := NewReadFS(dirFS(tmpDir)) file1 := "file1" - file1Path := path.Join(tmpDir, file1) + file1Path := pathutil.Join(tmpDir, file1) file1Contents := []byte{1} err := os.WriteFile(file1Path, file1Contents, 0o600) require.NoError(t, err) file2 := "file2" - file2Path := path.Join(tmpDir, file2) + file2Path := pathutil.Join(tmpDir, file2) file2Contents := []byte{2} err = os.WriteFile(file2Path, file2Contents, 0o600) require.NoError(t, err) @@ -44,11 +44,11 @@ func TestReadFS_Rmdir(t *testing.T) { testFS := NewReadFS(dirFS(dir)) - name := "rmdir" - realPath := path.Join(dir, name) + path := "rmdir" + realPath := pathutil.Join(dir, path) require.NoError(t, os.Mkdir(realPath, 0o700)) - err := testFS.Rmdir(name) + err := testFS.Rmdir(path) require.Equal(t, syscall.ENOSYS, err) } @@ -57,20 +57,25 @@ func TestReadFS_Unlink(t *testing.T) { testFS := NewReadFS(dirFS(dir)) - name := "unlink" - realPath := path.Join(dir, name) + path := "unlink" + realPath := pathutil.Join(dir, path) require.NoError(t, os.WriteFile(realPath, []byte{}, 0o600)) - err := testFS.Unlink(name) + err := testFS.Unlink(path) require.Equal(t, syscall.ENOSYS, err) } func TestReadFS_Utimes(t *testing.T) { - tmpDir := t.TempDir() + dir := t.TempDir() - testFS := NewReadFS(dirFS(tmpDir)) + testFS := NewReadFS(dirFS(dir)) - testFS_Utimes(t, tmpDir, testFS) + path := "utimes" + realPath := pathutil.Join(dir, path) + require.NoError(t, os.WriteFile(realPath, []byte{}, 0o600)) + + err := testFS.Utimes(path, 1, 1) + require.Equal(t, syscall.ENOSYS, err) } func TestReadFS_Open_Read(t *testing.T) { diff --git a/internal/syscallfs/syscallfs.go b/internal/syscallfs/syscallfs.go index 7acbf0d0..01cedc01 100644 --- a/internal/syscallfs/syscallfs.go +++ b/internal/syscallfs/syscallfs.go @@ -3,6 +3,7 @@ package syscallfs import ( "io" "io/fs" + "os" ) // FS is a writeable fs.FS bridge backed by syscall functions needed for ABI @@ -19,13 +20,13 @@ type FS interface { // OpenFile is similar to os.OpenFile, except the path is relative to this // file system. - OpenFile(name string, flag int, perm fs.FileMode) (fs.File, error) + 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. // Mkdir is similar to os.Mkdir, except the path is relative to this file // system. - Mkdir(name string, perm fs.FileMode) error + 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. @@ -141,3 +142,14 @@ func maskForReads(f fs.File) fs.File { panic("BUG: unhandled pattern") } } + +// StatPath is a convenience that calls FS.OpenFile until there is a stat +// method. +func StatPath(fs FS, path string) (fs.FileInfo, error) { + f, err := fs.OpenFile(path, os.O_RDONLY, 0) + if err != nil { + return nil, err + } + defer f.Close() + return f.Stat() +} diff --git a/internal/wasm/call_context_test.go b/internal/wasm/call_context_test.go index ce10b47f..719e4299 100644 --- a/internal/wasm/call_context_test.go +++ b/internal/wasm/call_context_test.go @@ -155,14 +155,14 @@ func TestCallContext_Close(t *testing.T) { // We use side effects to determine if Close in fact called Context.Close (without repeating sys_test.go). // One side effect of Context.Close is that it clears the openedFiles map. Verify our base case. - _, ok := fsCtx.OpenedFile(3) + _, ok := fsCtx.LookupFile(3) require.True(t, ok, "sysCtx.openedFiles was empty") // Closing should not err. require.NoError(t, m.Close(testCtx)) // Verify our intended side-effect - _, ok = fsCtx.OpenedFile(3) + _, ok = fsCtx.LookupFile(3) require.False(t, ok, "expected no opened files") // Verify no error closing again. @@ -184,7 +184,7 @@ func TestCallContext_Close(t *testing.T) { require.EqualError(t, m.Close(testCtx), "error closing") // Verify our intended side-effect - _, ok := fsCtx.OpenedFile(3) + _, ok := fsCtx.LookupFile(3) require.False(t, ok, "expected no opened files") }) } @@ -251,14 +251,14 @@ func TestCallContext_CallDynamic(t *testing.T) { // We use side effects to determine if Close in fact called Context.Close (without repeating sys_test.go). // One side effect of Context.Close is that it clears the openedFiles map. Verify our base case. - _, ok := fsCtx.OpenedFile(3) + _, ok := fsCtx.LookupFile(3) require.True(t, ok, "sysCtx.openedFiles was empty") // Closing should not err. require.NoError(t, m.Close(testCtx)) // Verify our intended side-effect - _, ok = fsCtx.OpenedFile(3) + _, ok = fsCtx.LookupFile(3) require.False(t, ok, "expected no opened files") // Verify no error closing again. @@ -271,7 +271,8 @@ func TestCallContext_CallDynamic(t *testing.T) { sysCtx := sys.DefaultContext(testFS) fsCtx := sysCtx.FS() - _, err := fsCtx.OpenFile("/foo", os.O_RDONLY, 0) + path := "/foo" + _, err := fsCtx.OpenFile(path, os.O_RDONLY, 0) require.NoError(t, err) m, err := s.Instantiate(context.Background(), ns, &Module{}, t.Name(), sysCtx) @@ -280,7 +281,7 @@ func TestCallContext_CallDynamic(t *testing.T) { require.EqualError(t, m.Close(testCtx), "error closing") // Verify our intended side-effect - _, ok := fsCtx.OpenedFile(3) + _, ok := fsCtx.LookupFile(3) require.False(t, ok, "expected no opened files") }) }