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