Skip to content

Commit

Permalink
fs: adds FSConfig to replace experimental writefs (tetratelabs#1061)
Browse files Browse the repository at this point in the history
This adds a new top-level type FSConfig, which is configured via
`ModuleConfig.WithFSConfig(fcfg)`. This implements read-only and
read-write directory mounts, something not formally supported before. It
also implements `WithFS` which adapts a normal `fs.FS`. For convenience,
we retain the old `ModuleConfig.WithFS` signature so as to not affect
existing users much. A new configuration for our emerging raw
filesystem, `FSConfig.WithSysfs()` will happen later without breaking
this API.

Here's an example:
```
moduleConfig = wazero.NewModuleConfig().
	// Make the current directory read-only accessible to the guest.
	WithReadOnlyDirMount(".", "/")
	// Make "/tmp/wasm" accessible to the guest as "/tmp".
	WithDirMount("/tmp/wasm", "/tmp")
```

Signed-off-by: Adrian Cole <[email protected]>
  • Loading branch information
codefromthecrypt authored Jan 25, 2023
1 parent affcf6c commit cc68f8e
Show file tree
Hide file tree
Showing 25 changed files with 458 additions and 231 deletions.
51 changes: 20 additions & 31 deletions cmd/wazero/wazero.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import (
"github.com/tetratelabs/wazero/experimental/logging"
gojs "github.com/tetratelabs/wazero/imports/go"
"github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
"github.com/tetratelabs/wazero/internal/sysfs"
"github.com/tetratelabs/wazero/internal/version"
"github.com/tetratelabs/wazero/sys"
)
Expand Down Expand Up @@ -161,7 +160,7 @@ func doRun(args []string, stdOut io.Writer, stdErr logging.Writer, exit func(cod
env = append(env, fields[0], fields[1])
}

rootFS := validateMounts(mounts, stdErr, exit)
fsConfig := validateMounts(mounts, stdErr, exit)

wasm, err := os.ReadFile(wasmPath)
if err != nil {
Expand All @@ -188,16 +187,14 @@ func doRun(args []string, stdOut io.Writer, stdErr logging.Writer, exit func(cod
WithStderr(stdErr).
WithStdin(os.Stdin).
WithRandSource(rand.Reader).
WithFSConfig(fsConfig).
WithSysNanosleep().
WithSysNanotime().
WithSysWalltime().
WithArgs(append([]string{wasmExe}, wasmArgs...)...)
for i := 0; i < len(env); i += 2 {
conf = conf.WithEnv(env[i], env[i+1])
}
if rootFS != nil {
conf = conf.WithFS(&sysfs.FSHolder{FS: rootFS})
}

code, err := rt.CompileModule(ctx, wasm)
if err != nil {
Expand Down Expand Up @@ -227,9 +224,8 @@ func doRun(args []string, stdOut io.Writer, stdErr logging.Writer, exit func(cod
exit(0)
}

func validateMounts(mounts sliceFlag, stdErr logging.Writer, exit func(code int)) sysfs.FS {
fs := make([]sysfs.FS, 0, len(mounts))
guestPaths := make([]string, 0, len(mounts))
func validateMounts(mounts sliceFlag, stdErr logging.Writer, exit func(code int)) (config wazero.FSConfig) {
config = wazero.NewFSConfig()
for _, mount := range mounts {
if len(mount) == 0 {
fmt.Fprintln(stdErr, "invalid mount: empty string")
Expand All @@ -243,48 +239,41 @@ func validateMounts(mounts sliceFlag, stdErr logging.Writer, exit func(code int)
}

// TODO(anuraaga): Support wasm paths with colon in them.
var host, guest string
var dir, guestPath string
if clnIdx := strings.LastIndexByte(mount, ':'); clnIdx != -1 {
host, guest = mount[:clnIdx], mount[clnIdx+1:]
dir, guestPath = mount[:clnIdx], mount[clnIdx+1:]
} else {
host = mount
guest = host
dir = mount
guestPath = dir
}

// Provide a better experience if duplicates are found later.
if guest == "" {
guest = "/"
if guestPath == "" {
guestPath = "/"
}

// Eagerly validate the mounts as we know they should be on the host.
if abs, err := filepath.Abs(host); err != nil {
fmt.Fprintf(stdErr, "invalid mount: path %q invalid: %v\n", host, err)
if abs, err := filepath.Abs(dir); err != nil {
fmt.Fprintf(stdErr, "invalid mount: path %q invalid: %v\n", dir, err)
exit(1)
} else {
host = abs
dir = abs
}

if stat, err := os.Stat(host); err != nil {
fmt.Fprintf(stdErr, "invalid mount: path %q error: %v\n", host, err)
if stat, err := os.Stat(dir); err != nil {
fmt.Fprintf(stdErr, "invalid mount: path %q error: %v\n", dir, err)
exit(1)
} else if !stat.IsDir() {
fmt.Fprintf(stdErr, "invalid mount: path %q is not a directory\n", host)
fmt.Fprintf(stdErr, "invalid mount: path %q is not a directory\n", dir)
}

next := sysfs.NewDirFS(host)
if readOnly {
next = sysfs.NewReadFS(next)
config = config.WithReadOnlyDirMount(dir, guestPath)
} else {
config = config.WithDirMount(dir, guestPath)
}
fs = append(fs, next)
guestPaths = append(guestPaths, guest)
}
if fs, err := sysfs.NewRootFS(fs, guestPaths); err != nil {
fmt.Fprintf(stdErr, "invalid mounts %v: %v\n", fs, err)
exit(1)
return nil
} else {
return fs
}
return
}

func detectImports(imports []api.FunctionDefinition) (needsWASI, needsGo bool) {
Expand Down
61 changes: 27 additions & 34 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/tetratelabs/wazero/internal/filecache"
"github.com/tetratelabs/wazero/internal/platform"
internalsys "github.com/tetratelabs/wazero/internal/sys"
"github.com/tetratelabs/wazero/internal/sysfs"
"github.com/tetratelabs/wazero/internal/wasm"
"github.com/tetratelabs/wazero/sys"
)
Expand Down Expand Up @@ -413,38 +414,18 @@ type ModuleConfig interface {
// See https://linux.die.net/man/3/environ and https://en.wikipedia.org/wiki/Null-terminated_string
WithEnv(key, value string) ModuleConfig

// WithFS assigns the file system to use for any paths beginning at "/".
// Defaults return fs.ErrNotExist.
//
// This example sets a read-only, embedded file-system:
//
// //go:embed testdata/index.html
// var testdataIndex embed.FS
//
// rooted, err := fs.Sub(testdataIndex, "testdata")
// require.NoError(t, err)
//
// // "index.html" is accessible as "/index.html".
// config := wazero.NewModuleConfig().WithFS(rooted)
//
// This example sets a mutable file-system:
//
// // Files relative to "/work/appA" are accessible as "/".
// config := wazero.NewModuleConfig().WithFS(os.DirFS("/work/appA"))
//
// Isolation
//
// os.DirFS documentation includes important notes about isolation, which
// also applies to fs.Sub. As of Go 1.19, the built-in file-systems are not
// jailed (chroot). See https://github.com/golang/go/issues/42322
//
// Working Directory "."
//
// Relative path resolution, such as "./config.yml" to "/config.yml" or
// otherwise, is compiler-specific. See /RATIONALE.md for notes.
// WithFS is a convenience that calls WithFSConfig with an FSConfig of the
// input for the root ("/") guest path.
WithFS(fs.FS) ModuleConfig

// WithName configures the module name. Defaults to what was decoded from the name section.
// WithFSConfig configures the filesystem available to each guest
// instantiated with this configuration. By default, no file access is
// allowed, so functions like `path_open` result in unsupported errors
// (e.g. syscall.ENOSYS).
WithFSConfig(FSConfig) ModuleConfig

// WithName configures the module name. Defaults to what was decoded from
// the name section.
WithName(string) ModuleConfig

// WithStartFunctions configures the functions to call after the module is
Expand Down Expand Up @@ -593,8 +574,8 @@ type moduleConfig struct {
environ [][]byte
// environKeys allow overwriting of existing values.
environKeys map[string]int
// fs is the file system to open files with
fs fs.FS
// fsConfig is the file system configuration for ABI like WASI.
fsConfig FSConfig
}

// NewModuleConfig returns a ModuleConfig that can be used for configuring module instantiation.
Expand Down Expand Up @@ -648,8 +629,13 @@ func (c *moduleConfig) WithEnv(key, value string) ModuleConfig {

// WithFS implements ModuleConfig.WithFS
func (c *moduleConfig) WithFS(fs fs.FS) ModuleConfig {
return c.WithFSConfig(NewFSConfig().WithFSMount(fs, ""))
}

// WithFSConfig implements ModuleConfig.WithFSConfig
func (c *moduleConfig) WithFSConfig(config FSConfig) ModuleConfig {
ret := c.clone()
ret.fs = fs
ret.fsConfig = config
return ret
}

Expand Down Expand Up @@ -764,6 +750,13 @@ func (c *moduleConfig) toSysContext() (sysCtx *internalsys.Context, err error) {
environ = append(environ, result)
}

var fs sysfs.FS
if f, ok := c.fsConfig.(*fsConfig); ok {
if fs, err = f.toFS(); err != nil {
return
}
}

return internalsys.NewContext(
math.MaxUint32,
c.args,
Expand All @@ -775,6 +768,6 @@ func (c *moduleConfig) toSysContext() (sysCtx *internalsys.Context, err error) {
c.walltime, c.walltimeResolution,
c.nanotime, c.nanotimeResolution,
c.nanosleep,
c.fs,
fs,
)
}
14 changes: 7 additions & 7 deletions config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import (
"context"
"crypto/rand"
"io"
"io/fs"
"math"
"testing"

"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/fstest"
"github.com/tetratelabs/wazero/internal/platform"
internalsys "github.com/tetratelabs/wazero/internal/sys"
"github.com/tetratelabs/wazero/internal/sysfs"
testfs "github.com/tetratelabs/wazero/internal/testing/fs"
"github.com/tetratelabs/wazero/internal/testing/require"
"github.com/tetratelabs/wazero/internal/wasm"
Expand Down Expand Up @@ -323,7 +323,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
nil, // nanosleep
testFS,
sysfs.Adapt(testFS),
),
},
{
Expand All @@ -339,8 +339,8 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // randSource
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
nil, // nanosleep
testFS2, // fs
nil, // nanosleep
sysfs.Adapt(testFS2), // fs
),
},
{
Expand Down Expand Up @@ -563,7 +563,7 @@ func TestModuleConfig_clone(t *testing.T) {
cloned := mc.clone()

// Make post-clone changes
mc.fs = fstest.FS
mc.fsConfig = NewFSConfig().WithFSMount(fstest.FS, "/")
mc.environKeys["2"] = 2

cloned.environKeys["1"] = 1
Expand All @@ -573,7 +573,7 @@ func TestModuleConfig_clone(t *testing.T) {
require.Equal(t, map[string]int{"1": 1}, cloned.environKeys)

// Ensure the fs is not shared
require.Nil(t, cloned.fs)
require.Nil(t, cloned.fsConfig)
}

func Test_compiledModule_Name(t *testing.T) {
Expand Down Expand Up @@ -697,7 +697,7 @@ func requireSysContext(
walltime *sys.Walltime, walltimeResolution sys.ClockResolution,
nanotime *sys.Nanotime, nanotimeResolution sys.ClockResolution,
nanosleep *sys.Nanosleep,
fs fs.FS,
fs sysfs.FS,
) *internalsys.Context {
sysCtx, err := internalsys.NewContext(
max,
Expand Down
40 changes: 0 additions & 40 deletions experimental/writefs/writefs.go

This file was deleted.

17 changes: 0 additions & 17 deletions experimental/writefs/writefs_example_test.go

This file was deleted.

Loading

0 comments on commit cc68f8e

Please sign in to comment.