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  }