Repository: tetratelabs/wazero
Pull request #1095 (2023-02-03 00:30:19)

wasi: implements fd_renumber
part of #1036 

Signed-off-by: Takeshi Yoneda <takeshi@tetrate.io>

mathetake: now 4 tests left in wasi-testsuit
> Tests:       4 failed, 30 passed, 0 total

Patch:
diff --git a/imports/wasi_snapshot_preview1/fs.go b/imports/wasi_snapshot_preview1/fs.go
index eea74c39..b8168fd0 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/internal/sys/file_table.go b/internal/sys/file_table.go
index 45396bed..0c85cf19 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 6275dbff..ac00cf08 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)


Test patch:
diff --git a/imports/wasi_snapshot_preview1/fs_test.go b/imports/wasi_snapshot_preview1/fs_test.go
index 1a783736..e1147d53 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/fs_test.go b/internal/sys/fs_test.go
index 31cf6b23..4a780c0c 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))
+	})
+}