From d6330d9cfa1ce5074c07417f73f1af06c0132236 Mon Sep 17 00:00:00 2001 From: Crypt Keeper <64215+codefromthecrypt@users.noreply.github.com> Date: Fri, 17 Jun 2022 11:31:48 +0800 Subject: [PATCH] Makes fake clocks increment and fixes mutability bug (#630) This ensures fake clocks increment so that compilers that implement sleep with them don't spin. This also fixes a mutability bug in config where we weren't really doing clone properly because map references are shared. Signed-off-by: Adrian Cole --- RATIONALE.md | 8 ++ config.go | 144 ++++++++++++--------- config_test.go | 191 +++++++++++++++++----------- internal/platform/time.go | 34 ++++- internal/platform/time_test.go | 22 ++++ internal/sys/fs.go | 22 +++- internal/sys/fs_test.go | 17 +++ internal/sys/sys.go | 6 +- internal/sys/sys_test.go | 16 ++- wasi_snapshot_preview1/wasi_test.go | 128 +++++++++---------- 10 files changed, 360 insertions(+), 228 deletions(-) diff --git a/RATIONALE.md b/RATIONALE.md index 32108c08..0fece587 100644 --- a/RATIONALE.md +++ b/RATIONALE.md @@ -425,6 +425,14 @@ of requiring configuration to opt-into real clocks. See https://gruss.cc/files/fantastictimers.pdf for an example attacks. +## Why does fake time increase on reading? + +Both the fake nanotime and walltime increase by 1ms on reading. Particularly in +the case of nanotime, this prevents spinning. For example, when Go compiles +`time.Sleep` using `GOOS=js GOARCH=wasm`, nanotime is used in a loop. If that +never increases, the gouroutine is mistaken for being busy. This would be worse +if a compiler implement sleep using nanotime, yet doesn't check for spinning! + ## Why not `time.Clock`? wazero can't use `time.Clock` as a plugin for clock implementation as it is diff --git a/config.go b/config.go index 1cc86891..920cccc3 100644 --- a/config.go +++ b/config.go @@ -110,7 +110,7 @@ type RuntimeConfig interface { // See https://github.com/WebAssembly/spec/blob/main/proposals/sign-extension-ops/Overview.md WithFeatureSignExtensionOps(bool) RuntimeConfig - // WithFeatureSIMD enables the vector value type and vector instructions (aka SIMD). This defaults to false + // WithFeatureSIMD enables the vector value type and vector instructions (aka SIMD). This defaults to false // as the feature was not in WebAssembly 1.0. // // See https://github.com/WebAssembly/spec/blob/main/proposals/simd/SIMD.md @@ -171,83 +171,89 @@ var engineLessConfig = &runtimeConfig{ // support Compiler. Use NewRuntimeConfig to safely detect and fallback to // NewRuntimeConfigInterpreter if needed. func NewRuntimeConfigCompiler() RuntimeConfig { - ret := *engineLessConfig // copy + ret := engineLessConfig.clone() ret.newEngine = compiler.NewEngine - return &ret + return ret } // NewRuntimeConfigInterpreter interprets WebAssembly modules instead of compiling them into assembly. func NewRuntimeConfigInterpreter() RuntimeConfig { - ret := *engineLessConfig // copy + ret := engineLessConfig.clone() ret.newEngine = interpreter.NewEngine + return ret +} + +// clone makes a deep copy of this runtime config. +func (c *runtimeConfig) clone() *runtimeConfig { + ret := *c // copy except maps which share a ref return &ret } // WithFeatureBulkMemoryOperations implements RuntimeConfig.WithFeatureBulkMemoryOperations func (c *runtimeConfig) WithFeatureBulkMemoryOperations(enabled bool) RuntimeConfig { - ret := *c // copy + ret := c.clone() ret.enabledFeatures = ret.enabledFeatures.Set(wasm.FeatureBulkMemoryOperations, enabled) // bulk-memory-operations proposal is mutually-dependant with reference-types proposal. ret.enabledFeatures = ret.enabledFeatures.Set(wasm.FeatureReferenceTypes, enabled) - return &ret + return ret } // WithFeatureMultiValue implements RuntimeConfig.WithFeatureMultiValue func (c *runtimeConfig) WithFeatureMultiValue(enabled bool) RuntimeConfig { - ret := *c // copy + ret := c.clone() ret.enabledFeatures = ret.enabledFeatures.Set(wasm.FeatureMultiValue, enabled) - return &ret + return ret } // WithFeatureMutableGlobal implements RuntimeConfig.WithFeatureMutableGlobal func (c *runtimeConfig) WithFeatureMutableGlobal(enabled bool) RuntimeConfig { - ret := *c // copy + ret := c.clone() ret.enabledFeatures = ret.enabledFeatures.Set(wasm.FeatureMutableGlobal, enabled) - return &ret + return ret } // WithFeatureNonTrappingFloatToIntConversion implements RuntimeConfig.WithFeatureNonTrappingFloatToIntConversion func (c *runtimeConfig) WithFeatureNonTrappingFloatToIntConversion(enabled bool) RuntimeConfig { - ret := *c // copy + ret := c.clone() ret.enabledFeatures = ret.enabledFeatures.Set(wasm.FeatureNonTrappingFloatToIntConversion, enabled) - return &ret + return ret } // WithFeatureReferenceTypes implements RuntimeConfig.WithFeatureReferenceTypes func (c *runtimeConfig) WithFeatureReferenceTypes(enabled bool) RuntimeConfig { - ret := *c // copy + ret := c.clone() ret.enabledFeatures = ret.enabledFeatures.Set(wasm.FeatureReferenceTypes, enabled) // reference-types proposal is mutually-dependant with bulk-memory-operations proposal. ret.enabledFeatures = ret.enabledFeatures.Set(wasm.FeatureBulkMemoryOperations, enabled) - return &ret + return ret } // WithFeatureSignExtensionOps implements RuntimeConfig.WithFeatureSignExtensionOps func (c *runtimeConfig) WithFeatureSignExtensionOps(enabled bool) RuntimeConfig { - ret := *c // copy + ret := c.clone() ret.enabledFeatures = ret.enabledFeatures.Set(wasm.FeatureSignExtensionOps, enabled) - return &ret + return ret } // WithFeatureSIMD implements RuntimeConfig.WithFeatureSIMD func (c *runtimeConfig) WithFeatureSIMD(enabled bool) RuntimeConfig { - ret := *c // copy + ret := c.clone() ret.enabledFeatures = ret.enabledFeatures.Set(wasm.FeatureSIMD, enabled) - return &ret + return ret } // WithWasmCore1 implements RuntimeConfig.WithWasmCore1 func (c *runtimeConfig) WithWasmCore1() RuntimeConfig { - ret := *c // copy + ret := c.clone() ret.enabledFeatures = wasm.Features20191205 - return &ret + return ret } // WithWasmCore2 implements RuntimeConfig.WithWasmCore2 func (c *runtimeConfig) WithWasmCore2() RuntimeConfig { - ret := *c // copy + ret := c.clone() ret.enabledFeatures = wasm.Features20220419 - return &ret + return ret } // CompiledModule is a WebAssembly 1.0 module ready to be instantiated (Runtime.InstantiateModule) as an api.Module. @@ -314,14 +320,20 @@ func NewCompileConfig() CompileConfig { } } +// clone makes a deep copy of this compile config. +func (c *compileConfig) clone() *compileConfig { + ret := *c // copy except maps which share a ref + return &ret +} + // WithImportRenamer implements CompileConfig.WithImportRenamer func (c *compileConfig) WithImportRenamer(importRenamer api.ImportRenamer) CompileConfig { if importRenamer == nil { return c } - ret := *c // copy + ret := c.clone() ret.importRenamer = importRenamer - return &ret + return ret } // WithMemorySizer implements CompileConfig.WithMemorySizer @@ -329,9 +341,9 @@ func (c *compileConfig) WithMemorySizer(memorySizer api.MemorySizer) CompileConf if memorySizer == nil { return c } - ret := *c // copy + ret := c.clone() ret.memorySizer = memorySizer - return &ret + return ret } // ModuleConfig configures resources needed by functions that have low-level interactions with the host operating @@ -446,7 +458,8 @@ type ModuleConfig interface { WithStdout(io.Writer) ModuleConfig // WithWalltime configures the wall clock, sometimes referred to as the - // real time clock. Defaults to a constant fake result. + // real time clock. Defaults to a fake result that increases by 1ms on + // each reading. // // Ex. To override with your own clock: // moduleConfig = moduleConfig. @@ -465,7 +478,8 @@ type ModuleConfig interface { WithSysWalltime() ModuleConfig // WithNanotime configures the monotonic clock, used to measure elapsed - // time in nanoseconds. Defaults to a constant fake result. + // time in nanoseconds. Defaults to a fake result that increases by 1ms + // on each reading. // // Ex. To override with your own clock: // moduleConfig = moduleConfig. @@ -512,9 +526,9 @@ type moduleConfig struct { stdout io.Writer stderr io.Writer randSource io.Reader - walltimeTime *sys.Walltime + walltime *sys.Walltime walltimeResolution sys.ClockResolution - nanotimeTime *sys.Nanotime + nanotime *sys.Nanotime nanotimeResolution sys.ClockResolution args []string // environ is pair-indexed to retain order similar to os.Environ. @@ -529,21 +543,31 @@ func NewModuleConfig() ModuleConfig { return &moduleConfig{ startFunctions: []string{"_start"}, environKeys: map[string]int{}, - - fs: internalsys.NewFSConfig(), + fs: internalsys.NewFSConfig(), } } +// clone makes a deep copy of this module config. +func (c *moduleConfig) clone() *moduleConfig { + ret := *c // copy except maps which share a ref + ret.environKeys = make(map[string]int, len(c.environKeys)) + for key, value := range c.environKeys { + ret.environKeys[key] = value + } + ret.fs = c.fs.Clone() + return &ret +} + // WithArgs implements ModuleConfig.WithArgs func (c *moduleConfig) WithArgs(args ...string) ModuleConfig { - ret := *c // copy + ret := c.clone() ret.args = args - return &ret + return ret } // WithEnv implements ModuleConfig.WithEnv func (c *moduleConfig) WithEnv(key, value string) ModuleConfig { - ret := *c // copy + ret := c.clone() // Check to see if this key already exists and update it. if i, ok := ret.environKeys[key]; ok { ret.environ[i+1] = value // environ is pair-indexed, so the value is 1 after the key. @@ -551,57 +575,57 @@ func (c *moduleConfig) WithEnv(key, value string) ModuleConfig { ret.environKeys[key] = len(ret.environ) ret.environ = append(ret.environ, key, value) } - return &ret + return ret } // WithFS implements ModuleConfig.WithFS func (c *moduleConfig) WithFS(fs fs.FS) ModuleConfig { - ret := *c // copy + ret := c.clone() ret.fs = ret.fs.WithFS(fs) - return &ret + return ret } // WithName implements ModuleConfig.WithName func (c *moduleConfig) WithName(name string) ModuleConfig { - ret := *c // copy + ret := c.clone() ret.name = name - return &ret + return ret } // WithStartFunctions implements ModuleConfig.WithStartFunctions func (c *moduleConfig) WithStartFunctions(startFunctions ...string) ModuleConfig { - ret := *c // copy + ret := c.clone() ret.startFunctions = startFunctions - return &ret + return ret } // WithStderr implements ModuleConfig.WithStderr func (c *moduleConfig) WithStderr(stderr io.Writer) ModuleConfig { - ret := *c // copy + ret := c.clone() ret.stderr = stderr - return &ret + return ret } // WithStdin implements ModuleConfig.WithStdin func (c *moduleConfig) WithStdin(stdin io.Reader) ModuleConfig { - ret := *c // copy + ret := c.clone() ret.stdin = stdin - return &ret + return ret } // WithStdout implements ModuleConfig.WithStdout func (c *moduleConfig) WithStdout(stdout io.Writer) ModuleConfig { - ret := *c // copy + ret := c.clone() ret.stdout = stdout - return &ret + return ret } // WithWalltime implements ModuleConfig.WithWalltime func (c *moduleConfig) WithWalltime(walltime sys.Walltime, resolution sys.ClockResolution) ModuleConfig { - ret := *c // copy - ret.walltimeTime = &walltime + ret := c.clone() + ret.walltime = &walltime ret.walltimeResolution = resolution - return &ret + return ret } // We choose arbitrary resolutions here because there's no perfect alternative. For example, according to the @@ -615,10 +639,10 @@ func (c *moduleConfig) WithSysWalltime() ModuleConfig { // WithNanotime implements ModuleConfig.WithNanotime func (c *moduleConfig) WithNanotime(nanotime sys.Nanotime, resolution sys.ClockResolution) ModuleConfig { - ret := *c // copy - ret.nanotimeTime = &nanotime + ret := c.clone() + ret.nanotime = &nanotime ret.nanotimeResolution = resolution - return &ret + return ret } // WithSysNanotime implements ModuleConfig.WithSysNanotime @@ -628,16 +652,16 @@ func (c *moduleConfig) WithSysNanotime() ModuleConfig { // WithRandSource implements ModuleConfig.WithRandSource func (c *moduleConfig) WithRandSource(source io.Reader) ModuleConfig { - ret := *c // copy + ret := c.clone() ret.randSource = source - return &ret + return ret } // WithWorkDirFS implements ModuleConfig.WithWorkDirFS func (c *moduleConfig) WithWorkDirFS(fs fs.FS) ModuleConfig { - ret := *c // copy + ret := c.clone() ret.fs = ret.fs.WithWorkDirFS(fs) - return &ret + return ret } // toSysContext creates a baseline wasm.Context configured by ModuleConfig. @@ -672,8 +696,8 @@ func (c *moduleConfig) toSysContext() (sysCtx *internalsys.Context, err error) { c.stdout, c.stderr, c.randSource, - c.walltimeTime, c.walltimeResolution, - c.nanotimeTime, c.nanotimeResolution, + c.walltime, c.walltimeResolution, + c.nanotime, c.nanotimeResolution, preopens, ) } diff --git a/config_test.go b/config_test.go index 9f519d4a..bacb5415 100644 --- a/config_test.go +++ b/config_test.go @@ -252,48 +252,58 @@ func TestModuleConfig(t *testing.T) { tests := []struct { name string with func(ModuleConfig) ModuleConfig - expected ModuleConfig + expected string }{ { name: "WithName", with: func(c ModuleConfig) ModuleConfig { return c.WithName("wazero") }, - expected: &moduleConfig{ - name: "wazero", - }, + expected: "wazero", }, { name: "WithName empty", with: func(c ModuleConfig) ModuleConfig { return c.WithName("") }, - expected: &moduleConfig{}, }, { name: "WithName twice", with: func(c ModuleConfig) ModuleConfig { return c.WithName("wazero").WithName("wa0") }, - expected: &moduleConfig{ - name: "wa0", - }, + expected: "wa0", }, } for _, tt := range tests { tc := tt t.Run(tc.name, func(t *testing.T) { - input := &moduleConfig{} + input := NewModuleConfig() rc := tc.with(input) - require.Equal(t, tc.expected, rc) + require.Equal(t, tc.expected, rc.(*moduleConfig).name) // The source wasn't modified - require.Equal(t, &moduleConfig{}, input) + require.Equal(t, NewModuleConfig(), input) }) } } +// TestModuleConfig_toSysContext only tests the cases that change the inputs to +// sys.NewContext. func TestModuleConfig_toSysContext(t *testing.T) { + // Always assigns clocks so that pointers are constant. + var wt sys.Walltime = func(context.Context) (int64, int32) { + return 0, 0 + } + var nt sys.Nanotime = func(context.Context) int64 { + return 0 + } + base := NewModuleConfig() + base.(*moduleConfig).walltime = &wt + base.(*moduleConfig).walltimeResolution = 1 + base.(*moduleConfig).nanotime = &nt + base.(*moduleConfig).nanotimeResolution = 1 + testFS := fstest.MapFS{} testFS2 := fstest.MapFS{} @@ -304,7 +314,7 @@ func TestModuleConfig_toSysContext(t *testing.T) { }{ { name: "empty", - input: NewModuleConfig(), + input: base, expected: requireSysContext(t, math.MaxUint32, // max nil, // args @@ -313,14 +323,14 @@ func TestModuleConfig_toSysContext(t *testing.T) { nil, // stdout nil, // stderr nil, // randSource - nil, 0, // walltime, walltimeResolution - nil, 0, // nanotime, nanotimeResolution + &wt, 1, // walltime, walltimeResolution + &nt, 1, // nanotime, nanotimeResolution nil, // openedFiles ), }, { name: "WithArgs", - input: NewModuleConfig().WithArgs("a", "bc"), + input: base.WithArgs("a", "bc"), expected: requireSysContext(t, math.MaxUint32, // max []string{"a", "bc"}, // args @@ -329,14 +339,15 @@ func TestModuleConfig_toSysContext(t *testing.T) { nil, // stdout nil, // stderr nil, // randSource - nil, 0, // walltime, walltimeResolution - nil, 0, // nanotime, nanotimeResolution + &wt, 1, // walltime, walltimeResolution + &nt, 1, // nanotime, nanotimeResolution + nil, // openedFiles ), }, { name: "WithArgs empty ok", // Particularly argv[0] can be empty, and we have no rules about others. - input: NewModuleConfig().WithArgs("", "bc"), + input: base.WithArgs("", "bc"), expected: requireSysContext(t, math.MaxUint32, // max []string{"", "bc"}, // args @@ -345,14 +356,15 @@ func TestModuleConfig_toSysContext(t *testing.T) { nil, // stdout nil, // stderr nil, // randSource - nil, 0, // walltime, walltimeResolution - nil, 0, // nanotime, nanotimeResolution + &wt, 1, // walltime, walltimeResolution + &nt, 1, // nanotime, nanotimeResolution + nil, // openedFiles ), }, { name: "WithArgs second call overwrites", - input: NewModuleConfig().WithArgs("a", "bc").WithArgs("bc", "a"), + input: base.WithArgs("a", "bc").WithArgs("bc", "a"), expected: requireSysContext(t, math.MaxUint32, // max []string{"bc", "a"}, // args @@ -361,14 +373,15 @@ func TestModuleConfig_toSysContext(t *testing.T) { nil, // stdout nil, // stderr nil, // randSource - nil, 0, // walltime, walltimeResolution - nil, 0, // nanotime, nanotimeResolution + &wt, 1, // walltime, walltimeResolution + &nt, 1, // nanotime, nanotimeResolution + nil, // openedFiles ), }, { name: "WithEnv", - input: NewModuleConfig().WithEnv("a", "b"), + input: base.WithEnv("a", "b"), expected: requireSysContext(t, math.MaxUint32, // max nil, // args @@ -377,14 +390,15 @@ func TestModuleConfig_toSysContext(t *testing.T) { nil, // stdout nil, // stderr nil, // randSource - nil, 0, // walltime, walltimeResolution - nil, 0, // nanotime, nanotimeResolution + &wt, 1, // walltime, walltimeResolution + &nt, 1, // nanotime, nanotimeResolution + nil, // openedFiles ), }, { name: "WithEnv empty value", - input: NewModuleConfig().WithEnv("a", ""), + input: base.WithEnv("a", ""), expected: requireSysContext(t, math.MaxUint32, // max nil, // args @@ -393,14 +407,14 @@ func TestModuleConfig_toSysContext(t *testing.T) { nil, // stdout nil, // stderr nil, // randSource - nil, 0, // walltime, walltimeResolution - nil, 0, // nanotime, nanotimeResolution + &wt, 1, // walltime, walltimeResolution + &nt, 1, // nanotime, nanotimeResolution nil, // openedFiles ), }, { name: "WithEnv twice", - input: NewModuleConfig().WithEnv("a", "b").WithEnv("c", "de"), + input: base.WithEnv("a", "b").WithEnv("c", "de"), expected: requireSysContext(t, math.MaxUint32, // max nil, // args @@ -409,14 +423,15 @@ func TestModuleConfig_toSysContext(t *testing.T) { nil, // stdout nil, // stderr nil, // randSource - nil, 0, // walltime, walltimeResolution - nil, 0, // nanotime, nanotimeResolution + &wt, 1, // walltime, walltimeResolution + &nt, 1, // nanotime, nanotimeResolution + nil, // openedFiles ), }, { name: "WithEnv overwrites", - input: NewModuleConfig().WithEnv("a", "bc").WithEnv("c", "de").WithEnv("a", "de"), + input: base.WithEnv("a", "bc").WithEnv("c", "de").WithEnv("a", "de"), expected: requireSysContext(t, math.MaxUint32, // max nil, // args @@ -425,14 +440,15 @@ func TestModuleConfig_toSysContext(t *testing.T) { nil, // stdout nil, // stderr nil, // randSource - nil, 0, // walltime, walltimeResolution - nil, 0, // nanotime, nanotimeResolution + &wt, 1, // walltime, walltimeResolution + &nt, 1, // nanotime, nanotimeResolution + nil, // openedFiles ), }, { name: "WithEnv twice", - input: NewModuleConfig().WithEnv("a", "b").WithEnv("c", "de"), + input: base.WithEnv("a", "b").WithEnv("c", "de"), expected: requireSysContext(t, math.MaxUint32, // max nil, // args @@ -441,14 +457,15 @@ func TestModuleConfig_toSysContext(t *testing.T) { nil, // stdout nil, // stderr nil, // randSource - nil, 0, // walltime, walltimeResolution - nil, 0, // nanotime, nanotimeResolution + &wt, 1, // walltime, walltimeResolution + &nt, 1, // nanotime, nanotimeResolution + nil, // openedFiles ), }, { name: "WithFS", - input: NewModuleConfig().WithFS(testFS), + input: base.WithFS(testFS), expected: requireSysContext(t, math.MaxUint32, // max nil, // args @@ -457,8 +474,9 @@ func TestModuleConfig_toSysContext(t *testing.T) { nil, // stdout nil, // stderr nil, // randSource - nil, 0, // walltime, walltimeResolution - nil, 0, // nanotime, nanotimeResolution + &wt, 1, // walltime, walltimeResolution + &nt, 1, // nanotime, nanotimeResolution + map[uint32]*internalsys.FileEntry{ // openedFiles 3: {Path: "/", FS: testFS}, 4: {Path: ".", FS: testFS}, @@ -467,7 +485,7 @@ func TestModuleConfig_toSysContext(t *testing.T) { }, { name: "WithFS overwrites", - input: NewModuleConfig().WithFS(testFS).WithFS(testFS2), + input: base.WithFS(testFS).WithFS(testFS2), expected: requireSysContext(t, math.MaxUint32, // max nil, // args @@ -476,8 +494,8 @@ func TestModuleConfig_toSysContext(t *testing.T) { nil, // stdout nil, // stderr nil, // randSource - nil, 0, // walltime, walltimeResolution - nil, 0, // nanotime, nanotimeResolution + &wt, 1, // walltime, walltimeResolution + &nt, 1, // nanotime, nanotimeResolution map[uint32]*internalsys.FileEntry{ // openedFiles 3: {Path: "/", FS: testFS2}, 4: {Path: ".", FS: testFS2}, @@ -486,7 +504,7 @@ func TestModuleConfig_toSysContext(t *testing.T) { }, { name: "WithWorkDirFS", - input: NewModuleConfig().WithWorkDirFS(testFS), + input: base.WithWorkDirFS(testFS), expected: requireSysContext(t, math.MaxUint32, // max nil, // args @@ -495,8 +513,8 @@ func TestModuleConfig_toSysContext(t *testing.T) { nil, // stdout nil, // stderr nil, // randSource - nil, 0, // walltime, walltimeResolution - nil, 0, // nanotime, nanotimeResolution + &wt, 1, // walltime, walltimeResolution + &nt, 1, // nanotime, nanotimeResolution map[uint32]*internalsys.FileEntry{ // openedFiles 3: {Path: ".", FS: testFS}, }, @@ -504,7 +522,7 @@ func TestModuleConfig_toSysContext(t *testing.T) { }, { name: "WithFS and WithWorkDirFS", - input: NewModuleConfig().WithFS(testFS).WithWorkDirFS(testFS2), + input: base.WithFS(testFS).WithWorkDirFS(testFS2), expected: requireSysContext(t, math.MaxUint32, // max nil, // args @@ -513,8 +531,8 @@ func TestModuleConfig_toSysContext(t *testing.T) { nil, // stdout nil, // stderr nil, // randSource - nil, 0, // walltime, walltimeResolution - nil, 0, // nanotime, nanotimeResolution + &wt, 1, // walltime, walltimeResolution + &nt, 1, // nanotime, nanotimeResolution map[uint32]*internalsys.FileEntry{ // openedFiles 3: {Path: "/", FS: testFS}, 4: {Path: ".", FS: testFS2}, @@ -523,7 +541,7 @@ func TestModuleConfig_toSysContext(t *testing.T) { }, { name: "WithWorkDirFS and WithFS", - input: NewModuleConfig().WithWorkDirFS(testFS).WithFS(testFS2), + input: base.WithWorkDirFS(testFS).WithFS(testFS2), expected: requireSysContext(t, math.MaxUint32, // max nil, // args @@ -532,8 +550,8 @@ func TestModuleConfig_toSysContext(t *testing.T) { nil, // stdout nil, // stderr nil, // randSource - nil, 0, // walltime, walltimeResolution - nil, 0, // nanotime, nanotimeResolution + &wt, 1, // walltime, walltimeResolution + &nt, 1, // nanotime, nanotimeResolution map[uint32]*internalsys.FileEntry{ // openedFiles 3: {Path: ".", FS: testFS}, 4: {Path: "/", FS: testFS2}, @@ -748,6 +766,49 @@ func TestModuleConfig_toSysContext_Errors(t *testing.T) { }) } } +func TestModuleConfig_clone(t *testing.T) { + mc := NewModuleConfig().(*moduleConfig) + cloned := mc.clone() + + fs1 := fstest.MapFS{} + mc.fs.WithWorkDirFS(fs1) + mc.environKeys["2"] = 2 + + cloned.environKeys["1"] = 1 + + // Ensure the maps are not shared + require.Equal(t, map[string]int{"2": 2}, mc.environKeys) + require.Equal(t, map[string]int{"1": 1}, cloned.environKeys) + + // Ensure the fs is not shared + preopens, err := cloned.fs.Preopens() + require.NoError(t, err) + require.Equal(t, map[uint32]*internalsys.FileEntry{}, preopens) +} + +func TestCompiledCode_Close(t *testing.T) { + for _, ctx := range []context.Context{nil, testCtx} { // Ensure it doesn't crash on nil! + e := &mockEngine{name: "1", cachedModules: map[*wasm.Module]struct{}{}} + + var cs []*compiledModule + for i := 0; i < 10; i++ { + m := &wasm.Module{} + err := e.CompileModule(ctx, m) + require.NoError(t, err) + cs = append(cs, &compiledModule{module: m, compiledEngine: e}) + } + + // Before Close. + require.Equal(t, 10, len(e.cachedModules)) + + for _, c := range cs { + require.NoError(t, c.Close(ctx)) + } + + // After Close. + require.Zero(t, len(e.cachedModules)) + } +} // requireSysContext ensures wasm.NewContext doesn't return an error, which makes it usable in test matrices. func requireSysContext( @@ -776,27 +837,3 @@ func requireSysContext( require.NoError(t, err) return sysCtx } - -func TestCompiledCode_Close(t *testing.T) { - for _, ctx := range []context.Context{nil, testCtx} { // Ensure it doesn't crash on nil! - e := &mockEngine{name: "1", cachedModules: map[*wasm.Module]struct{}{}} - - var cs []*compiledModule - for i := 0; i < 10; i++ { - m := &wasm.Module{} - err := e.CompileModule(ctx, m) - require.NoError(t, err) - cs = append(cs, &compiledModule{module: m, compiledEngine: e}) - } - - // Before Close. - require.Equal(t, 10, len(e.cachedModules)) - - for _, c := range cs { - require.NoError(t, c.Close(ctx)) - } - - // After Close. - require.Zero(t, len(e.cachedModules)) - } -} diff --git a/internal/platform/time.go b/internal/platform/time.go index 64203b73..bde551a2 100644 --- a/internal/platform/time.go +++ b/internal/platform/time.go @@ -2,19 +2,39 @@ package platform import ( "context" + "sync/atomic" "time" + + "github.com/tetratelabs/wazero/sys" ) -const FakeEpochNanos = int64(1640995200000000000) // midnight UTC 2022-01-01 +const ( + ms = int64(time.Millisecond) + // FakeEpochNanos is midnight UTC 2022-01-01 and exposed for testing + FakeEpochNanos = 1640995200000 * ms +) -// FakeWalltime implements sys.Walltime with FakeEpochNanos. -func FakeWalltime(context.Context) (sec int64, nsec int32) { - return FakeEpochNanos / 1e9, int32(FakeEpochNanos % 1e9) +// NewFakeWalltime implements sys.Walltime with FakeEpochNanos that increases by 1ms each reading. +// See /RATIONALE.md +func NewFakeWalltime() *sys.Walltime { + // AddInt64 returns the new value. Adjust so the first reading will be FakeEpochNanos + t := FakeEpochNanos - ms + var wt sys.Walltime = func(context.Context) (sec int64, nsec int32) { + wt := atomic.AddInt64(&t, ms) + return wt / 1e9, int32(wt % 1e9) + } + return &wt } -// FakeNanotime implements sys.Nanotime with FakeEpochNanos. -func FakeNanotime(context.Context) int64 { - return FakeEpochNanos +// NewFakeNanotime implements sys.Nanotime that increases by 1ms each reading. +// See /RATIONALE.md +func NewFakeNanotime() *sys.Nanotime { + // AddInt64 returns the new value. Adjust so the first reading will be zero. + t := int64(0) - ms + var nt sys.Nanotime = func(context.Context) int64 { + return atomic.AddInt64(&t, ms) + } + return &nt } // Walltime implements sys.Walltime with time.Now. diff --git a/internal/platform/time_test.go b/internal/platform/time_test.go index 34358291..d040e753 100644 --- a/internal/platform/time_test.go +++ b/internal/platform/time_test.go @@ -9,6 +9,28 @@ import ( "github.com/tetratelabs/wazero/sys" ) +func Test_NewFakeWalltime(t *testing.T) { + wt := NewFakeWalltime() + + // Base should be the same as FakeEpochNanos + sec, nsec := (*wt)(context.Background()) + ft := time.UnixMicro(FakeEpochNanos / time.Microsecond.Nanoseconds()).UTC() + require.Equal(t, ft, time.Unix(sec, int64(nsec)).UTC()) + + // next reading should increase by 1ms + sec, nsec = (*wt)(context.Background()) + require.Equal(t, ft.Add(time.Millisecond), time.Unix(sec, int64(nsec)).UTC()) +} + +func Test_NewFakeNanotime(t *testing.T) { + nt := NewFakeNanotime() + + require.Equal(t, int64(0), (*nt)(context.Background())) + + // next reading should increase by 1ms + require.Equal(t, int64(time.Millisecond), (*nt)(context.Background())) +} + func Test_Walltime(t *testing.T) { now := time.Now().Unix() sec, nsec := Walltime(context.Background()) diff --git a/internal/sys/fs.go b/internal/sys/fs.go index 6c553343..59f12f8f 100644 --- a/internal/sys/fs.go +++ b/internal/sys/fs.go @@ -121,6 +121,20 @@ func NewFSConfig() *FSConfig { } } +// Clone makes a deep copy of this FS config. +func (c *FSConfig) Clone() *FSConfig { + ret := *c // copy except maps which share a ref + ret.preopens = make(map[uint32]*FileEntry, len(c.preopens)) + for key, value := range c.preopens { + ret.preopens[key] = value + } + ret.preopenPaths = make(map[string]uint32, len(c.preopenPaths)) + for key, value := range c.preopenPaths { + ret.preopenPaths[key] = value + } + return &ret +} + // setFS maps a path to a file-system. This is only used for base paths: "/" and ".". func (c *FSConfig) setFS(path string, fs fs.FS) { // Check to see if this key already exists and update it. @@ -135,15 +149,15 @@ func (c *FSConfig) setFS(path string, fs fs.FS) { } func (c *FSConfig) WithFS(fs fs.FS) *FSConfig { - ret := *c // copy + ret := c.Clone() ret.setFS("/", fs) - return &ret + return ret } func (c *FSConfig) WithWorkDirFS(fs fs.FS) *FSConfig { - ret := *c // copy + ret := c.Clone() ret.setFS(".", fs) - return &ret + return ret } func (c *FSConfig) Preopens() (map[uint32]*FileEntry, error) { diff --git a/internal/sys/fs_test.go b/internal/sys/fs_test.go index 5b85cbec..54046f21 100644 --- a/internal/sys/fs_test.go +++ b/internal/sys/fs_test.go @@ -57,3 +57,20 @@ func (f *testFile) Close() error { return f.closeErr } func (f *testFile) Stat() (fs.FileInfo, error) { return nil, nil } func (f *testFile) Read(_ []byte) (int, error) { return 0, nil } func (f *testFile) Seek(_ int64, _ int) (int64, error) { return 0, nil } + +func TestFSConfig_Clone(t *testing.T) { + fsc := NewFSConfig() + cloned := fsc.Clone() + + fsc.preopens[2] = nil + fsc.preopenPaths["2"] = 2 + + cloned.preopens[1] = nil + cloned.preopenPaths["1"] = 1 + + // Ensure the maps are not shared + require.Equal(t, map[uint32]*FileEntry{2: nil}, fsc.preopens) + require.Equal(t, map[string]uint32{"2": 2}, fsc.preopenPaths) + require.Equal(t, map[uint32]*FileEntry{1: nil}, cloned.preopens) + require.Equal(t, map[string]uint32{"1": 1}, cloned.preopenPaths) +} diff --git a/internal/sys/sys.go b/internal/sys/sys.go index f1143ef5..d06ae4a3 100644 --- a/internal/sys/sys.go +++ b/internal/sys/sys.go @@ -136,8 +136,6 @@ func DefaultContext() *Context { } var _ = DefaultContext() // Force panic on bug. -var wt sys.Walltime = platform.FakeWalltime -var nt sys.Nanotime = platform.FakeNanotime // NewContext is a factory function which helps avoid needing to know defaults or exporting all fields. // Note: max is exposed for testing. max is only used for env/args validation. @@ -192,7 +190,7 @@ func NewContext( sysCtx.walltime = walltime sysCtx.walltimeResolution = walltimeResolution } else { - sysCtx.walltime = &wt + sysCtx.walltime = platform.NewFakeWalltime() sysCtx.walltimeResolution = sys.ClockResolution(time.Microsecond.Nanoseconds()) } @@ -203,7 +201,7 @@ func NewContext( sysCtx.nanotime = nanotime sysCtx.nanotimeResolution = nanotimeResolution } else { - sysCtx.nanotime = &nt + sysCtx.nanotime = platform.NewFakeNanotime() sysCtx.nanotimeResolution = sys.ClockResolution(time.Nanosecond) } diff --git a/internal/sys/sys_test.go b/internal/sys/sys_test.go index 53fd38bc..94df7a8b 100644 --- a/internal/sys/sys_test.go +++ b/internal/sys/sys_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/tetratelabs/wazero/internal/platform" "github.com/tetratelabs/wazero/internal/testing/require" "github.com/tetratelabs/wazero/sys" ) @@ -33,9 +34,12 @@ func TestDefaultSysContext(t *testing.T) { require.Equal(t, eofReader{}, sysCtx.Stdin()) require.Equal(t, io.Discard, sysCtx.Stdout()) require.Equal(t, io.Discard, sysCtx.Stderr()) - require.Equal(t, &wt, sysCtx.walltime) // To compare functions, we can only compare pointers. + // To compare functions, we can only compare pointers, but the pointer will + // change. Hence, we have to compare the results instead. + sec, _ := sysCtx.Walltime(testCtx) + require.Equal(t, platform.FakeEpochNanos/time.Second.Nanoseconds(), sec) require.Equal(t, sys.ClockResolution(1_000), sysCtx.WalltimeResolution()) - require.Equal(t, &nt, sysCtx.nanotime) // To compare functions, we can only compare pointers. + require.Zero(t, sysCtx.Nanotime(testCtx)) // See above on functions. require.Equal(t, sys.ClockResolution(1), sysCtx.NanotimeResolution()) require.Equal(t, rand.Reader, sysCtx.RandSource()) require.Equal(t, NewFSContext(map[uint32]*FileEntry{}), sysCtx.FS()) @@ -172,12 +176,12 @@ func TestNewContext_Walltime(t *testing.T) { }{ { name: "ok", - time: &wt, + time: platform.NewFakeWalltime(), resolution: 3, }, { name: "invalid resolution", - time: &wt, + time: platform.NewFakeWalltime(), resolution: 0, expectedErr: "invalid Walltime resolution: 0", }, @@ -219,12 +223,12 @@ func TestNewContext_Nanotime(t *testing.T) { }{ { name: "ok", - time: &nt, + time: platform.NewFakeNanotime(), resolution: 3, }, { name: "invalid resolution", - time: &nt, + time: platform.NewFakeNanotime(), resolution: 0, expectedErr: "invalid Nanotime resolution: 0", }, diff --git a/wasi_snapshot_preview1/wasi_test.go b/wasi_snapshot_preview1/wasi_test.go index 9d9c640f..9c1c1725 100644 --- a/wasi_snapshot_preview1/wasi_test.go +++ b/wasi_snapshot_preview1/wasi_test.go @@ -19,7 +19,6 @@ import ( "github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero/api" "github.com/tetratelabs/wazero/experimental" - "github.com/tetratelabs/wazero/internal/platform" internalsys "github.com/tetratelabs/wazero/internal/sys" "github.com/tetratelabs/wazero/internal/testing/require" "github.com/tetratelabs/wazero/internal/wasm" @@ -520,90 +519,79 @@ func TestSnapshotPreview1_ClockResGet_Unsupported(t *testing.T) { } } -func TestSnapshotPreview1_ClockTimeGet_Realtime(t *testing.T) { - resultTimestamp := uint32(1) // arbitrary offset - expectedMemory := []byte{ - '?', // resultTimestamp is after this - 0x0, 0x0, 0x1f, 0xa6, 0x70, 0xfc, 0xc5, 0x16, // little endian-encoded epochNanos - '?', // stopped after encoding - } - - mod, fn := instantiateModule(testCtx, t, functionClockTimeGet, importClockTimeGet, nil) - defer mod.Close(testCtx) - - tests := []struct { - name string - invocation func() Errno - }{ - { - name: "wasi.ClockTimeGet", - invocation: func() Errno { - // invoke ClockTimeGet directly and check the memory side effects! - return a.ClockTimeGet(testCtx, mod, 0 /* REALTIME */, 0 /* TODO: precision */, resultTimestamp) - }, - }, - { - name: functionClockTimeGet, - invocation: func() Errno { - results, err := fn.Call(testCtx, 0 /* REALTIME */, 0 /* TODO: precision */, uint64(resultTimestamp)) - require.NoError(t, err) - errno := Errno(results[0]) // results[0] is the errno - return errno - }, - }, - } - - for _, tt := range tests { - tc := tt - t.Run(tc.name, func(t *testing.T) { - maskMemory(t, testCtx, mod, len(expectedMemory)) - - errno := tc.invocation() - require.Zero(t, errno, ErrnoName(errno)) - - actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(expectedMemory))) - require.True(t, ok) - require.Equal(t, expectedMemory, actual) - }) - } -} - -func TestSnapshotPreview1_ClockTimeGet_Monotonic(t *testing.T) { +func TestSnapshotPreview1_ClockTimeGet(t *testing.T) { resultTimestamp := uint32(1) // arbitrary offset mod, fn := instantiateModule(testCtx, t, functionClockTimeGet, importClockTimeGet, nil) defer mod.Close(testCtx) - tests := []struct { - name string - invocation func() Errno + clocks := []struct { + clock string + id uint32 + expectedMemory []byte }{ { - name: "wasi.ClockTimeGet", - invocation: func() Errno { - return a.ClockTimeGet(testCtx, mod, 1 /* MONOTONIC */, 0 /* TODO: precision */, resultTimestamp) + clock: "Realtime", + id: clockIDRealtime, + expectedMemory: []byte{ + '?', // resultTimestamp is after this + 0x0, 0x0, 0x1f, 0xa6, 0x70, 0xfc, 0xc5, 0x16, // little endian-encoded epochNanos + '?', // stopped after encoding }, }, { - name: functionClockTimeGet, - invocation: func() Errno { - results, err := fn.Call(testCtx, 1 /* MONOTONIC */, 0 /* TODO: precision */, uint64(resultTimestamp)) - require.NoError(t, err) - errno := Errno(results[0]) // results[0] is the errno - return errno + clock: "Monotonic", + id: clockIDMonotonic, + expectedMemory: []byte{ + '?', // resultTimestamp is after this + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // fake nanotime starts at zero + '?', // stopped after encoding }, }, } - for _, tt := range tests { - tc := tt - t.Run(tc.name, func(t *testing.T) { - errno := tc.invocation() - require.Zero(t, errno, ErrnoName(errno)) + for _, c := range clocks { + cc := c + t.Run(cc.clock, func(t *testing.T) { + tests := []struct { + name string + invocation func() Errno + }{ + { + name: "wasi.ClockTimeGet", + invocation: func() Errno { + return a.ClockTimeGet(testCtx, mod, cc.id, 0 /* TODO: precision */, resultTimestamp) + }, + }, + { + name: functionClockTimeGet, + invocation: func() Errno { + results, err := fn.Call(testCtx, uint64(cc.id), 0 /* TODO: precision */, uint64(resultTimestamp)) + require.NoError(t, err) + errno := Errno(results[0]) // results[0] is the errno + return errno + }, + }, + } - tick, ok := mod.Memory().ReadUint64Le(testCtx, resultTimestamp) - require.True(t, ok) - require.Equal(t, uint64(platform.FakeEpochNanos), tick) + for _, tt := range tests { + tc := tt + t.Run(tc.name, func(t *testing.T) { + // Reset the fake clock + sysCtx, err := newSysContext(nil, nil, nil) + require.NoError(t, err) + mod.(*wasm.CallContext).Sys = sysCtx + + maskMemory(t, testCtx, mod, len(cc.expectedMemory)) + + errno := tc.invocation() + require.Zero(t, errno, ErrnoName(errno)) + + actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(cc.expectedMemory))) + require.True(t, ok) + require.Equal(t, cc.expectedMemory, actual) + }) + } }) } }