wa-lang.org/wazero@v1.0.2/internal/engine/compiler/compiler_memory_test.go (about) 1 package compiler 2 3 import ( 4 "encoding/binary" 5 "fmt" 6 "math" 7 "testing" 8 "unsafe" 9 10 "wa-lang.org/wazero/internal/testing/require" 11 "wa-lang.org/wazero/internal/wasm" 12 "wa-lang.org/wazero/internal/wazeroir" 13 ) 14 15 func TestCompiler_compileMemoryGrow(t *testing.T) { 16 env := newCompilerEnvironment() 17 compiler := env.requireNewCompiler(t, newCompiler, nil) 18 err := compiler.compilePreamble() 19 require.NoError(t, err) 20 21 err = compiler.compileMemoryGrow() 22 require.NoError(t, err) 23 24 // Emit arbitrary code after MemoryGrow returned so that we can verify 25 // that the code can set the return address properly. 26 const expValue uint32 = 100 27 err = compiler.compileConstI32(&wazeroir.OperationConstI32{Value: expValue}) 28 require.NoError(t, err) 29 err = compiler.compileReturnFunction() 30 require.NoError(t, err) 31 32 // Generate and run the code under test. 33 code, _, err := compiler.compile() 34 require.NoError(t, err) 35 env.exec(code) 36 37 // After the initial exec, the code must exit with builtin function call status and funcaddress for memory grow. 38 require.Equal(t, nativeCallStatusCodeCallBuiltInFunction, env.compilerStatus()) 39 require.Equal(t, builtinFunctionIndexMemoryGrow, env.builtinFunctionCallAddress()) 40 41 // Reenter from the return address. 42 nativecall( 43 env.ce.returnAddress, 44 uintptr(unsafe.Pointer(env.callEngine())), 45 uintptr(unsafe.Pointer(env.module())), 46 ) 47 48 // Check if the code successfully executed the code after builtin function call. 49 require.Equal(t, expValue, env.stackTopAsUint32()) 50 require.Equal(t, nativeCallStatusCodeReturned, env.compilerStatus()) 51 } 52 53 func TestCompiler_compileMemorySize(t *testing.T) { 54 env := newCompilerEnvironment() 55 compiler := env.requireNewCompiler(t, newCompiler, &wazeroir.CompilationResult{HasMemory: true, Signature: &wasm.FunctionType{}}) 56 57 err := compiler.compilePreamble() 58 require.NoError(t, err) 59 60 // Emit memory.size instructions. 61 err = compiler.compileMemorySize() 62 require.NoError(t, err) 63 // At this point, the size of memory should be pushed onto the stack. 64 requireRuntimeLocationStackPointerEqual(t, uint64(1), compiler) 65 66 err = compiler.compileReturnFunction() 67 require.NoError(t, err) 68 69 // Generate and run the code under test. 70 code, _, err := compiler.compile() 71 require.NoError(t, err) 72 env.exec(code) 73 74 require.Equal(t, nativeCallStatusCodeReturned, env.compilerStatus()) 75 require.Equal(t, uint32(defaultMemoryPageNumInTest), env.stackTopAsUint32()) 76 } 77 78 func TestCompiler_compileLoad(t *testing.T) { 79 // For testing. Arbitrary number is fine. 80 loadTargetValue := uint64(0x12_34_56_78_9a_bc_ef_fe) 81 baseOffset := uint32(100) 82 arg := &wazeroir.MemoryArg{Offset: 361} 83 offset := baseOffset + arg.Offset 84 85 tests := []struct { 86 name string 87 isFloatTarget bool 88 operationSetupFn func(t *testing.T, compiler compilerImpl) 89 loadedValueVerifyFn func(t *testing.T, loadedValueAsUint64 uint64) 90 }{ 91 { 92 name: "i32.load", 93 operationSetupFn: func(t *testing.T, compiler compilerImpl) { 94 err := compiler.compileLoad(&wazeroir.OperationLoad{Arg: arg, Type: wazeroir.UnsignedTypeI32}) 95 require.NoError(t, err) 96 }, 97 loadedValueVerifyFn: func(t *testing.T, loadedValueAsUint64 uint64) { 98 require.Equal(t, uint32(loadTargetValue), uint32(loadedValueAsUint64)) 99 }, 100 }, 101 { 102 name: "i64.load", 103 operationSetupFn: func(t *testing.T, compiler compilerImpl) { 104 err := compiler.compileLoad(&wazeroir.OperationLoad{Arg: arg, Type: wazeroir.UnsignedTypeI64}) 105 require.NoError(t, err) 106 }, 107 loadedValueVerifyFn: func(t *testing.T, loadedValueAsUint64 uint64) { 108 require.Equal(t, loadTargetValue, loadedValueAsUint64) 109 }, 110 }, 111 { 112 name: "f32.load", 113 operationSetupFn: func(t *testing.T, compiler compilerImpl) { 114 err := compiler.compileLoad(&wazeroir.OperationLoad{Arg: arg, Type: wazeroir.UnsignedTypeF32}) 115 require.NoError(t, err) 116 }, 117 loadedValueVerifyFn: func(t *testing.T, loadedValueAsUint64 uint64) { 118 require.Equal(t, uint32(loadTargetValue), uint32(loadedValueAsUint64)) 119 }, 120 isFloatTarget: true, 121 }, 122 { 123 name: "f64.load", 124 operationSetupFn: func(t *testing.T, compiler compilerImpl) { 125 err := compiler.compileLoad(&wazeroir.OperationLoad{Arg: arg, Type: wazeroir.UnsignedTypeF64}) 126 require.NoError(t, err) 127 }, 128 loadedValueVerifyFn: func(t *testing.T, loadedValueAsUint64 uint64) { 129 require.Equal(t, loadTargetValue, loadedValueAsUint64) 130 }, 131 isFloatTarget: true, 132 }, 133 { 134 name: "i32.load8s", 135 operationSetupFn: func(t *testing.T, compiler compilerImpl) { 136 err := compiler.compileLoad8(&wazeroir.OperationLoad8{Arg: arg, Type: wazeroir.SignedInt32}) 137 require.NoError(t, err) 138 }, 139 loadedValueVerifyFn: func(t *testing.T, loadedValueAsUint64 uint64) { 140 require.Equal(t, int32(int8(loadedValueAsUint64)), int32(uint32(loadedValueAsUint64))) 141 }, 142 }, 143 { 144 name: "i32.load8u", 145 operationSetupFn: func(t *testing.T, compiler compilerImpl) { 146 err := compiler.compileLoad8(&wazeroir.OperationLoad8{Arg: arg, Type: wazeroir.SignedUint32}) 147 require.NoError(t, err) 148 }, 149 loadedValueVerifyFn: func(t *testing.T, loadedValueAsUint64 uint64) { 150 require.Equal(t, uint32(byte(loadedValueAsUint64)), uint32(loadedValueAsUint64)) 151 }, 152 }, 153 { 154 name: "i64.load8s", 155 operationSetupFn: func(t *testing.T, compiler compilerImpl) { 156 err := compiler.compileLoad8(&wazeroir.OperationLoad8{Arg: arg, Type: wazeroir.SignedInt64}) 157 require.NoError(t, err) 158 }, 159 loadedValueVerifyFn: func(t *testing.T, loadedValueAsUint64 uint64) { 160 require.Equal(t, int64(int8(loadedValueAsUint64)), int64(loadedValueAsUint64)) 161 }, 162 }, 163 { 164 name: "i64.load8u", 165 operationSetupFn: func(t *testing.T, compiler compilerImpl) { 166 err := compiler.compileLoad8(&wazeroir.OperationLoad8{Arg: arg, Type: wazeroir.SignedUint64}) 167 require.NoError(t, err) 168 }, 169 loadedValueVerifyFn: func(t *testing.T, loadedValueAsUint64 uint64) { 170 require.Equal(t, uint64(byte(loadedValueAsUint64)), loadedValueAsUint64) 171 }, 172 }, 173 { 174 name: "i32.load16s", 175 operationSetupFn: func(t *testing.T, compiler compilerImpl) { 176 err := compiler.compileLoad16(&wazeroir.OperationLoad16{Arg: arg, Type: wazeroir.SignedInt32}) 177 require.NoError(t, err) 178 }, 179 loadedValueVerifyFn: func(t *testing.T, loadedValueAsUint64 uint64) { 180 require.Equal(t, int32(int16(loadedValueAsUint64)), int32(uint32(loadedValueAsUint64))) 181 }, 182 }, 183 { 184 name: "i32.load16u", 185 operationSetupFn: func(t *testing.T, compiler compilerImpl) { 186 err := compiler.compileLoad16(&wazeroir.OperationLoad16{Arg: arg, Type: wazeroir.SignedUint32}) 187 require.NoError(t, err) 188 }, 189 loadedValueVerifyFn: func(t *testing.T, loadedValueAsUint64 uint64) { 190 require.Equal(t, uint32(loadedValueAsUint64), uint32(loadedValueAsUint64)) 191 }, 192 }, 193 { 194 name: "i64.load16s", 195 operationSetupFn: func(t *testing.T, compiler compilerImpl) { 196 err := compiler.compileLoad16(&wazeroir.OperationLoad16{Arg: arg, Type: wazeroir.SignedInt64}) 197 require.NoError(t, err) 198 }, 199 loadedValueVerifyFn: func(t *testing.T, loadedValueAsUint64 uint64) { 200 require.Equal(t, int64(int16(loadedValueAsUint64)), int64(loadedValueAsUint64)) 201 }, 202 }, 203 { 204 name: "i64.load16u", 205 operationSetupFn: func(t *testing.T, compiler compilerImpl) { 206 err := compiler.compileLoad16(&wazeroir.OperationLoad16{Arg: arg, Type: wazeroir.SignedUint64}) 207 require.NoError(t, err) 208 }, 209 loadedValueVerifyFn: func(t *testing.T, loadedValueAsUint64 uint64) { 210 require.Equal(t, uint64(uint16(loadedValueAsUint64)), loadedValueAsUint64) 211 }, 212 }, 213 { 214 name: "i64.load32s", 215 operationSetupFn: func(t *testing.T, compiler compilerImpl) { 216 err := compiler.compileLoad32(&wazeroir.OperationLoad32{Arg: arg, Signed: true}) 217 require.NoError(t, err) 218 }, 219 loadedValueVerifyFn: func(t *testing.T, loadedValueAsUint64 uint64) { 220 require.Equal(t, int64(int32(loadedValueAsUint64)), int64(loadedValueAsUint64)) 221 }, 222 }, 223 { 224 name: "i64.load32u", 225 operationSetupFn: func(t *testing.T, compiler compilerImpl) { 226 err := compiler.compileLoad32(&wazeroir.OperationLoad32{Arg: arg, Signed: false}) 227 require.NoError(t, err) 228 }, 229 loadedValueVerifyFn: func(t *testing.T, loadedValueAsUint64 uint64) { 230 require.Equal(t, uint64(uint32(loadedValueAsUint64)), loadedValueAsUint64) 231 }, 232 }, 233 } 234 235 for _, tt := range tests { 236 tc := tt 237 t.Run(tc.name, func(t *testing.T) { 238 env := newCompilerEnvironment() 239 compiler := env.requireNewCompiler(t, newCompiler, &wazeroir.CompilationResult{HasMemory: true, Signature: &wasm.FunctionType{}}) 240 241 err := compiler.compilePreamble() 242 require.NoError(t, err) 243 244 binary.LittleEndian.PutUint64(env.memory()[offset:], loadTargetValue) 245 246 // Before load operation, we must push the base offset value. 247 err = compiler.compileConstI32(&wazeroir.OperationConstI32{Value: baseOffset}) 248 require.NoError(t, err) 249 250 tc.operationSetupFn(t, compiler) 251 252 // At this point, the loaded value must be on top of the stack, and placed on a register. 253 requireRuntimeLocationStackPointerEqual(t, uint64(1), compiler) 254 require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters)) 255 loadedLocation := compiler.runtimeValueLocationStack().peek() 256 require.True(t, loadedLocation.onRegister()) 257 if tc.isFloatTarget { 258 require.Equal(t, registerTypeVector, loadedLocation.getRegisterType()) 259 } else { 260 require.Equal(t, registerTypeGeneralPurpose, loadedLocation.getRegisterType()) 261 } 262 err = compiler.compileReturnFunction() 263 require.NoError(t, err) 264 265 // Generate and run the code under test. 266 code, _, err := compiler.compile() 267 require.NoError(t, err) 268 env.exec(code) 269 270 // Verify the loaded value. 271 require.Equal(t, uint64(1), env.stackPointer()) 272 tc.loadedValueVerifyFn(t, env.stackTopAsUint64()) 273 }) 274 } 275 } 276 277 func TestCompiler_compileStore(t *testing.T) { 278 // For testing. Arbitrary number is fine. 279 storeTargetValue := uint64(math.MaxUint64) 280 baseOffset := uint32(100) 281 arg := &wazeroir.MemoryArg{Offset: 361} 282 offset := arg.Offset + baseOffset 283 284 tests := []struct { 285 name string 286 isFloatTarget bool 287 targetSizeInBytes uint32 288 operationSetupFn func(t *testing.T, compiler compilerImpl) 289 storedValueVerifyFn func(t *testing.T, mem []byte) 290 }{ 291 { 292 name: "i32.store", 293 targetSizeInBytes: 32 / 8, 294 operationSetupFn: func(t *testing.T, compiler compilerImpl) { 295 err := compiler.compileStore(&wazeroir.OperationStore{Arg: arg, Type: wazeroir.UnsignedTypeI32}) 296 require.NoError(t, err) 297 }, 298 storedValueVerifyFn: func(t *testing.T, mem []byte) { 299 require.Equal(t, uint32(storeTargetValue), binary.LittleEndian.Uint32(mem[offset:])) 300 }, 301 }, 302 { 303 name: "f32.store", 304 isFloatTarget: true, 305 targetSizeInBytes: 32 / 8, 306 operationSetupFn: func(t *testing.T, compiler compilerImpl) { 307 err := compiler.compileStore(&wazeroir.OperationStore{Arg: arg, Type: wazeroir.UnsignedTypeF32}) 308 require.NoError(t, err) 309 }, 310 storedValueVerifyFn: func(t *testing.T, mem []byte) { 311 require.Equal(t, uint32(storeTargetValue), binary.LittleEndian.Uint32(mem[offset:])) 312 }, 313 }, 314 { 315 name: "i64.store", 316 targetSizeInBytes: 64 / 8, 317 operationSetupFn: func(t *testing.T, compiler compilerImpl) { 318 err := compiler.compileStore(&wazeroir.OperationStore{Arg: arg, Type: wazeroir.UnsignedTypeI64}) 319 require.NoError(t, err) 320 }, 321 storedValueVerifyFn: func(t *testing.T, mem []byte) { 322 require.Equal(t, storeTargetValue, binary.LittleEndian.Uint64(mem[offset:])) 323 }, 324 }, 325 { 326 name: "f64.store", 327 isFloatTarget: true, 328 targetSizeInBytes: 64 / 8, 329 operationSetupFn: func(t *testing.T, compiler compilerImpl) { 330 err := compiler.compileStore(&wazeroir.OperationStore{Arg: arg, Type: wazeroir.UnsignedTypeF64}) 331 require.NoError(t, err) 332 }, 333 storedValueVerifyFn: func(t *testing.T, mem []byte) { 334 require.Equal(t, storeTargetValue, binary.LittleEndian.Uint64(mem[offset:])) 335 }, 336 }, 337 { 338 name: "store8", 339 targetSizeInBytes: 1, 340 operationSetupFn: func(t *testing.T, compiler compilerImpl) { 341 err := compiler.compileStore8(&wazeroir.OperationStore8{Arg: arg}) 342 require.NoError(t, err) 343 }, 344 storedValueVerifyFn: func(t *testing.T, mem []byte) { 345 require.Equal(t, byte(storeTargetValue), mem[offset]) 346 }, 347 }, 348 { 349 name: "store16", 350 targetSizeInBytes: 16 / 8, 351 operationSetupFn: func(t *testing.T, compiler compilerImpl) { 352 err := compiler.compileStore16(&wazeroir.OperationStore16{Arg: arg}) 353 require.NoError(t, err) 354 }, 355 storedValueVerifyFn: func(t *testing.T, mem []byte) { 356 require.Equal(t, uint16(storeTargetValue), binary.LittleEndian.Uint16(mem[offset:])) 357 }, 358 }, 359 { 360 name: "store32", 361 targetSizeInBytes: 32 / 8, 362 operationSetupFn: func(t *testing.T, compiler compilerImpl) { 363 err := compiler.compileStore32(&wazeroir.OperationStore32{Arg: arg}) 364 require.NoError(t, err) 365 }, 366 storedValueVerifyFn: func(t *testing.T, mem []byte) { 367 require.Equal(t, uint32(storeTargetValue), binary.LittleEndian.Uint32(mem[offset:])) 368 }, 369 }, 370 } 371 372 for _, tt := range tests { 373 tc := tt 374 t.Run(tc.name, func(t *testing.T) { 375 env := newCompilerEnvironment() 376 compiler := env.requireNewCompiler(t, newCompiler, &wazeroir.CompilationResult{HasMemory: true, Signature: &wasm.FunctionType{}}) 377 378 err := compiler.compilePreamble() 379 require.NoError(t, err) 380 381 // Before store operations, we must push the base offset, and the store target values. 382 err = compiler.compileConstI32(&wazeroir.OperationConstI32{Value: baseOffset}) 383 require.NoError(t, err) 384 if tc.isFloatTarget { 385 err = compiler.compileConstF64(&wazeroir.OperationConstF64{Value: math.Float64frombits(storeTargetValue)}) 386 } else { 387 err = compiler.compileConstI64(&wazeroir.OperationConstI64{Value: storeTargetValue}) 388 } 389 require.NoError(t, err) 390 391 tc.operationSetupFn(t, compiler) 392 393 // At this point, no registers must be in use, and no values on the stack since we consumed two values. 394 require.Zero(t, len(compiler.runtimeValueLocationStack().usedRegisters)) 395 requireRuntimeLocationStackPointerEqual(t, uint64(0), compiler) 396 397 // Generate the code under test. 398 err = compiler.compileReturnFunction() 399 require.NoError(t, err) 400 code, _, err := compiler.compile() 401 require.NoError(t, err) 402 403 // Set the value on the left and right neighboring memoryregion, 404 // so that we can verify the operation doesn't affect there. 405 ceil := offset + tc.targetSizeInBytes 406 mem := env.memory() 407 expectedNeighbor8Bytes := uint64(0x12_34_56_78_9a_bc_ef_fe) 408 binary.LittleEndian.PutUint64(mem[offset-8:offset], expectedNeighbor8Bytes) 409 binary.LittleEndian.PutUint64(mem[ceil:ceil+8], expectedNeighbor8Bytes) 410 411 // Run code. 412 env.exec(code) 413 414 tc.storedValueVerifyFn(t, mem) 415 416 // The neighboring bytes must be intact. 417 require.Equal(t, expectedNeighbor8Bytes, binary.LittleEndian.Uint64(mem[offset-8:offset])) 418 require.Equal(t, expectedNeighbor8Bytes, binary.LittleEndian.Uint64(mem[ceil:ceil+8])) 419 }) 420 } 421 } 422 423 func TestCompiler_MemoryOutOfBounds(t *testing.T) { 424 bases := []uint32{0, 1 << 5, 1 << 9, 1 << 10, 1 << 15, math.MaxUint32 - 1, math.MaxUint32} 425 offsets := []uint32{ 426 0, 427 1 << 10, 1 << 31, 428 defaultMemoryPageNumInTest*wasm.MemoryPageSize - 1, defaultMemoryPageNumInTest * wasm.MemoryPageSize, 429 math.MaxInt32 - 1, math.MaxInt32 - 2, math.MaxInt32 - 3, math.MaxInt32 - 4, 430 math.MaxInt32 - 5, math.MaxInt32 - 8, math.MaxInt32 - 9, math.MaxInt32, math.MaxUint32, 431 } 432 targetSizeInBytes := []int64{1, 2, 4, 8} 433 for _, base := range bases { 434 base := base 435 for _, offset := range offsets { 436 offset := offset 437 for _, targetSizeInByte := range targetSizeInBytes { 438 targetSizeInByte := targetSizeInByte 439 t.Run(fmt.Sprintf("base=%d,offset=%d,targetSizeInBytes=%d", base, offset, targetSizeInByte), func(t *testing.T) { 440 env := newCompilerEnvironment() 441 compiler := env.requireNewCompiler(t, newCompiler, nil) 442 443 err := compiler.compilePreamble() 444 require.NoError(t, err) 445 446 err = compiler.compileConstI32(&wazeroir.OperationConstI32{Value: base}) 447 require.NoError(t, err) 448 449 arg := &wazeroir.MemoryArg{Offset: offset} 450 451 switch targetSizeInByte { 452 case 1: 453 err = compiler.compileLoad8(&wazeroir.OperationLoad8{Type: wazeroir.SignedInt32, Arg: arg}) 454 case 2: 455 err = compiler.compileLoad16(&wazeroir.OperationLoad16{Type: wazeroir.SignedInt32, Arg: arg}) 456 case 4: 457 err = compiler.compileLoad32(&wazeroir.OperationLoad32{Signed: false, Arg: arg}) 458 case 8: 459 err = compiler.compileLoad(&wazeroir.OperationLoad{Type: wazeroir.UnsignedTypeF64, Arg: arg}) 460 default: 461 t.Fail() 462 } 463 464 require.NoError(t, err) 465 466 require.NoError(t, compiler.compileReturnFunction()) 467 468 // Generate the code under test and run. 469 code, _, err := compiler.compile() 470 require.NoError(t, err) 471 env.exec(code) 472 473 mem := env.memory() 474 if ceil := int64(base) + int64(offset) + int64(targetSizeInByte); int64(len(mem)) < ceil { 475 // If the targe memory region's ceil exceeds the length of memory, we must exit the function 476 // with nativeCallStatusCodeMemoryOutOfBounds status code. 477 require.Equal(t, nativeCallStatusCodeMemoryOutOfBounds, env.compilerStatus()) 478 } 479 }) 480 } 481 } 482 } 483 }