Files
wazero/internal/sys/fs_test.go
Edoardo Vacchi d63813a830 fs: stat and cache mode for stdio devices (#1295)
Ensure that stdio device modes are consistent with the given
file descriptors by stat'ing, instead of returning mocks.

* Use `Stat()` on `poll_oneoff()` too, instead of `IsTerminal()`,
thus avoiding a useless syscall.

* Delete leftover type decl `fileModeStat`.

* Remove IsPlatform()

* Propagate error when Stat() fails

Signed-off-by: Edoardo Vacchi <evacchi@users.noreply.github.com>
2023-03-28 12:48:24 +02:00

416 lines
12 KiB
Go

package sys
import (
"context"
"embed"
"errors"
"io"
"io/fs"
"log"
"os"
"path"
"syscall"
"testing"
"testing/fstest"
"github.com/tetratelabs/wazero/internal/platform"
"github.com/tetratelabs/wazero/internal/sysfs"
testfs "github.com/tetratelabs/wazero/internal/testing/fs"
"github.com/tetratelabs/wazero/internal/testing/require"
)
// testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors.
var testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary")
var (
noopStdin = &FileEntry{Name: "stdin", File: &stdioFileReader{r: eofReader{}, s: noopStdinStat}}
noopStdout = &FileEntry{Name: "stdout", File: &stdioFileWriter{w: io.Discard, s: noopStdoutStat}}
noopStderr = &FileEntry{Name: "stderr", File: &stdioFileWriter{w: io.Discard, s: noopStderrStat}}
)
//go:embed testdata
var testdata embed.FS
func TestNewFSContext(t *testing.T) {
embedFS, err := fs.Sub(testdata, "testdata")
require.NoError(t, err)
dirfs := sysfs.NewDirFS(".")
// Test various usual configuration for the file system.
tests := []struct {
name string
fs sysfs.FS
}{
{
name: "embed.FS",
fs: sysfs.Adapt(embedFS),
},
{
name: "sysfs.NewDirFS",
// Don't use "testdata" because it may not be present in
// cross-architecture (a.k.a. scratch) build containers.
fs: dirfs,
},
{
name: "sysfs.NewReadFS",
fs: sysfs.NewReadFS(dirfs),
},
{
name: "fstest.MapFS",
fs: sysfs.Adapt(fstest.MapFS{}),
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(b *testing.T) {
fsc, err := NewFSContext(nil, nil, nil, tc.fs)
require.NoError(t, err)
defer fsc.Close(testCtx)
preopenedDir, _ := fsc.openedFiles.Lookup(FdPreopen)
require.Equal(t, tc.fs, fsc.rootFS)
require.NotNil(t, preopenedDir)
require.Equal(t, "/", preopenedDir.Name)
// Verify that each call to OpenFile returns a different file
// descriptor.
f1, errno := fsc.OpenFile(preopenedDir.FS, preopenedDir.Name, 0, 0)
require.Zero(t, errno)
require.NotEqual(t, FdPreopen, f1)
// Verify that file descriptors are reused.
//
// Note that this specific behavior is not required by WASI which
// only documents that file descriptor numbers will be selected
// randomly and applications should not rely on them. We added this
// 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.Zero(t, fsc.CloseFile(f1))
f2, errno := fsc.OpenFile(preopenedDir.FS, preopenedDir.Name, 0, 0)
require.Zero(t, errno)
require.Equal(t, f1, f2)
})
}
}
func TestFSContext_CloseFile(t *testing.T) {
embedFS, err := fs.Sub(testdata, "testdata")
require.NoError(t, err)
testFS := sysfs.Adapt(embedFS)
fsc, err := NewFSContext(nil, nil, nil, testFS)
require.NoError(t, err)
defer fsc.Close(testCtx)
fdToClose, errno := fsc.OpenFile(testFS, "empty.txt", os.O_RDONLY, 0)
require.Zero(t, errno)
fdToKeep, errno := fsc.OpenFile(testFS, "test.txt", os.O_RDONLY, 0)
require.Zero(t, errno)
// Close
require.Zero(t, fsc.CloseFile(fdToClose))
// Verify fdToClose is closed and removed from the opened FDs.
_, ok := fsc.LookupFile(fdToClose)
require.False(t, ok)
// Verify fdToKeep is not closed
_, ok = fsc.LookupFile(fdToKeep)
require.True(t, ok)
t.Run("EBADF for an invalid FD", func(t *testing.T) {
require.EqualErrno(t, syscall.EBADF, fsc.CloseFile(42)) // 42 is an arbitrary invalid FD
})
t.Run("Can close a pre-open", func(t *testing.T) {
require.Zero(t, fsc.CloseFile(FdPreopen))
})
}
func TestUnimplementedFSContext(t *testing.T) {
testFS, err := NewFSContext(nil, nil, nil, sysfs.UnimplementedFS{})
require.NoError(t, err)
expected := &FSContext{rootFS: sysfs.UnimplementedFS{}}
expected.openedFiles.Insert(noopStdin)
expected.openedFiles.Insert(noopStdout)
expected.openedFiles.Insert(noopStderr)
t.Run("Close closes", func(t *testing.T) {
err := testFS.Close(testCtx)
require.NoError(t, err)
// Closes opened files
require.Equal(t, &FSContext{rootFS: sysfs.UnimplementedFS{}}, testFS)
})
}
func TestCompositeFSContext(t *testing.T) {
tmpDir1 := t.TempDir()
testFS1 := sysfs.NewDirFS(tmpDir1)
tmpDir2 := t.TempDir()
testFS2 := sysfs.NewDirFS(tmpDir2)
rootFS, err := sysfs.NewRootFS([]sysfs.FS{testFS2, testFS1}, []string{"/tmp", "/"})
require.NoError(t, err)
testFS, err := NewFSContext(nil, nil, nil, rootFS)
require.NoError(t, err)
// Ensure the pre-opens have exactly the name specified, and are in order.
preopen3, ok := testFS.openedFiles.Lookup(3)
require.True(t, ok)
require.Equal(t, "/tmp", preopen3.Name)
preopen4, ok := testFS.openedFiles.Lookup(4)
require.True(t, ok)
require.Equal(t, "/", preopen4.Name)
t.Run("Close closes", func(t *testing.T) {
err := testFS.Close(testCtx)
require.NoError(t, err)
// Closes opened files
require.Equal(t, &FSContext{rootFS: rootFS}, testFS)
})
}
func TestContext_Close(t *testing.T) {
testFS := sysfs.Adapt(testfs.FS{"foo": &testfs.File{}})
fsc, err := NewFSContext(nil, nil, nil, testFS)
require.NoError(t, err)
// Verify base case
require.Equal(t, 1+FdPreopen, uint32(fsc.openedFiles.Len()))
_, errno := fsc.OpenFile(testFS, "foo", os.O_RDONLY, 0)
require.Zero(t, errno)
require.Equal(t, 2+FdPreopen, uint32(fsc.openedFiles.Len()))
// Closing should not err.
require.NoError(t, fsc.Close(testCtx))
// Verify our intended side-effect
require.Zero(t, fsc.openedFiles.Len())
// Verify no error closing again.
require.NoError(t, fsc.Close(testCtx))
}
func TestContext_Close_Error(t *testing.T) {
file := &testfs.File{CloseErr: errors.New("error closing")}
testFS := sysfs.Adapt(testfs.FS{"foo": file})
fsc, err := NewFSContext(nil, nil, nil, testFS)
require.NoError(t, err)
// open another file
_, errno := fsc.OpenFile(testFS, "foo", os.O_RDONLY, 0)
require.Zero(t, errno)
require.EqualError(t, fsc.Close(testCtx), "error closing")
// Paths should clear even under error
require.Zero(t, fsc.openedFiles.Len(), "expected no opened files")
}
func TestFSContext_ReOpenDir(t *testing.T) {
tmpDir := t.TempDir()
dirFs := sysfs.NewDirFS(tmpDir)
const dirName = "dir"
errno := dirFs.Mkdir(dirName, 0o700)
require.Zero(t, errno)
fsc, err := NewFSContext(nil, nil, nil, dirFs)
require.NoError(t, err)
defer func() {
require.NoError(t, fsc.Close(context.Background()))
}()
t.Run("ok", func(t *testing.T) {
dirFd, errno := fsc.OpenFile(dirFs, dirName, os.O_RDONLY, 0o600)
require.Zero(t, errno)
ent, ok := fsc.LookupFile(dirFd)
require.True(t, ok)
// Set arbitrary state.
ent.ReadDir = &ReadDir{Dirents: make([]*platform.Dirent, 10), CountRead: 12345}
// Then reopen the same file descriptor.
ent, errno = fsc.ReOpenDir(dirFd)
require.Zero(t, errno)
// Verify the read dir state has been reset.
require.Equal(t, &ReadDir{}, ent.ReadDir)
})
t.Run("non existing ", func(t *testing.T) {
_, errno = fsc.ReOpenDir(12345)
require.EqualErrno(t, syscall.EBADF, errno)
})
t.Run("not dir", func(t *testing.T) {
const fileName = "dog"
fd, errno := fsc.OpenFile(dirFs, fileName, os.O_CREATE, 0o600)
require.Zero(t, errno)
_, errno = fsc.ReOpenDir(fd)
require.EqualErrno(t, syscall.EISDIR, errno)
})
}
func TestFSContext_Renumber(t *testing.T) {
tmpDir := t.TempDir()
dirFs := sysfs.NewDirFS(tmpDir)
const dirName = "dir"
errno := dirFs.Mkdir(dirName, 0o700)
require.Zero(t, errno)
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, errno := c.OpenFile(dirFs, dirName, os.O_RDONLY, 0)
require.Zero(t, errno)
prevDirFile, ok := c.LookupFile(fromFd)
require.True(t, ok)
require.Zero(t, 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))
})
}
func TestFSContext_ChangeOpenFlag(t *testing.T) {
tmpDir := t.TempDir()
dirFs := sysfs.NewDirFS(tmpDir)
const fileName = "dir"
require.NoError(t, os.WriteFile(path.Join(tmpDir, fileName), []byte("0123456789"), 0o600))
c, errno := NewFSContext(nil, nil, nil, dirFs)
require.NoError(t, errno)
defer func() {
require.NoError(t, c.Close(context.Background()))
}()
// Without APPEND.
fd, errno := c.OpenFile(dirFs, fileName, os.O_RDWR, 0o600)
require.Zero(t, errno)
f0, ok := c.openedFiles.Lookup(fd)
require.True(t, ok)
require.Equal(t, f0.openFlag&syscall.O_APPEND, 0)
// Set the APPEND flag.
require.Zero(t, c.ChangeOpenFlag(fd, syscall.O_APPEND))
f1, ok := c.openedFiles.Lookup(fd)
require.True(t, ok)
require.Equal(t, f1.openFlag&syscall.O_APPEND, syscall.O_APPEND)
// Remove the APPEND flag.
require.Zero(t, c.ChangeOpenFlag(fd, 0))
f2, ok := c.openedFiles.Lookup(fd)
require.True(t, ok)
require.Equal(t, f2.openFlag&syscall.O_APPEND, 0)
}
func TestWriterForFile(t *testing.T) {
testFS, err := NewFSContext(nil, nil, nil, sysfs.UnimplementedFS{})
require.NoError(t, err)
require.Nil(t, WriterForFile(testFS, FdStdin))
require.Equal(t, noopStdout.File, WriterForFile(testFS, FdStdout))
require.Equal(t, noopStderr.File, WriterForFile(testFS, FdStderr))
require.Nil(t, WriterForFile(testFS, FdPreopen))
}
func TestStdioStat(t *testing.T) {
stat, err := stdioStat(os.Stdin, noopStdinStat)
require.NoError(t, err)
stdinStatMode := stat.Mode()
// ensure we are consistent with sys stdin
osStdinStat, _ := os.Stdin.Stat()
osStdinMode := osStdinStat.Mode().Type()
require.Equal(t, osStdinMode&fs.ModeDevice, stdinStatMode&fs.ModeDevice)
require.Equal(t, osStdinMode&fs.ModeCharDevice, stdinStatMode&fs.ModeCharDevice)
stat, err = stdioStat(os.Stdout, noopStdoutStat)
stdoutStatMode := stat.Mode()
require.NoError(t, err)
// ensure we are consistent with sys stdout
osStdoutStat, _ := os.Stdout.Stat()
osStdoutMode := osStdoutStat.Mode().Type()
require.Equal(t, osStdoutMode&fs.ModeDevice, stdoutStatMode&fs.ModeDevice)
require.Equal(t, osStdoutMode&fs.ModeCharDevice, stdoutStatMode&fs.ModeCharDevice)
stat, err = stdioStat(os.Stderr, noopStderrStat)
require.NoError(t, err)
stderrStatMode := stat.Mode()
// ensure we are consistent with sys stderr
osStderrStat, _ := os.Stderr.Stat()
osStderrMode := osStderrStat.Mode().Type()
require.Equal(t, osStderrMode&fs.ModeDevice, stderrStatMode&fs.ModeDevice)
require.Equal(t, osStderrMode&fs.ModeCharDevice, stderrStatMode&fs.ModeCharDevice)
// simulate regular file attached to stdin
f, err := os.CreateTemp("", "somefile")
if err != nil {
log.Fatal(err)
}
defer os.Remove(f.Name()) // clean up
stat, err = stdioStat(f, noopStdinStat)
require.NoError(t, err)
fStat := stat.Mode()
osFStat, _ := f.Stat()
osFStatMode := osFStat.Mode()
require.Equal(t, osFStatMode&fs.ModeDevice, fStat&fs.ModeDevice)
require.Equal(t, osFStatMode&fs.ModeCharDevice, fStat&fs.ModeCharDevice)
// interface{} returns default
stat, err = stdioStat("whatevs", noopStdinStat)
require.NoError(t, err)
require.Equal(t, noopStdinStat, stat)
// nil *File returns err
var nilFile *os.File
_, err = stdioStat(nilFile, noopStdinStat)
require.Error(t, err)
}