github.com/tetratelabs/wazero@v1.2.1/internal/integration_test/engine/adhoc_test.go (about) 1 package adhoc 2 3 import ( 4 "context" 5 _ "embed" 6 "math" 7 "strconv" 8 "testing" 9 "time" 10 "unsafe" 11 12 "github.com/tetratelabs/wazero" 13 "github.com/tetratelabs/wazero/api" 14 "github.com/tetratelabs/wazero/internal/platform" 15 "github.com/tetratelabs/wazero/internal/testing/binaryencoding" 16 "github.com/tetratelabs/wazero/internal/testing/proxy" 17 "github.com/tetratelabs/wazero/internal/testing/require" 18 "github.com/tetratelabs/wazero/internal/wasm" 19 "github.com/tetratelabs/wazero/sys" 20 ) 21 22 // testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors. 23 var testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary") 24 25 const ( 26 i32, i64 = wasm.ValueTypeI32, wasm.ValueTypeI64 27 ) 28 29 var memoryCapacityPages = uint32(2) 30 31 var moduleConfig = wazero.NewModuleConfig() 32 33 var tests = map[string]func(t *testing.T, r wazero.Runtime){ 34 "huge stack": testHugeStack, 35 "unreachable": testUnreachable, 36 "recursive entry": testRecursiveEntry, 37 "host func memory": testHostFuncMemory, 38 "host function with context parameter": testHostFunctionContextParameter, 39 "host function with nested context": testNestedGoContext, 40 "host function with numeric parameter": testHostFunctionNumericParameter, 41 "close module with in-flight calls": testCloseInFlight, 42 "multiple instantiation from same source": testMultipleInstantiation, 43 "exported function that grows memory": testMemOps, 44 "import functions with reference type in signature": testReftypeImports, 45 "overflow integer addition": testOverflow, 46 "un-signed extend global": testGlobalExtend, 47 "user-defined primitive in host func": testUserDefinedPrimitiveHostFunc, 48 "ensures invocations terminate on module close": testEnsureTerminationOnClose, 49 "call host function indirectly": callHostFunctionIndirect, 50 } 51 52 func TestEngineCompiler(t *testing.T) { 53 if !platform.CompilerSupported() { 54 t.Skip() 55 } 56 runAllTests(t, tests, wazero.NewRuntimeConfigCompiler().WithCloseOnContextDone(true)) 57 } 58 59 func TestEngineInterpreter(t *testing.T) { 60 runAllTests(t, tests, wazero.NewRuntimeConfigInterpreter().WithCloseOnContextDone(true)) 61 } 62 63 func runAllTests(t *testing.T, tests map[string]func(t *testing.T, r wazero.Runtime), config wazero.RuntimeConfig) { 64 for name, testf := range tests { 65 name := name // pin 66 testf := testf // pin 67 t.Run(name, func(t *testing.T) { 68 t.Parallel() 69 testf(t, wazero.NewRuntimeWithConfig(testCtx, config)) 70 }) 71 } 72 } 73 74 var ( 75 //go:embed testdata/unreachable.wasm 76 unreachableWasm []byte 77 //go:embed testdata/recursive.wasm 78 recursiveWasm []byte 79 //go:embed testdata/host_memory.wasm 80 hostMemoryWasm []byte 81 //go:embed testdata/hugestack.wasm 82 hugestackWasm []byte 83 //go:embed testdata/memory.wasm 84 memoryWasm []byte 85 //go:embed testdata/reftype_imports.wasm 86 reftypeImportsWasm []byte 87 //go:embed testdata/overflow.wasm 88 overflowWasm []byte 89 //go:embed testdata/global_extend.wasm 90 globalExtendWasm []byte 91 //go:embed testdata/infinite_loop.wasm 92 infiniteLoopWasm []byte 93 ) 94 95 func testEnsureTerminationOnClose(t *testing.T, r wazero.Runtime) { 96 compiled, err := r.CompileModule(context.Background(), infiniteLoopWasm) 97 require.NoError(t, err) 98 99 newInfiniteLoopFn := func(t *testing.T) (m api.Module, infinite api.Function) { 100 var err error 101 m, err = r.InstantiateModule(context.Background(), compiled, wazero.NewModuleConfig().WithName(t.Name())) 102 require.NoError(t, err) 103 infinite = m.ExportedFunction("infinite_loop") 104 require.NotNil(t, infinite) 105 return 106 } 107 108 t.Run("context cancel", func(t *testing.T) { 109 _, infinite := newInfiniteLoopFn(t) 110 ctx, cancel := context.WithCancel(context.Background()) 111 go func() { 112 time.Sleep(time.Second) 113 cancel() 114 }() 115 _, err = infinite.Call(ctx) 116 require.Error(t, err) 117 require.Contains(t, err.Error(), "module closed with context canceled") 118 }) 119 120 t.Run("context cancel in advance", func(t *testing.T) { 121 _, infinite := newInfiniteLoopFn(t) 122 ctx, cancel := context.WithCancel(context.Background()) 123 cancel() 124 _, err = infinite.Call(ctx) 125 require.Error(t, err) 126 require.Contains(t, err.Error(), "module closed with context canceled") 127 }) 128 129 t.Run("context timeout", func(t *testing.T) { 130 _, infinite := newInfiniteLoopFn(t) 131 ctx, cancel := context.WithTimeout(context.Background(), time.Second) 132 defer cancel() 133 _, err = infinite.Call(ctx) 134 require.Error(t, err) 135 require.Contains(t, err.Error(), "module closed with context deadline exceeded") 136 }) 137 138 t.Run("explicit close of module", func(t *testing.T) { 139 module, infinite := newInfiniteLoopFn(t) 140 go func() { 141 time.Sleep(time.Second) 142 require.NoError(t, module.CloseWithExitCode(context.Background(), 2)) 143 }() 144 _, err = infinite.Call(context.Background()) 145 require.Error(t, err) 146 require.Contains(t, err.Error(), "module closed with exit_code(2)") 147 }) 148 } 149 150 func testUserDefinedPrimitiveHostFunc(t *testing.T, r wazero.Runtime) { 151 type u32 uint32 152 type u64 uint64 153 type f32 float32 154 type f64 float64 155 156 const fn = "fn" 157 hostCompiled, err := r.NewHostModuleBuilder("host").NewFunctionBuilder(). 158 WithFunc(func(u1 u32, u2 u64, f1 f32, f2 f64) u64 { 159 return u64(u1) + u2 + u64(math.Float32bits(float32(f1))) + u64(math.Float64bits(float64(f2))) 160 }).Export(fn).Compile(testCtx) 161 require.NoError(t, err) 162 163 _, err = r.InstantiateModule(testCtx, hostCompiled, wazero.NewModuleConfig()) 164 require.NoError(t, err) 165 166 proxyBin := proxy.NewModuleBinary("host", hostCompiled) 167 168 mod, err := r.Instantiate(testCtx, proxyBin) 169 require.NoError(t, err) 170 171 f := mod.ExportedFunction(fn) 172 require.NotNil(t, f) 173 174 const u1, u2, f1, f2 = 1, 2, float32(1234.123), 5431.123 175 res, err := f.Call(context.Background(), uint64(u1), uint64(u2), uint64(math.Float32bits(f1)), math.Float64bits(f2)) 176 require.NoError(t, err) 177 require.Equal(t, res[0], uint64(u1)+uint64(u2)+uint64(math.Float32bits(f1))+math.Float64bits(f2)) 178 } 179 180 func testReftypeImports(t *testing.T, r wazero.Runtime) { 181 type dog struct { 182 name string 183 } 184 185 hostObj := &dog{name: "hello"} 186 host, err := r.NewHostModuleBuilder("host"). 187 NewFunctionBuilder(). 188 WithFunc(func(ctx context.Context, externrefFromRefNull uintptr) uintptr { 189 require.Zero(t, externrefFromRefNull) 190 return uintptr(unsafe.Pointer(hostObj)) 191 }). 192 Export("externref"). 193 Instantiate(testCtx) 194 require.NoError(t, err) 195 defer host.Close(testCtx) 196 197 module, err := r.Instantiate(testCtx, reftypeImportsWasm) 198 require.NoError(t, err) 199 defer module.Close(testCtx) 200 201 actual, err := module.ExportedFunction("get_externref_by_host").Call(testCtx) 202 require.NoError(t, err) 203 204 // Verifies that the returned raw uintptr is the same as the one for the host object. 205 require.Equal(t, uintptr(unsafe.Pointer(hostObj)), uintptr(actual[0])) 206 } 207 208 func testHugeStack(t *testing.T, r wazero.Runtime) { 209 module, err := r.Instantiate(testCtx, hugestackWasm) 210 require.NoError(t, err) 211 defer module.Close(testCtx) 212 213 fn := module.ExportedFunction("main") 214 require.NotNil(t, fn) 215 216 res, err := fn.Call(testCtx, 0, 0, 0, 0, 0, 0) // params ignored by wasm 217 require.NoError(t, err) 218 219 const resultNumInUint64 = 180 220 require.Equal(t, resultNumInUint64, len(res)) 221 for i := uint64(1); i <= resultNumInUint64; i++ { 222 require.Equal(t, i, res[i-1]) 223 } 224 } 225 226 // testOverflow ensures that adding one into the maximum integer results in the 227 // minimum one. See #636. 228 func testOverflow(t *testing.T, r wazero.Runtime) { 229 module, err := r.Instantiate(testCtx, overflowWasm) 230 require.NoError(t, err) 231 defer module.Close(testCtx) 232 233 for _, name := range []string{"i32", "i64"} { 234 i32 := module.ExportedFunction(name) 235 require.NotNil(t, i32) 236 237 res, err := i32.Call(testCtx) 238 require.NoError(t, err) 239 240 require.Equal(t, uint64(1), res[0]) 241 } 242 } 243 244 // testGlobalExtend ensures that un-signed extension of i32 globals must be zero extended. See #656. 245 func testGlobalExtend(t *testing.T, r wazero.Runtime) { 246 module, err := r.Instantiate(testCtx, globalExtendWasm) 247 require.NoError(t, err) 248 defer module.Close(testCtx) 249 250 extend := module.ExportedFunction("extend") 251 require.NotNil(t, extend) 252 253 res, err := extend.Call(testCtx) 254 require.NoError(t, err) 255 256 require.Equal(t, uint64(0xffff_ffff), res[0]) 257 } 258 259 func testUnreachable(t *testing.T, r wazero.Runtime) { 260 callUnreachable := func() { 261 panic("panic in host function") 262 } 263 264 _, err := r.NewHostModuleBuilder("host"). 265 NewFunctionBuilder().WithFunc(callUnreachable).Export("cause_unreachable"). 266 Instantiate(testCtx) 267 require.NoError(t, err) 268 269 module, err := r.Instantiate(testCtx, unreachableWasm) 270 require.NoError(t, err) 271 defer module.Close(testCtx) 272 273 _, err = module.ExportedFunction("main").Call(testCtx) 274 exp := `panic in host function (recovered by wazero) 275 wasm stack trace: 276 host.cause_unreachable() 277 .two() 278 .one() 279 .main()` 280 require.Equal(t, exp, err.Error()) 281 } 282 283 func testRecursiveEntry(t *testing.T, r wazero.Runtime) { 284 hostfunc := func(ctx context.Context, mod api.Module) { 285 _, err := mod.ExportedFunction("called_by_host_func").Call(testCtx) 286 require.NoError(t, err) 287 } 288 289 _, err := r.NewHostModuleBuilder("env"). 290 NewFunctionBuilder().WithFunc(hostfunc).Export("host_func"). 291 Instantiate(testCtx) 292 require.NoError(t, err) 293 294 module, err := r.Instantiate(testCtx, recursiveWasm) 295 require.NoError(t, err) 296 defer module.Close(testCtx) 297 298 _, err = module.ExportedFunction("main").Call(testCtx, 1) 299 require.NoError(t, err) 300 } 301 302 // testHostFuncMemory ensures that host functions can see the callers' memory 303 func testHostFuncMemory(t *testing.T, r wazero.Runtime) { 304 var memory *wasm.MemoryInstance 305 storeInt := func(ctx context.Context, m api.Module, offset uint32, val uint64) uint32 { 306 if !m.Memory().WriteUint64Le(offset, val) { 307 return 1 308 } 309 // sneak a reference to the memory, so we can check it later 310 memory = m.Memory().(*wasm.MemoryInstance) 311 return 0 312 } 313 314 host, err := r.NewHostModuleBuilder("host"). 315 NewFunctionBuilder().WithFunc(storeInt).Export("store_int"). 316 Instantiate(testCtx) 317 require.NoError(t, err) 318 defer host.Close(testCtx) 319 320 module, err := r.Instantiate(testCtx, hostMemoryWasm) 321 require.NoError(t, err) 322 defer module.Close(testCtx) 323 324 // Call store_int and ensure it didn't return an error code. 325 fn := module.ExportedFunction("store_int") 326 results, err := fn.Call(testCtx, 1, math.MaxUint64) 327 require.NoError(t, err) 328 require.Equal(t, uint64(0), results[0]) 329 330 // Since offset=1 and val=math.MaxUint64, we expect to have written exactly 8 bytes, with all bits set, at index 1. 331 require.Equal(t, []byte{0x0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x0}, memory.Buffer[0:10]) 332 } 333 334 // testNestedGoContext ensures context is updated when a function calls another. 335 func testNestedGoContext(t *testing.T, r wazero.Runtime) { 336 nestedCtx := context.WithValue(context.Background(), struct{}{}, "nested") 337 338 importedName := t.Name() + "-imported" 339 importingName := t.Name() + "-importing" 340 341 var importing api.Module 342 343 imported, err := r.NewHostModuleBuilder(importedName). 344 NewFunctionBuilder(). 345 WithFunc(func(ctx context.Context, p uint32) uint32 { 346 // We expect the initial context, testCtx, to be overwritten by "outer" when it called this. 347 require.Equal(t, nestedCtx, ctx) 348 return p + 1 349 }). 350 Export("inner"). 351 NewFunctionBuilder(). 352 WithFunc(func(ctx context.Context, module api.Module, p uint32) uint32 { 353 require.Equal(t, testCtx, ctx) 354 results, err := module.ExportedFunction("inner").Call(nestedCtx, uint64(p)) 355 require.NoError(t, err) 356 return uint32(results[0]) + 1 357 }). 358 Export("outer"). 359 Instantiate(testCtx) 360 require.NoError(t, err) 361 defer imported.Close(testCtx) 362 363 // Instantiate a module that uses Wasm code to call the host function. 364 importing, err = r.Instantiate(testCtx, callOuterInnerWasm(t, importedName, importingName)) 365 require.NoError(t, err) 366 defer importing.Close(testCtx) 367 368 input := uint64(math.MaxUint32 - 2) // We expect two calls where each increment by one. 369 results, err := importing.ExportedFunction("call->outer").Call(testCtx, input) 370 require.NoError(t, err) 371 require.Equal(t, uint64(math.MaxUint32), results[0]) 372 } 373 374 // testHostFunctionContextParameter ensures arg0 is optionally a context. 375 func testHostFunctionContextParameter(t *testing.T, r wazero.Runtime) { 376 importedName := t.Name() + "-imported" 377 importingName := t.Name() + "-importing" 378 379 var importing api.Module 380 fns := map[string]interface{}{ 381 "ctx": func(ctx context.Context, p uint32) uint32 { 382 require.Equal(t, testCtx, ctx) 383 return p + 1 384 }, 385 "ctx mod": func(ctx context.Context, module api.Module, p uint32) uint32 { 386 require.Equal(t, importing, module) 387 return p + 1 388 }, 389 } 390 391 for test := range fns { 392 t.Run(test, func(t *testing.T) { 393 imported, err := r.NewHostModuleBuilder(importedName). 394 NewFunctionBuilder().WithFunc(fns[test]).Export("return_input"). 395 Instantiate(testCtx) 396 require.NoError(t, err) 397 defer imported.Close(testCtx) 398 399 // Instantiate a module that uses Wasm code to call the host function. 400 importing, err = r.Instantiate(testCtx, 401 callReturnImportWasm(t, importedName, importingName, i32)) 402 require.NoError(t, err) 403 defer importing.Close(testCtx) 404 405 results, err := importing.ExportedFunction("call_return_input").Call(testCtx, math.MaxUint32-1) 406 require.NoError(t, err) 407 require.Equal(t, uint64(math.MaxUint32), results[0]) 408 }) 409 } 410 } 411 412 // testHostFunctionNumericParameter ensures numeric parameters aren't corrupted 413 func testHostFunctionNumericParameter(t *testing.T, r wazero.Runtime) { 414 importedName := t.Name() + "-imported" 415 importingName := t.Name() + "-importing" 416 417 fns := map[string]interface{}{ 418 "i32": func(ctx context.Context, p uint32) uint32 { 419 return p + 1 420 }, 421 "i64": func(ctx context.Context, p uint64) uint64 { 422 return p + 1 423 }, 424 "f32": func(ctx context.Context, p float32) float32 { 425 return p + 1 426 }, 427 "f64": func(ctx context.Context, p float64) float64 { 428 return p + 1 429 }, 430 } 431 432 for _, test := range []struct { 433 name string 434 vt wasm.ValueType 435 input, expected uint64 436 }{ 437 { 438 name: "i32", 439 vt: i32, 440 input: math.MaxUint32 - 1, 441 expected: math.MaxUint32, 442 }, 443 { 444 name: "i64", 445 vt: i64, 446 input: math.MaxUint64 - 1, 447 expected: math.MaxUint64, 448 }, 449 { 450 name: "f32", 451 vt: wasm.ValueTypeF32, 452 input: api.EncodeF32(math.MaxFloat32 - 1), 453 expected: api.EncodeF32(math.MaxFloat32), 454 }, 455 { 456 name: "f64", 457 vt: wasm.ValueTypeF64, 458 input: api.EncodeF64(math.MaxFloat64 - 1), 459 expected: api.EncodeF64(math.MaxFloat64), 460 }, 461 } { 462 t.Run(test.name, func(t *testing.T) { 463 imported, err := r.NewHostModuleBuilder(importedName). 464 NewFunctionBuilder().WithFunc(fns[test.name]).Export("return_input"). 465 Instantiate(testCtx) 466 require.NoError(t, err) 467 defer imported.Close(testCtx) 468 469 // Instantiate a module that uses Wasm code to call the host function. 470 importing, err := r.Instantiate(testCtx, 471 callReturnImportWasm(t, importedName, importingName, test.vt)) 472 require.NoError(t, err) 473 defer importing.Close(testCtx) 474 475 results, err := importing.ExportedFunction("call_return_input").Call(testCtx, test.input) 476 require.NoError(t, err) 477 require.Equal(t, test.expected, results[0]) 478 }) 479 } 480 } 481 482 func callHostFunctionIndirect(t *testing.T, r wazero.Runtime) { 483 // With the following call graph, 484 // originWasmModule -- call --> importingWasmModule -- call --> hostModule 485 // this ensures that hostModule's hostFn only has access importingWasmModule, not originWasmModule. 486 487 const hostModule, importingWasmModule, originWasmModule = "host", "importing", "origin" 488 const hostFn, importingWasmModuleFn, originModuleFn = "host_fn", "call_host_func", "origin" 489 importingModule := &wasm.Module{ 490 TypeSection: []wasm.FunctionType{{Params: []wasm.ValueType{}, Results: []wasm.ValueType{}}}, 491 ImportSection: []wasm.Import{{Module: hostModule, Name: hostFn, Type: wasm.ExternTypeFunc, DescFunc: 0}}, 492 FunctionSection: []wasm.Index{0}, 493 ExportSection: []wasm.Export{{Name: importingWasmModuleFn, Type: wasm.ExternTypeFunc, Index: 1}}, 494 CodeSection: []wasm.Code{{Body: []byte{wasm.OpcodeCall, 0, wasm.OpcodeEnd}}}, 495 NameSection: &wasm.NameSection{ModuleName: importingWasmModule}, 496 } 497 498 originModule := &wasm.Module{ 499 TypeSection: []wasm.FunctionType{{Params: []wasm.ValueType{}, Results: []wasm.ValueType{}}}, 500 ImportSection: []wasm.Import{{Module: importingWasmModule, Name: importingWasmModuleFn, Type: wasm.ExternTypeFunc, DescFunc: 0}}, 501 FunctionSection: []wasm.Index{0}, 502 ExportSection: []wasm.Export{{Name: "origin", Type: wasm.ExternTypeFunc, Index: 1}}, 503 CodeSection: []wasm.Code{{Body: []byte{wasm.OpcodeCall, 0, wasm.OpcodeEnd}}}, 504 NameSection: &wasm.NameSection{ModuleName: originWasmModule}, 505 } 506 507 require.NoError(t, importingModule.Validate(api.CoreFeaturesV2)) 508 require.NoError(t, originModule.Validate(api.CoreFeaturesV2)) 509 importingModuleBytes := binaryencoding.EncodeModule(importingModule) 510 originModuleBytes := binaryencoding.EncodeModule(originModule) 511 512 var originInst, importingInst api.Module 513 _, err := r.NewHostModuleBuilder(hostModule). 514 NewFunctionBuilder(). 515 WithFunc(func(ctx context.Context, mod api.Module) { 516 // Module must be the caller (importing module), not the origin. 517 require.Equal(t, mod, importingInst) 518 require.NotEqual(t, mod, originInst) 519 // Name must be the caller, not origin. 520 require.Equal(t, importingWasmModule, mod.Name()) 521 }). 522 Export(hostFn). 523 Instantiate(testCtx) 524 require.NoError(t, err) 525 526 importingInst, err = r.Instantiate(testCtx, importingModuleBytes) 527 require.NoError(t, err) 528 originInst, err = r.Instantiate(testCtx, originModuleBytes) 529 require.NoError(t, err) 530 531 originFn := originInst.ExportedFunction(originModuleFn) 532 require.NotNil(t, originFn) 533 534 _, err = originFn.Call(testCtx) 535 require.NoError(t, err) 536 } 537 538 func callReturnImportWasm(t *testing.T, importedModule, importingModule string, vt wasm.ValueType) []byte { 539 // test an imported function by re-exporting it 540 module := &wasm.Module{ 541 TypeSection: []wasm.FunctionType{{Params: []wasm.ValueType{vt}, Results: []wasm.ValueType{vt}}}, 542 // (import "%[2]s" "return_input" (func $return_input (param i32) (result i32))) 543 ImportSection: []wasm.Import{ 544 {Module: importedModule, Name: "return_input", Type: wasm.ExternTypeFunc, DescFunc: 0}, 545 }, 546 FunctionSection: []wasm.Index{0}, 547 ExportSection: []wasm.Export{ 548 // (export "return_input" (func $return_input)) 549 {Name: "return_input", Type: wasm.ExternTypeFunc, Index: 0}, 550 // (export "call_return_input" (func $call_return_input)) 551 {Name: "call_return_input", Type: wasm.ExternTypeFunc, Index: 1}, 552 }, 553 // (func $call_return_input (param i32) (result i32) local.get 0 call $return_input) 554 CodeSection: []wasm.Code{ 555 {Body: []byte{wasm.OpcodeLocalGet, 0, wasm.OpcodeCall, 0, wasm.OpcodeEnd}}, 556 }, 557 NameSection: &wasm.NameSection{ 558 ModuleName: importingModule, 559 FunctionNames: wasm.NameMap{ 560 {Index: 0, Name: "return_input"}, 561 {Index: 1, Name: "call_return_input"}, 562 }, 563 }, 564 } 565 require.NoError(t, module.Validate(api.CoreFeaturesV2)) 566 return binaryencoding.EncodeModule(module) 567 } 568 569 func callOuterInnerWasm(t *testing.T, importedModule, importingModule string) []byte { 570 module := &wasm.Module{ 571 TypeSection: []wasm.FunctionType{{Params: []wasm.ValueType{i32}, Results: []wasm.ValueType{i32}}}, 572 // (import "%[2]s" "outer" (func $outer (param i32) (result i32))) 573 // (import "%[2]s" "inner" (func $inner (param i32) (result i32))) 574 ImportSection: []wasm.Import{ 575 {Module: importedModule, Name: "outer", Type: wasm.ExternTypeFunc, DescFunc: 0}, 576 {Module: importedModule, Name: "inner", Type: wasm.ExternTypeFunc, DescFunc: 0}, 577 }, 578 FunctionSection: []wasm.Index{0, 0}, 579 ExportSection: []wasm.Export{ 580 // (export "call->outer" (func $call_outer)) 581 {Name: "call->outer", Type: wasm.ExternTypeFunc, Index: 2}, 582 // (export "inner" (func $call_inner)) 583 {Name: "inner", Type: wasm.ExternTypeFunc, Index: 3}, 584 }, 585 CodeSection: []wasm.Code{ 586 // (func $call_outer (param i32) (result i32) local.get 0 call $outer) 587 {Body: []byte{wasm.OpcodeLocalGet, 0, wasm.OpcodeCall, 0, wasm.OpcodeEnd}}, 588 // (func $call_inner (param i32) (result i32) local.get 0 call $inner) 589 {Body: []byte{wasm.OpcodeLocalGet, 0, wasm.OpcodeCall, 1, wasm.OpcodeEnd}}, 590 }, 591 NameSection: &wasm.NameSection{ 592 ModuleName: importingModule, 593 FunctionNames: wasm.NameMap{ 594 {Index: 0, Name: "outer"}, 595 {Index: 1, Name: "inner"}, 596 {Index: 2, Name: "call_outer"}, 597 {Index: 3, Name: "call_inner"}, 598 }, 599 }, 600 } 601 require.NoError(t, module.Validate(api.CoreFeaturesV2)) 602 return binaryencoding.EncodeModule(module) 603 } 604 605 func testCloseInFlight(t *testing.T, r wazero.Runtime) { 606 tests := []struct { 607 name, function string 608 closeImporting, closeImported uint32 609 closeImportingCode, closeImportedCode bool 610 }{ 611 { // e.g. WASI proc_exit or AssemblyScript abort handler. 612 name: "importing", 613 function: "call_return_input", 614 closeImporting: 1, 615 }, 616 // TODO: A module that re-exports a function (ex "return_input") can call it after it is closed! 617 { // e.g. A function that stops the runtime. 618 name: "both", 619 function: "call_return_input", 620 closeImporting: 1, 621 closeImported: 2, 622 }, 623 { // e.g. WASI proc_exit or AssemblyScript abort handler. 624 name: "importing", 625 function: "call_return_input", 626 closeImporting: 1, 627 closeImportedCode: true, 628 }, 629 { // e.g. WASI proc_exit or AssemblyScript abort handler. 630 name: "importing", 631 function: "call_return_input", 632 closeImporting: 1, 633 closeImportedCode: true, 634 closeImportingCode: true, 635 }, 636 // TODO: A module that re-exports a function (ex "return_input") can call it after it is closed! 637 { // e.g. A function that stops the runtime. 638 name: "both", 639 function: "call_return_input", 640 closeImporting: 1, 641 closeImported: 2, 642 closeImportingCode: true, 643 }, 644 } 645 for _, tt := range tests { 646 tc := tt 647 648 t.Run(tc.name, func(t *testing.T) { 649 var importingCode, importedCode wazero.CompiledModule 650 var imported, importing api.Module 651 var err error 652 closeAndReturn := func(ctx context.Context, x uint32) uint32 { 653 if tc.closeImporting != 0 { 654 require.NoError(t, importing.CloseWithExitCode(ctx, tc.closeImporting)) 655 } 656 if tc.closeImported != 0 { 657 require.NoError(t, imported.CloseWithExitCode(ctx, tc.closeImported)) 658 } 659 if tc.closeImportedCode { 660 importedCode.Close(testCtx) 661 } 662 if tc.closeImportingCode { 663 importingCode.Close(testCtx) 664 } 665 return x 666 } 667 668 // Create the host module, which exports the function that closes the importing module. 669 importedCode, err = r.NewHostModuleBuilder(t.Name() + "-imported"). 670 NewFunctionBuilder().WithFunc(closeAndReturn).Export("return_input"). 671 Compile(testCtx) 672 require.NoError(t, err) 673 674 imported, err = r.InstantiateModule(testCtx, importedCode, moduleConfig) 675 require.NoError(t, err) 676 defer imported.Close(testCtx) 677 678 // Import that module. 679 binary := callReturnImportWasm(t, imported.Name(), t.Name()+"-importing", i32) 680 importingCode, err = r.CompileModule(testCtx, binary) 681 require.NoError(t, err) 682 683 importing, err = r.InstantiateModule(testCtx, importingCode, moduleConfig) 684 require.NoError(t, err) 685 defer importing.Close(testCtx) 686 687 var expectedErr error 688 if tc.closeImported != 0 && tc.closeImporting != 0 { 689 // When both modules are closed, importing is the better one to choose in the error message. 690 expectedErr = sys.NewExitError(tc.closeImporting) 691 } else if tc.closeImported != 0 { 692 expectedErr = sys.NewExitError(tc.closeImported) 693 } else if tc.closeImporting != 0 { 694 expectedErr = sys.NewExitError(tc.closeImporting) 695 } else { 696 t.Fatal("invalid test case") 697 } 698 699 // Functions that return after being closed should have an exit error. 700 _, err = importing.ExportedFunction(tc.function).Call(testCtx, 5) 701 require.Equal(t, expectedErr, err) 702 }) 703 } 704 } 705 706 func testMemOps(t *testing.T, r wazero.Runtime) { 707 // Instantiate a module that manages its memory 708 mod, err := r.Instantiate(testCtx, memoryWasm) 709 require.NoError(t, err) 710 defer mod.Close(testCtx) 711 712 // Check the export worked 713 require.Equal(t, mod.Memory(), mod.ExportedMemory("memory")) 714 memory := mod.Memory() 715 716 sizeFn, storeFn, growFn := mod.ExportedFunction("size"), mod.ExportedFunction("store"), mod.ExportedFunction("grow") 717 718 // Check the size command worked 719 results, err := sizeFn.Call(testCtx) 720 require.NoError(t, err) 721 require.Zero(t, results[0]) 722 require.Zero(t, memory.Size()) 723 724 // Any offset should be out of bounds error even when it is less than memory capacity(=memoryCapacityPages). 725 _, err = storeFn.Call(testCtx, wasm.MemoryPagesToBytesNum(memoryCapacityPages)-8) 726 require.Error(t, err) // Out of bounds error. 727 728 // Try to grow the memory by one page 729 results, err = growFn.Call(testCtx, 1) 730 require.NoError(t, err) 731 require.Zero(t, results[0]) // should succeed and return the old size in pages. 732 733 // Any offset larger than the current size should be out of bounds error even when it is less than memory capacity. 734 _, err = storeFn.Call(testCtx, wasm.MemoryPagesToBytesNum(memoryCapacityPages)-8) 735 require.Error(t, err) // Out of bounds error. 736 737 // Check the size command works! 738 results, err = sizeFn.Call(testCtx) 739 require.NoError(t, err) 740 require.Equal(t, uint64(1), results[0]) // 1 page 741 require.Equal(t, uint32(65536), memory.Size()) // 64KB 742 743 // Grow again so that the memory size matches memory capacity. 744 results, err = growFn.Call(testCtx, 1) 745 require.NoError(t, err) 746 require.Equal(t, uint64(1), results[0]) 747 748 // Verify the size matches cap. 749 results, err = sizeFn.Call(testCtx) 750 require.NoError(t, err) 751 require.Equal(t, uint64(memoryCapacityPages), results[0]) 752 753 // Now the store instruction at the memory capcity bound should succeed. 754 _, err = storeFn.Call(testCtx, wasm.MemoryPagesToBytesNum(memoryCapacityPages)-8) // i64.store needs 8 bytes from offset. 755 require.NoError(t, err) 756 } 757 758 func testMultipleInstantiation(t *testing.T, r wazero.Runtime) { 759 bin := binaryencoding.EncodeModule(&wasm.Module{ 760 TypeSection: []wasm.FunctionType{{}}, 761 FunctionSection: []wasm.Index{0}, 762 MemorySection: &wasm.Memory{Min: 1, Cap: 1, Max: 1, IsMaxEncoded: true}, 763 CodeSection: []wasm.Code{{ 764 Body: []byte{ 765 wasm.OpcodeI32Const, 1, // i32.const 1 ;; memory offset 766 wasm.OpcodeI64Const, 0xe8, 0x7, // i64.const 1000 ;; expected value 767 wasm.OpcodeI64Store, 0x3, 0x0, // i64.store 768 wasm.OpcodeEnd, 769 }, 770 }}, 771 ExportSection: []wasm.Export{{Name: "store"}}, 772 }) 773 compiled, err := r.CompileModule(testCtx, bin) 774 require.NoError(t, err) 775 defer compiled.Close(testCtx) 776 777 // Instantiate multiple modules with the same source (*CompiledModule). 778 for i := 0; i < 100; i++ { 779 module, err := r.InstantiateModule(testCtx, compiled, wazero.NewModuleConfig().WithName(strconv.Itoa(i))) 780 require.NoError(t, err) 781 defer module.Close(testCtx) 782 783 // Ensure that compilation cache doesn't cause race on memory instance. 784 before, ok := module.Memory().ReadUint64Le(1) 785 require.True(t, ok) 786 // Value must be zero as the memory must not be affected by the previously instantiated modules. 787 require.Zero(t, before) 788 789 f := module.ExportedFunction("store") 790 require.NotNil(t, f) 791 792 _, err = f.Call(testCtx) 793 require.NoError(t, err) 794 795 // After the call, the value must be set properly. 796 after, ok := module.Memory().ReadUint64Le(1) 797 require.True(t, ok) 798 require.Equal(t, uint64(1000), after) 799 } 800 }