Repository: tetratelabs/wazero
Pull request #1047 (2023-01-18 16:06:09)

fs: allows non-root only mounts
This adds syntax needed for wasi-testsuite, which mounts a path like below without mounting anything for "/".

```
-mount=./tests/c/testsuite/fs-tests.dir:/fs-tests.dir
```

Basically, this creates a virtual root path with fake stat.

With this in, https://github.com/WebAssembly/wasi-testsuite/compare/prod/testsuite-base...codefromthecrypt:wazero?expand=1 passes 100pct cc @achille-roussel 

```bash
$ python3 test-runner/wasi_test_runner.py -t ./tests/assemblyscript/testsuite/  ./tests/c/testsuite/ -r adapters/wazero.sh
Test environ_get-multiple-variables passed
Test fd_write-to-invalid-fd passed
Test environ_sizes_get-multiple-variables passed
Test args_sizes_get-no-arguments passed
Test proc_exit-success passed
Test proc_exit-failure passed
Test args_get-multiple-arguments passed
Test args_sizes_get-multiple-arguments passed
Test fd_write-to-stdout passed
Test random_get-non-zero-length passed
Test environ_sizes_get-no-variables passed
Test random_get-zero-length passed
Test lseek passed
Test clock_getres-realtime passed
Test clock_gettime-monotonic passed
Test clock_getres-monotonic passed
Test fopen-with-access passed
Test stat-dev-ino passed
Test fdopendir-with-access passed
Test clock_gettime-realtime passed
Test pwrite-with-access passed
Test fopen-with-no-access passed
Test pread-with-access passed

===== Test results =====
Runtime: wazero v0.0.0
Suite: WASI Assemblyscript tests
  Total: 12
  Passed:  12
  Failed:  0

Suite: WASI C tests
  Total: 11
  Passed:  11
  Failed:  0

Test suites: 2 passed, 0 total
Tests:       23 passed, 0 total

```


Patch:
diff --git a/internal/syscallfs/rootfs.go b/internal/syscallfs/rootfs.go
index faa0142a..8bcafd87 100644
--- a/internal/syscallfs/rootfs.go
+++ b/internal/syscallfs/rootfs.go
@@ -53,10 +53,9 @@ func NewRootFS(fs ...FS) (FS, error) {
 
 	// Ensure there is always a root match to keep runtime logic simpler.
 	if ret.rootIndex == -1 {
-		// TODO: Make a fake root filesystem that can do a directory listing of
-		// any existing prefixes. We can't use UnimplementedFS as the pre-open
-		// for root must work.
-		return nil, fmt.Errorf("you must supply a root filesystem: %s", fsString(fs))
+		ret.rootIndex = len(fs)
+		ret.prefixes = append(ret.prefixes, "")
+		ret.fs = append(ret.fs, fakeRootFS{})
 	}
 	return ret, nil
 }
@@ -90,7 +89,7 @@ func fsString(fs []FS) string {
 func (c *CompositeFS) Unwrap() []FS {
 	result := make([]FS, 0, len(c.fs))
 	for i := len(c.fs) - 1; i >= 0; i-- {
-		if fs := c.fs[i]; fs != (UnimplementedFS{}) {
+		if fs := c.fs[i]; fs != (fakeRootFS{}) {
 			result = append(result, fs)
 		}
 	}
@@ -376,3 +375,35 @@ loop:
 	}
 	return
 }
+
+type fakeRootFS struct{ UnimplementedFS }
+
+// OpenFile implements FS.OpenFile
+func (fakeRootFS) OpenFile(path string, flag int, perm fs.FileMode) (fs.File, error) {
+	switch path {
+	case ".", "/", "":
+		return fakeRootDir{}, nil
+	}
+	return nil, syscall.ENOENT
+}
+
+type fakeRootDir struct{}
+
+func (fakeRootDir) Close() (err error) { return }
+
+func (fakeRootDir) Stat() (fs.FileInfo, error) { return fakeRootDirInfo{}, nil }
+
+func (fakeRootDir) Read([]byte) (int, error) {
+	return 0, &fs.PathError{Op: "read", Path: "/", Err: syscall.EISDIR}
+}
+
+type fakeRootDirInfo struct{}
+
+func (fakeRootDirInfo) Name() string       { return "/" }
+func (fakeRootDirInfo) Size() int64        { return 0 }
+func (fakeRootDirInfo) Mode() fs.FileMode  { return fs.ModeDir | 0o500 }
+func (fakeRootDirInfo) ModTime() time.Time { return time.Unix(0, 0) }
+func (fakeRootDirInfo) IsDir() bool        { return true }
+func (fakeRootDirInfo) Sys() interface{}   { return nil }
+
+func (fakeRootDir) ReadDir(int) (dirents []fs.DirEntry, err error) { return }


Test patch:
diff --git a/cmd/wazero/wazero_test.go b/cmd/wazero/wazero_test.go
index 4e9affaf..b314ba81 100644
--- a/cmd/wazero/wazero_test.go
+++ b/cmd/wazero/wazero_test.go
@@ -233,6 +233,13 @@ func TestRun(t *testing.T) {
 			wazeroOpts: []string{fmt.Sprintf("--mount=%s:/:ro", bearDir)},
 			stdOut:     "pooh\n",
 		},
+		{
+			name:       "wasi non root",
+			wasm:       wasmCat,
+			wazeroOpts: []string{fmt.Sprintf("--mount=%s:/animals:ro", bearDir)},
+			wasmArgs:   []string{"/animals/bear.txt"},
+			stdOut:     "pooh\n",
+		},
 		{
 			name:       "wasi logging",
 			wasm:       wasmWasiFd,
diff --git a/internal/syscallfs/rootfs_test.go b/internal/syscallfs/rootfs_test.go
index 08a56038..3d865a4d 100644
--- a/internal/syscallfs/rootfs_test.go
+++ b/internal/syscallfs/rootfs_test.go
@@ -34,12 +34,29 @@ func TestNewRootFS(t *testing.T) {
 		// Should not be a composite filesystem
 		require.Equal(t, testFS, rootFS)
 	})
-	t.Run("only non root unsupported", func(t *testing.T) {
+	t.Run("only non root", func(t *testing.T) {
 		testFS, err := NewDirFS(".", "/tmp")
 		require.NoError(t, err)
 
-		_, err = NewRootFS(testFS)
-		require.EqualError(t, err, "you must supply a root filesystem: .:/tmp")
+		rootFS, err := NewRootFS(testFS)
+		require.NoError(t, err)
+
+		// String doesn't include the fake name
+		require.Equal(t, ".:/tmp", rootFS.String())
+
+		// Guest can look up /tmp
+		f, err := rootFS.OpenFile("/tmp", os.O_RDONLY, 0)
+		require.NoError(t, err)
+		require.NoError(t, f.Close())
+
+		// Guest can look up / and see "/tmp" in it
+		f, err = rootFS.OpenFile("/", os.O_RDONLY, 0)
+		require.NoError(t, err)
+		dirents, err := f.(fs.ReadDirFile).ReadDir(-1)
+		require.NoError(t, err)
+		require.Equal(t, 1, len(dirents))
+		require.Equal(t, "tmp", dirents[0].Name())
+		require.True(t, dirents[0].IsDir())
 	})
 	t.Run("multiple roots unsupported", func(t *testing.T) {
 		testFS, err := NewDirFS(".", "/")