diff --git a/imports/wasi_snapshot_preview1/fs.go b/imports/wasi_snapshot_preview1/fs.go index eea74c394a..b8168fd044 100644 --- a/imports/wasi_snapshot_preview1/fs.go +++ b/imports/wasi_snapshot_preview1/fs.go @@ -958,7 +958,19 @@ func openedDir(fsc *sys.FSContext, fd uint32) (fs.ReadDirFile, *sys.ReadDir, Err // replaces a file descriptor by renumbering another file descriptor. // // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_renumberfd-fd-to-fd---errno -var fdRenumber = stubFunction(FdRenumberName, []wasm.ValueType{i32, i32}, "fd", "to") +var fdRenumber = newHostFunc(FdRenumberName, fdRenumberFn, []wasm.ValueType{i32, i32}, "fd", "to") + +func fdRenumberFn(_ context.Context, mod api.Module, params []uint64) Errno { + fsc := mod.(*wasm.CallContext).Sys.FS() + + from := uint32(params[0]) + to := uint32(params[1]) + + if err := fsc.Renumber(from, to); err != nil { + return ToErrno(err) + } + return ErrnoSuccess +} // fdSeek is the WASI function named FdSeekName which moves the offset of a // file descriptor. diff --git a/imports/wasi_snapshot_preview1/fs_test.go b/imports/wasi_snapshot_preview1/fs_test.go index 1a783736b3..e1147d53e2 100644 --- a/imports/wasi_snapshot_preview1/fs_test.go +++ b/imports/wasi_snapshot_preview1/fs_test.go @@ -2098,11 +2098,98 @@ func Test_fdReaddir_Errors(t *testing.T) { // Test_fdRenumber only tests it is stubbed for GrainLang per #271 func Test_fdRenumber(t *testing.T) { - log := requireErrnoNosys(t, FdRenumberName, 0, 0) - require.Equal(t, ` ---> wasi_snapshot_preview1.fd_renumber(fd=0,to=0) -<-- errno=ENOSYS -`, log) + const preopenFd, fileFd, dirFd = 3, 4, 5 + + tests := []struct { + name string + from, to uint32 + expectedErrno Errno + expectedLog string + }{ + { + name: "from=preopen", + from: preopenFd, + to: dirFd, + expectedErrno: ErrnoNotsup, + expectedLog: ` +==> wasi_snapshot_preview1.fd_renumber(fd=3,to=5) +<== errno=ENOTSUP +`, + }, + { + name: "to=preopen", + from: dirFd, + to: 3, + expectedErrno: ErrnoNotsup, + expectedLog: ` +==> wasi_snapshot_preview1.fd_renumber(fd=5,to=3) +<== errno=ENOTSUP +`, + }, + { + name: "file to dir", + from: fileFd, + to: dirFd, + expectedErrno: ErrnoSuccess, + expectedLog: ` +==> wasi_snapshot_preview1.fd_renumber(fd=4,to=5) +<== errno=ESUCCESS +`, + }, + { + name: "dir to file", + from: dirFd, + to: fileFd, + expectedErrno: ErrnoSuccess, + expectedLog: ` +==> wasi_snapshot_preview1.fd_renumber(fd=5,to=4) +<== errno=ESUCCESS +`, + }, + { + name: "dir to any", + from: dirFd, + to: 12345, + expectedErrno: ErrnoSuccess, + expectedLog: ` +==> wasi_snapshot_preview1.fd_renumber(fd=5,to=12345) +<== errno=ESUCCESS +`, + }, + { + name: "file to any", + from: fileFd, + to: 54, + expectedErrno: ErrnoSuccess, + expectedLog: ` +==> wasi_snapshot_preview1.fd_renumber(fd=4,to=54) +<== errno=ESUCCESS +`, + }, + } + + for _, tt := range tests { + tc := tt + t.Run(tc.name, func(t *testing.T) { + mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(fstest.FS)) + defer r.Close(testCtx) + + fsc := mod.(*wasm.CallContext).Sys.FS() + preopen := fsc.RootFS() + + // Sanity check of the file descriptor assignment. + fileFdAssigned, err := fsc.OpenFile(preopen, "animals.txt", os.O_RDONLY, 0) + require.NoError(t, err) + require.Equal(t, uint32(fileFd), fileFdAssigned) + + dirFdAssigned, err := fsc.OpenFile(preopen, "dir", os.O_RDONLY, 0) + require.NoError(t, err) + require.Equal(t, uint32(dirFd), dirFdAssigned) + + requireErrno(t, tc.expectedErrno, mod, FdRenumberName, uint64(tc.from), uint64(tc.to)) + require.Equal(t, tc.expectedLog, "\n"+log.String()) + }) + } } func Test_fdSeek(t *testing.T) { diff --git a/internal/sys/file_table.go b/internal/sys/file_table.go index 45396beda2..0c85cf192e 100644 --- a/internal/sys/file_table.go +++ b/internal/sys/file_table.go @@ -92,6 +92,17 @@ func (t *FileTable) Lookup(fd uint32) (file *FileEntry, found bool) { return } +// InsertAt inserts the given `file` at the file descriptor `fd`. +func (t *FileTable) InsertAt(file *FileEntry, fd uint32) { + if diff := int(fd) - t.Len(); diff > 0 { + t.Grow(diff) + } + index := uint(fd) / 64 + shift := uint(fd) % 64 + t.masks[index] |= 1 << shift + t.files[fd] = file +} + // Delete deletes the file stored at the given fd from the table. func (t *FileTable) Delete(fd uint32) { if index, shift := fd/64, fd%64; int(index) < len(t.masks) { diff --git a/internal/sys/fs.go b/internal/sys/fs.go index 6275dbff67..ac00cf0897 100644 --- a/internal/sys/fs.go +++ b/internal/sys/fs.go @@ -345,6 +345,29 @@ func (c *FSContext) LookupFile(fd uint32) (*FileEntry, bool) { return f, ok } +// Renumber assigns the file pointed by the descriptor `from` to `to`. +func (c *FSContext) Renumber(from, to uint32) error { + fromFile, ok := c.openedFiles.Lookup(from) + if !ok { + return syscall.EBADF + } else if fromFile.IsPreopen { + return syscall.ENOTSUP + } + + toFile, ok := c.openedFiles.Lookup(to) + if ok && toFile.IsPreopen { + return syscall.ENOTSUP + } + + // TODO: What should we do to the dangling file `toFile` if `to` is already opened? + // The doc is unclear and other implementations does nothing for already-opened To FDs. + // https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_renumberfd-fd-to-fd---errno + // https://github.com/bytecodealliance/wasmtime/blob/main/crates/wasi-common/src/snapshots/preview_1.rs#L531-L546 + c.openedFiles.Delete(from) + c.openedFiles.InsertAt(fromFile, to) + return nil +} + // CloseFile returns any error closing the existing file. func (c *FSContext) CloseFile(fd uint32) error { f, ok := c.openedFiles.Lookup(fd) diff --git a/internal/sys/fs_test.go b/internal/sys/fs_test.go index 31cf6b23ec..4a780c0ce7 100644 --- a/internal/sys/fs_test.go +++ b/internal/sys/fs_test.go @@ -262,3 +262,53 @@ func TestFSContext_ReOpenDir(t *testing.T) { require.ErrorIs(t, err, syscall.EISDIR) }) } + +func TestFSContext_Renumber(t *testing.T) { + tmpDir := t.TempDir() + dirFs := sysfs.NewDirFS(tmpDir) + + const dirName = "dir" + err := dirFs.Mkdir(dirName, 0o700) + require.NoError(t, err) + + c, err := NewFSContext(nil, nil, nil, dirFs) + require.NoError(t, err) + defer func() { + require.NoError(t, c.Close(context.Background())) + }() + + for _, toFd := range []uint32{10, 100, 100} { + fromFd, err := c.OpenFile(dirFs, dirName, os.O_RDONLY, 0) + require.NoError(t, err) + + prevDirFile, ok := c.LookupFile(fromFd) + require.True(t, ok) + + require.Equal(t, nil, c.Renumber(fromFd, toFd)) + + renumberedDirFile, ok := c.LookupFile(toFd) + require.True(t, ok) + + require.Equal(t, prevDirFile, renumberedDirFile) + + // Previous file descriptor shouldn't be used. + _, ok = c.LookupFile(fromFd) + require.False(t, ok) + } + + t.Run("errors", func(t *testing.T) { + // Sanity check for 3 being preopen. + preopen, ok := c.LookupFile(3) + require.True(t, ok) + require.True(t, preopen.IsPreopen) + + // From is preopen. + require.Equal(t, syscall.ENOTSUP, c.Renumber(3, 100)) + + // From does not exist. + require.Equal(t, syscall.EBADF, c.Renumber(12345, 3)) + + // Both are preopen. + require.Equal(t, syscall.ENOTSUP, c.Renumber(3, 3)) + }) +}