package compiler import ( "fmt" "strconv" "testing" "unsafe" "github.com/tetratelabs/wazero/internal/asm" "github.com/tetratelabs/wazero/internal/testing/require" "github.com/tetratelabs/wazero/internal/wasm" "github.com/tetratelabs/wazero/internal/wazeroir" ) func TestCompiler_compileSignExtend(t *testing.T) { type fromKind byte from8, from16, from32 := fromKind(0), fromKind(1), fromKind(2) t.Run("32bit", func(t *testing.T) { tests := []struct { in int32 expected int32 fromKind fromKind }{ // https://github.com/WebAssembly/spec/blob/ee4a6c40afa22e3e4c58610ce75186aafc22344e/test/core/i32.wast#L270-L276 {in: 0, expected: 0, fromKind: from8}, {in: 0x7f, expected: 127, fromKind: from8}, {in: 0x80, expected: -128, fromKind: from8}, {in: 0xff, expected: -1, fromKind: from8}, {in: 0x012345_00, expected: 0, fromKind: from8}, {in: -19088768 /* = 0xfedcba_80 bit pattern */, expected: -0x80, fromKind: from8}, {in: -1, expected: -1, fromKind: from8}, // https://github.com/WebAssembly/spec/blob/ee4a6c40afa22e3e4c58610ce75186aafc22344e/test/core/i32.wast#L278-L284 {in: 0, expected: 0, fromKind: from16}, {in: 0x7fff, expected: 32767, fromKind: from16}, {in: 0x8000, expected: -32768, fromKind: from16}, {in: 0xffff, expected: -1, fromKind: from16}, {in: 0x0123_0000, expected: 0, fromKind: from16}, {in: -19103744 /* = 0xfedc_8000 bit pattern */, expected: -0x8000, fromKind: from16}, {in: -1, expected: -1, fromKind: from16}, } for _, tt := range tests { tc := tt t.Run(fmt.Sprintf("0x%x", tc.in), func(t *testing.T) { env := newCompilerEnvironment() compiler := env.requireNewCompiler(t, &wasm.FunctionType{}, newCompiler, nil) err := compiler.compilePreamble() require.NoError(t, err) // Setup the promote target. err = compiler.compileConstI32(operationPtr(wazeroir.NewOperationConstI32(uint32(tc.in)))) require.NoError(t, err) if tc.fromKind == from8 { err = compiler.compileSignExtend32From8() } else { err = compiler.compileSignExtend32From16() } require.NoError(t, err) // To verify the behavior, we release the value // to the stack. err = compiler.compileReturnFunction() require.NoError(t, err) code := asm.CodeSegment{} defer func() { require.NoError(t, code.Unmap()) }() // Generate and run the code under test. _, err = compiler.compile(code.NextCodeSection()) require.NoError(t, err) env.exec(code.Bytes()) require.Equal(t, uint64(1), env.stackPointer()) require.Equal(t, tc.expected, env.stackTopAsInt32()) }) } }) t.Run("64bit", func(t *testing.T) { tests := []struct { in int64 expected int64 fromKind fromKind }{ // https://github.com/WebAssembly/spec/blob/ee4a6c40afa22e3e4c58610ce75186aafc22344e/test/core/i64.wast#L271-L277 {in: 0, expected: 0, fromKind: from8}, {in: 0x7f, expected: 127, fromKind: from8}, {in: 0x80, expected: -128, fromKind: from8}, {in: 0xff, expected: -1, fromKind: from8}, {in: 0x01234567_89abcd_00, expected: 0, fromKind: from8}, {in: 81985529216486784 /* = 0xfedcba98_765432_80 bit pattern */, expected: -0x80, fromKind: from8}, {in: -1, expected: -1, fromKind: from8}, // https://github.com/WebAssembly/spec/blob/ee4a6c40afa22e3e4c58610ce75186aafc22344e/test/core/i64.wast#L279-L285 {in: 0, expected: 0, fromKind: from16}, {in: 0x7fff, expected: 32767, fromKind: from16}, {in: 0x8000, expected: -32768, fromKind: from16}, {in: 0xffff, expected: -1, fromKind: from16}, {in: 0x12345678_9abc_0000, expected: 0, fromKind: from16}, {in: 81985529216466944 /* = 0xfedcba98_7654_8000 bit pattern */, expected: -0x8000, fromKind: from16}, {in: -1, expected: -1, fromKind: from16}, // https://github.com/WebAssembly/spec/blob/ee4a6c40afa22e3e4c58610ce75186aafc22344e/test/core/i64.wast#L287-L296 {in: 0, expected: 0, fromKind: from32}, {in: 0x7fff, expected: 32767, fromKind: from32}, {in: 0x8000, expected: 32768, fromKind: from32}, {in: 0xffff, expected: 65535, fromKind: from32}, {in: 0x7fffffff, expected: 0x7fffffff, fromKind: from32}, {in: 0x80000000, expected: -0x80000000, fromKind: from32}, {in: 0xffffffff, expected: -1, fromKind: from32}, {in: 0x01234567_00000000, expected: 0, fromKind: from32}, {in: -81985529054232576 /* = 0xfedcba98_80000000 bit pattern */, expected: -0x80000000, fromKind: from32}, {in: -1, expected: -1, fromKind: from32}, } for _, tt := range tests { tc := tt t.Run(fmt.Sprintf("0x%x", tc.in), func(t *testing.T) { env := newCompilerEnvironment() compiler := env.requireNewCompiler(t, &wasm.FunctionType{}, newCompiler, nil) err := compiler.compilePreamble() require.NoError(t, err) // Setup the promote target. err = compiler.compileConstI64(operationPtr(wazeroir.NewOperationConstI64(uint64(tc.in)))) require.NoError(t, err) if tc.fromKind == from8 { err = compiler.compileSignExtend64From8() } else if tc.fromKind == from16 { err = compiler.compileSignExtend64From16() } else { err = compiler.compileSignExtend64From32() } require.NoError(t, err) // To verify the behavior, we release the value // to the stack. err = compiler.compileReturnFunction() require.NoError(t, err) code := asm.CodeSegment{} defer func() { require.NoError(t, code.Unmap()) }() // Generate and run the code under test. _, err = compiler.compile(code.NextCodeSection()) require.NoError(t, err) env.exec(code.Bytes()) require.Equal(t, uint64(1), env.stackPointer()) require.Equal(t, tc.expected, env.stackTopAsInt64()) }) } }) } func TestCompiler_compileMemoryCopy(t *testing.T) { const checkCeil = 100 tests := []struct { sourceOffset, destOffset, size uint32 requireOutOfBoundsError bool }{ {sourceOffset: 0, destOffset: 0, size: 0}, {sourceOffset: 10, destOffset: 5, size: 10}, {sourceOffset: 10, destOffset: 9, size: 1}, {sourceOffset: 10, destOffset: 9, size: 2}, {sourceOffset: 0, destOffset: 10, size: 10}, {sourceOffset: 0, destOffset: 5, size: 10}, {sourceOffset: 9, destOffset: 10, size: 10}, {sourceOffset: 11, destOffset: 13, size: 4}, {sourceOffset: 0, destOffset: 10, size: 5}, {sourceOffset: 1, destOffset: 10, size: 5}, {sourceOffset: 0, destOffset: 10, size: 1}, {sourceOffset: 0, destOffset: 10, size: 0}, {sourceOffset: 5, destOffset: 10, size: 10}, {sourceOffset: 5, destOffset: 10, size: 5}, {sourceOffset: 5, destOffset: 10, size: 1}, {sourceOffset: 5, destOffset: 10, size: 0}, {sourceOffset: 10, destOffset: 0, size: 10}, {sourceOffset: 1, destOffset: 0, size: 2}, {sourceOffset: 1, destOffset: 0, size: 20}, {sourceOffset: 10, destOffset: 0, size: 5}, {sourceOffset: 10, destOffset: 0, size: 1}, {sourceOffset: 10, destOffset: 0, size: 0}, {sourceOffset: 0, destOffset: 50, size: 48}, {sourceOffset: 0, destOffset: 50, size: 49}, {sourceOffset: 10, destOffset: 20, size: 72}, {sourceOffset: 20, destOffset: 10, size: 72}, {sourceOffset: 19, destOffset: 18, size: 79}, {sourceOffset: 20, destOffset: 19, size: 79}, {sourceOffset: defaultMemoryPageNumInTest * wasm.MemoryPageSize, destOffset: 0, size: 1, requireOutOfBoundsError: true}, {sourceOffset: defaultMemoryPageNumInTest*wasm.MemoryPageSize + 1, destOffset: 0, size: 0, requireOutOfBoundsError: true}, {sourceOffset: 0, destOffset: defaultMemoryPageNumInTest * wasm.MemoryPageSize, size: 1, requireOutOfBoundsError: true}, {sourceOffset: 0, destOffset: defaultMemoryPageNumInTest*wasm.MemoryPageSize + 1, size: 0, requireOutOfBoundsError: true}, {sourceOffset: defaultMemoryPageNumInTest*wasm.MemoryPageSize - 99, destOffset: 0, size: 100, requireOutOfBoundsError: true}, {sourceOffset: 0, destOffset: defaultMemoryPageNumInTest*wasm.MemoryPageSize - 99, size: 100, requireOutOfBoundsError: true}, } for i, tt := range tests { tc := tt t.Run(strconv.Itoa(i), func(t *testing.T) { env := newCompilerEnvironment() compiler := env.requireNewCompiler(t, &wasm.FunctionType{}, newCompiler, &wazeroir.CompilationResult{HasMemory: true}) err := compiler.compilePreamble() require.NoError(t, err) // Compile operands. err = compiler.compileConstI32(operationPtr(wazeroir.NewOperationConstI32(tc.destOffset))) require.NoError(t, err) err = compiler.compileConstI32(operationPtr(wazeroir.NewOperationConstI32(tc.sourceOffset))) require.NoError(t, err) err = compiler.compileConstI32(operationPtr(wazeroir.NewOperationConstI32(tc.size))) require.NoError(t, err) err = compiler.compileMemoryCopy() require.NoError(t, err) code := asm.CodeSegment{} defer func() { require.NoError(t, code.Unmap()) }() // Generate the code under test. err = compiler.compileReturnFunction() require.NoError(t, err) _, err = compiler.compile(code.NextCodeSection()) require.NoError(t, err) // Setup the source memory region. mem := env.memory() for i := 0; i < checkCeil; i++ { mem[i] = byte(i) } // Run code. env.exec(code.Bytes()) if !tc.requireOutOfBoundsError { exp := make([]byte, checkCeil) for i := 0; i < checkCeil; i++ { exp[i] = byte(i) } copy(exp[tc.destOffset:], exp[tc.sourceOffset:tc.sourceOffset+tc.size]) // Check the status code and the destination memory region. require.Equal(t, nativeCallStatusCodeReturned, env.compilerStatus()) require.Equal(t, exp, mem[:checkCeil]) } else { require.Equal(t, nativeCallStatusCodeMemoryOutOfBounds, env.compilerStatus()) } }) } } func TestCompiler_compileMemoryFill(t *testing.T) { const checkCeil = 50 tests := []struct { v, destOffset uint32 size uint32 requireOutOfBoundsError bool }{ {v: 0, destOffset: 0, size: 25}, {v: 0, destOffset: 10, size: 17}, {v: 0, destOffset: 10, size: 15}, {v: 0, destOffset: 10, size: 5}, {v: 0, destOffset: 10, size: 1}, {v: 0, destOffset: 10, size: 0}, {v: 5, destOffset: 10, size: 27}, {v: 5, destOffset: 10, size: 25}, {v: 5, destOffset: 10, size: 21}, {v: 5, destOffset: 10, size: 10}, {v: 5, destOffset: 10, size: 5}, {v: 5, destOffset: 10, size: 1}, {v: 5, destOffset: 10, size: 0}, {v: 10, destOffset: 0, size: 10}, {v: 10, destOffset: 0, size: 5}, {v: 10, destOffset: 0, size: 1}, {v: 10, destOffset: 0, size: 0}, {v: 10, destOffset: defaultMemoryPageNumInTest*wasm.MemoryPageSize - 99, size: 100, requireOutOfBoundsError: true}, {v: 10, destOffset: defaultMemoryPageNumInTest * wasm.MemoryPageSize, size: 5, requireOutOfBoundsError: true}, {v: 10, destOffset: defaultMemoryPageNumInTest * wasm.MemoryPageSize, size: 1, requireOutOfBoundsError: true}, {v: 10, destOffset: defaultMemoryPageNumInTest*wasm.MemoryPageSize + 1, size: 0, requireOutOfBoundsError: true}, } for i, tt := range tests { tc := tt t.Run(strconv.Itoa(i), func(t *testing.T) { env := newCompilerEnvironment() compiler := env.requireNewCompiler(t, &wasm.FunctionType{}, newCompiler, &wazeroir.CompilationResult{HasMemory: true}) err := compiler.compilePreamble() require.NoError(t, err) // Compile operands. err = compiler.compileConstI32(operationPtr(wazeroir.NewOperationConstI32(tc.destOffset))) require.NoError(t, err) err = compiler.compileConstI32(operationPtr(wazeroir.NewOperationConstI32(tc.v))) require.NoError(t, err) err = compiler.compileConstI32(operationPtr(wazeroir.NewOperationConstI32(tc.size))) require.NoError(t, err) err = compiler.compileMemoryFill() require.NoError(t, err) code := asm.CodeSegment{} defer func() { require.NoError(t, code.Unmap()) }() // Generate the code under test. err = compiler.compileReturnFunction() require.NoError(t, err) _, err = compiler.compile(code.NextCodeSection()) require.NoError(t, err) // Setup the memory region. mem := env.memory() for i := 0; i < checkCeil; i++ { mem[i] = byte(i) } // Run code. env.exec(code.Bytes()) if !tc.requireOutOfBoundsError { exp := make([]byte, checkCeil) for i := 0; i < checkCeil; i++ { if i >= int(tc.destOffset) && i < int(tc.destOffset+tc.size) { exp[i] = byte(tc.v) } else { exp[i] = byte(i) } } // Check the status code and the destination memory region. require.Equal(t, nativeCallStatusCodeReturned, env.compilerStatus()) require.Equal(t, exp, mem[:checkCeil]) } else { require.Equal(t, nativeCallStatusCodeMemoryOutOfBounds, env.compilerStatus()) } }) } } func TestCompiler_compileDataDrop(t *testing.T) { origins := [][]byte{ {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}, {10}, } for i := 0; i < len(origins); i++ { t.Run(strconv.Itoa(i), func(t *testing.T) { env := newCompilerEnvironment() env.module().DataInstances = make([][]byte, len(origins)) copy(env.module().DataInstances, origins) compiler := env.requireNewCompiler(t, &wasm.FunctionType{}, newCompiler, &wazeroir.CompilationResult{ HasDataInstances: true, }) err := compiler.compilePreamble() require.NoError(t, err) err = compiler.compileDataDrop(operationPtr(wazeroir.NewOperationDataDrop(uint32(i)))) require.NoError(t, err) code := asm.CodeSegment{} defer func() { require.NoError(t, code.Unmap()) }() // Generate the code under test. err = compiler.compileReturnFunction() require.NoError(t, err) _, err = compiler.compile(code.NextCodeSection()) require.NoError(t, err) // Run code. env.exec(code.Bytes()) require.Equal(t, nativeCallStatusCodeReturned, env.compilerStatus()) // Check if the target data instance is dropped from the dataInstances slice. for j := 0; j < len(origins); j++ { if i == j { require.Nil(t, env.module().DataInstances[j]) } else { require.NotNil(t, env.module().DataInstances[j]) } } }) } } func TestCompiler_compileMemoryInit(t *testing.T) { dataInstances := []wasm.DataInstance{ nil, {1, 2, 3, 4, 5}, } tests := []struct { sourceOffset, destOffset uint32 dataIndex uint32 copySize uint32 expOutOfBounds bool }{ {sourceOffset: 0, destOffset: 0, copySize: 0, dataIndex: 0}, {sourceOffset: 0, destOffset: 0, copySize: 1, dataIndex: 0, expOutOfBounds: true}, {sourceOffset: 1, destOffset: 0, copySize: 0, dataIndex: 0, expOutOfBounds: true}, {sourceOffset: 0, destOffset: 0, copySize: 0, dataIndex: 1}, {sourceOffset: 0, destOffset: 0, copySize: 5, dataIndex: 1}, {sourceOffset: 0, destOffset: 0, copySize: 1, dataIndex: 1}, {sourceOffset: 0, destOffset: 0, copySize: 3, dataIndex: 1}, {sourceOffset: 0, destOffset: 1, copySize: 3, dataIndex: 1}, {sourceOffset: 0, destOffset: 7, copySize: 4, dataIndex: 1}, {sourceOffset: 1, destOffset: 7, copySize: 4, dataIndex: 1}, {sourceOffset: 4, destOffset: 7, copySize: 1, dataIndex: 1}, {sourceOffset: 5, destOffset: 7, copySize: 0, dataIndex: 1}, {sourceOffset: 0, destOffset: 7, copySize: 5, dataIndex: 1}, {sourceOffset: 1, destOffset: 0, copySize: 3, dataIndex: 1}, {sourceOffset: 0, destOffset: 1, copySize: 4, dataIndex: 1}, {sourceOffset: 1, destOffset: 1, copySize: 3, dataIndex: 1}, {sourceOffset: 0, destOffset: 10, copySize: 5, dataIndex: 1}, {sourceOffset: 0, destOffset: 0, copySize: 6, dataIndex: 1, expOutOfBounds: true}, {sourceOffset: 0, destOffset: defaultMemoryPageNumInTest * wasm.MemoryPageSize, copySize: 5, dataIndex: 1, expOutOfBounds: true}, {sourceOffset: 0, destOffset: defaultMemoryPageNumInTest*wasm.MemoryPageSize - 3, copySize: 5, dataIndex: 1, expOutOfBounds: true}, {sourceOffset: 6, destOffset: 0, copySize: 0, dataIndex: 1, expOutOfBounds: true}, } for i, tt := range tests { tc := tt t.Run(strconv.Itoa(i), func(t *testing.T) { env := newCompilerEnvironment() env.module().DataInstances = dataInstances compiler := env.requireNewCompiler(t, &wasm.FunctionType{}, newCompiler, &wazeroir.CompilationResult{ HasDataInstances: true, HasMemory: true, }) err := compiler.compilePreamble() require.NoError(t, err) // Compile operands. err = compiler.compileConstI32(operationPtr(wazeroir.NewOperationConstI32(tc.destOffset))) require.NoError(t, err) err = compiler.compileConstI32(operationPtr(wazeroir.NewOperationConstI32(tc.sourceOffset))) require.NoError(t, err) err = compiler.compileConstI32(operationPtr(wazeroir.NewOperationConstI32(tc.copySize))) require.NoError(t, err) err = compiler.compileMemoryInit(operationPtr(wazeroir.NewOperationMemoryInit(tc.dataIndex))) require.NoError(t, err) code := asm.CodeSegment{} defer func() { require.NoError(t, code.Unmap()) }() // Generate the code under test. err = compiler.compileReturnFunction() require.NoError(t, err) _, err = compiler.compile(code.NextCodeSection()) require.NoError(t, err) // Run code. env.exec(code.Bytes()) if !tc.expOutOfBounds { mem := env.memory() exp := make([]byte, defaultMemoryPageNumInTest*wasm.MemoryPageSize) if dataInst := dataInstances[tc.dataIndex]; dataInst != nil { copy(exp[tc.destOffset:], dataInst[tc.sourceOffset:tc.sourceOffset+tc.copySize]) } require.Equal(t, exp[:20], mem[:20]) } else { require.Equal(t, nativeCallStatusCodeMemoryOutOfBounds, env.compilerStatus()) } }) } } func TestCompiler_compileElemDrop(t *testing.T) { origins := []wasm.ElementInstance{{1}, {2}, {3}, {4}, {5}} for i := 0; i < len(origins); i++ { t.Run(strconv.Itoa(i), func(t *testing.T) { env := newCompilerEnvironment() insts := make([]wasm.ElementInstance, len(origins)) copy(insts, origins) env.module().ElementInstances = insts // Verify assumption that before Drop instruction, all the element instances are not empty. for _, inst := range insts { require.NotEqual(t, 0, len(inst)) } compiler := env.requireNewCompiler(t, &wasm.FunctionType{}, newCompiler, &wazeroir.CompilationResult{ HasElementInstances: true, }) err := compiler.compilePreamble() require.NoError(t, err) err = compiler.compileElemDrop(operationPtr(wazeroir.NewOperationElemDrop(uint32(i)))) require.NoError(t, err) code := asm.CodeSegment{} defer func() { require.NoError(t, code.Unmap()) }() // Generate the code under test. err = compiler.compileReturnFunction() require.NoError(t, err) _, err = compiler.compile(code.NextCodeSection()) require.NoError(t, err) // Run code. env.exec(code.Bytes()) require.Equal(t, nativeCallStatusCodeReturned, env.compilerStatus()) for j := 0; j < len(insts); j++ { if i == j { require.Zero(t, len(env.module().ElementInstances[j])) } else { require.NotEqual(t, 0, len(env.module().ElementInstances[j])) } } }) } } func TestCompiler_compileTableCopy(t *testing.T) { const tableSize = 100 tests := []struct { sourceOffset, destOffset, size uint32 requireOutOfBoundsError bool }{ {sourceOffset: 0, destOffset: 0, size: 0}, {sourceOffset: 10, destOffset: 5, size: 10}, {sourceOffset: 10, destOffset: 9, size: 1}, {sourceOffset: 10, destOffset: 9, size: 2}, {sourceOffset: 0, destOffset: 10, size: 10}, {sourceOffset: 0, destOffset: 5, size: 10}, {sourceOffset: 9, destOffset: 10, size: 10}, {sourceOffset: 11, destOffset: 13, size: 4}, {sourceOffset: 0, destOffset: 10, size: 5}, {sourceOffset: 1, destOffset: 10, size: 5}, {sourceOffset: 0, destOffset: 10, size: 1}, {sourceOffset: 0, destOffset: 10, size: 0}, {sourceOffset: 5, destOffset: 10, size: 10}, {sourceOffset: 5, destOffset: 10, size: 5}, {sourceOffset: 5, destOffset: 10, size: 1}, {sourceOffset: 5, destOffset: 10, size: 0}, {sourceOffset: 10, destOffset: 0, size: 10}, {sourceOffset: 1, destOffset: 0, size: 2}, {sourceOffset: 1, destOffset: 0, size: 20}, {sourceOffset: 10, destOffset: 0, size: 5}, {sourceOffset: 10, destOffset: 0, size: 1}, {sourceOffset: 10, destOffset: 0, size: 0}, {sourceOffset: tableSize, destOffset: 0, size: 1, requireOutOfBoundsError: true}, {sourceOffset: tableSize + 1, destOffset: 0, size: 0, requireOutOfBoundsError: true}, {sourceOffset: 0, destOffset: tableSize, size: 1, requireOutOfBoundsError: true}, {sourceOffset: 0, destOffset: tableSize + 1, size: 0, requireOutOfBoundsError: true}, {sourceOffset: tableSize - 99, destOffset: 0, size: 100, requireOutOfBoundsError: true}, {sourceOffset: 0, destOffset: tableSize - 99, size: 100, requireOutOfBoundsError: true}, } for i, tt := range tests { tc := tt t.Run(strconv.Itoa(i), func(t *testing.T) { env := newCompilerEnvironment() compiler := env.requireNewCompiler(t, &wasm.FunctionType{}, newCompiler, &wazeroir.CompilationResult{HasTable: true}) err := compiler.compilePreamble() require.NoError(t, err) // Compile operands. err = compiler.compileConstI32(operationPtr(wazeroir.NewOperationConstI32(tc.destOffset))) require.NoError(t, err) err = compiler.compileConstI32(operationPtr(wazeroir.NewOperationConstI32(tc.sourceOffset))) require.NoError(t, err) err = compiler.compileConstI32(operationPtr(wazeroir.NewOperationConstI32(tc.size))) require.NoError(t, err) err = compiler.compileTableCopy(operationPtr(wazeroir.NewOperationTableCopy(0, 0))) require.NoError(t, err) code := asm.CodeSegment{} defer func() { require.NoError(t, code.Unmap()) }() // Generate the code under test. err = compiler.compileReturnFunction() require.NoError(t, err) _, err = compiler.compile(code.NextCodeSection()) require.NoError(t, err) // Setup the table. table := make([]wasm.Reference, tableSize) env.addTable(&wasm.TableInstance{References: table}) for i := 0; i < tableSize; i++ { table[i] = uintptr(i) } // Run code. env.exec(code.Bytes()) if !tc.requireOutOfBoundsError { exp := make([]wasm.Reference, tableSize) for i := 0; i < tableSize; i++ { exp[i] = uintptr(i) } copy(exp[tc.destOffset:], exp[tc.sourceOffset:tc.sourceOffset+tc.size]) // Check the status code and the destination memory region. require.Equal(t, nativeCallStatusCodeReturned, env.compilerStatus()) require.Equal(t, exp, table) } else { require.Equal(t, nativeCallStatusCodeInvalidTableAccess, env.compilerStatus()) } }) } } func TestCompiler_compileTableInit(t *testing.T) { elementInstances := []wasm.ElementInstance{ {}, {1, 2, 3, 4, 5}, } const tableSize = 100 tests := []struct { sourceOffset, destOffset uint32 elemIndex uint32 copySize uint32 expOutOfBounds bool }{ {sourceOffset: 0, destOffset: 0, copySize: 0, elemIndex: 0}, {sourceOffset: 0, destOffset: 0, copySize: 1, elemIndex: 0, expOutOfBounds: true}, {sourceOffset: 1, destOffset: 0, copySize: 0, elemIndex: 0, expOutOfBounds: true}, {sourceOffset: 0, destOffset: 0, copySize: 0, elemIndex: 1}, {sourceOffset: 0, destOffset: 0, copySize: 5, elemIndex: 1}, {sourceOffset: 0, destOffset: 0, copySize: 1, elemIndex: 1}, {sourceOffset: 0, destOffset: 0, copySize: 3, elemIndex: 1}, {sourceOffset: 0, destOffset: 1, copySize: 3, elemIndex: 1}, {sourceOffset: 0, destOffset: 7, copySize: 4, elemIndex: 1}, {sourceOffset: 1, destOffset: 7, copySize: 4, elemIndex: 1}, {sourceOffset: 4, destOffset: 7, copySize: 1, elemIndex: 1}, {sourceOffset: 5, destOffset: 7, copySize: 0, elemIndex: 1}, {sourceOffset: 0, destOffset: 7, copySize: 5, elemIndex: 1}, {sourceOffset: 1, destOffset: 0, copySize: 3, elemIndex: 1}, {sourceOffset: 0, destOffset: 1, copySize: 4, elemIndex: 1}, {sourceOffset: 1, destOffset: 1, copySize: 3, elemIndex: 1}, {sourceOffset: 0, destOffset: 10, copySize: 5, elemIndex: 1}, {sourceOffset: 0, destOffset: 0, copySize: 6, elemIndex: 1, expOutOfBounds: true}, {sourceOffset: 6, destOffset: 0, copySize: 0, elemIndex: 1, expOutOfBounds: true}, } for i, tt := range tests { tc := tt t.Run(strconv.Itoa(i), func(t *testing.T) { env := newCompilerEnvironment() env.module().ElementInstances = elementInstances compiler := env.requireNewCompiler(t, &wasm.FunctionType{}, newCompiler, &wazeroir.CompilationResult{ HasElementInstances: true, HasTable: true, }) err := compiler.compilePreamble() require.NoError(t, err) // Compile operands. err = compiler.compileConstI32(operationPtr(wazeroir.NewOperationConstI32(tc.destOffset))) require.NoError(t, err) err = compiler.compileConstI32(operationPtr(wazeroir.NewOperationConstI32(tc.sourceOffset))) require.NoError(t, err) err = compiler.compileConstI32(operationPtr(wazeroir.NewOperationConstI32(tc.copySize))) require.NoError(t, err) err = compiler.compileTableInit(operationPtr(wazeroir.NewOperationTableInit(tc.elemIndex, 0))) require.NoError(t, err) // Setup the table. table := make([]wasm.Reference, tableSize) env.addTable(&wasm.TableInstance{References: table}) for i := 0; i < tableSize; i++ { table[i] = uintptr(i) } code := asm.CodeSegment{} defer func() { require.NoError(t, code.Unmap()) }() // Generate the code under test. err = compiler.compileReturnFunction() require.NoError(t, err) _, err = compiler.compile(code.NextCodeSection()) require.NoError(t, err) // Run code. env.exec(code.Bytes()) if !tc.expOutOfBounds { require.Equal(t, nativeCallStatusCodeReturned, env.compilerStatus()) exp := make([]wasm.Reference, tableSize) for i := 0; i < tableSize; i++ { exp[i] = uintptr(i) } if inst := elementInstances[tc.elemIndex]; inst != nil { copy(exp[tc.destOffset:], inst[tc.sourceOffset:tc.sourceOffset+tc.copySize]) } require.Equal(t, exp, table) } else { require.Equal(t, nativeCallStatusCodeInvalidTableAccess, env.compilerStatus()) } }) } } type dog struct{ name string } func TestCompiler_compileTableSet(t *testing.T) { externDog := &dog{name: "sushi"} externrefOpaque := uintptr(unsafe.Pointer(externDog)) funcref := &function{moduleInstance: &wasm.ModuleInstance{}} funcrefOpaque := uintptr(unsafe.Pointer(funcref)) externTable := &wasm.TableInstance{Type: wasm.RefTypeExternref, References: []wasm.Reference{0, 0, externrefOpaque, 0, 0}} funcrefTable := &wasm.TableInstance{Type: wasm.RefTypeFuncref, References: []wasm.Reference{0, 0, 0, 0, funcrefOpaque}} tables := []*wasm.TableInstance{externTable, funcrefTable} tests := []struct { name string tableIndex uint32 offset uint32 in uintptr expExtern bool expError bool }{ { name: "externref - non nil", tableIndex: 0, offset: 2, in: externrefOpaque, expExtern: true, }, { name: "externref - nil", tableIndex: 0, offset: 1, in: 0, expExtern: true, }, { name: "externref - out of bounds", tableIndex: 0, offset: 10, in: 0, expError: true, }, { name: "funcref - non nil", tableIndex: 1, offset: 4, in: funcrefOpaque, expExtern: false, }, { name: "funcref - nil", tableIndex: 1, offset: 3, in: 0, expExtern: false, }, { name: "funcref - out of bounds", tableIndex: 1, offset: 100000, in: 0, expError: true, }, } for _, tt := range tests { tc := tt t.Run(tc.name, func(t *testing.T) { env := newCompilerEnvironment() for _, table := range tables { env.addTable(table) } compiler := env.requireNewCompiler(t, &wasm.FunctionType{}, newCompiler, &wazeroir.CompilationResult{ HasTable: true, }) err := compiler.compilePreamble() require.NoError(t, err) err = compiler.compileConstI32(operationPtr(wazeroir.NewOperationConstI32(tc.offset))) require.NoError(t, err) err = compiler.compileConstI64(operationPtr(wazeroir.NewOperationConstI64(uint64(tc.in)))) require.NoError(t, err) err = compiler.compileTableSet(operationPtr(wazeroir.NewOperationTableSet(tc.tableIndex))) require.NoError(t, err) code := asm.CodeSegment{} defer func() { require.NoError(t, code.Unmap()) }() // Generate the code under test. err = compiler.compileReturnFunction() require.NoError(t, err) _, err = compiler.compile(code.NextCodeSection()) require.NoError(t, err) // Run code. env.exec(code.Bytes()) if tc.expError { require.Equal(t, nativeCallStatusCodeInvalidTableAccess, env.compilerStatus()) } else { require.Equal(t, nativeCallStatusCodeReturned, env.compilerStatus()) require.Equal(t, uint64(0), env.stackPointer()) if tc.expExtern { actual := dogFromPtr(externTable.References[tc.offset]) exp := externDog if tc.in == 0 { exp = nil } require.Equal(t, exp, actual) } else { actual := functionFromPtr(funcrefTable.References[tc.offset]) exp := funcref if tc.in == 0 { exp = nil } require.Equal(t, exp, actual) } } }) } } //go:nocheckptr ignore "pointer arithmetic result points to invalid allocation" func dogFromPtr(ptr uintptr) *dog { if ptr == 0 { return nil } return (*dog)(unsafe.Pointer(ptr)) } //go:nocheckptr ignore "pointer arithmetic result points to invalid allocation" func functionFromPtr(ptr uintptr) *function { if ptr == 0 { return nil } return (*function)(unsafe.Pointer(ptr)) } func TestCompiler_compileTableGet(t *testing.T) { externDog := &dog{name: "sushi"} externrefOpaque := uintptr(unsafe.Pointer(externDog)) funcref := &function{moduleInstance: &wasm.ModuleInstance{}} funcrefOpaque := uintptr(unsafe.Pointer(funcref)) tables := []*wasm.TableInstance{ {Type: wasm.RefTypeExternref, References: []wasm.Reference{0, 0, externrefOpaque, 0, 0}}, {Type: wasm.RefTypeFuncref, References: []wasm.Reference{0, 0, 0, 0, funcrefOpaque}}, } tests := []struct { name string tableIndex uint32 offset uint32 exp uintptr expError bool }{ { name: "externref - non nil", tableIndex: 0, offset: 2, exp: externrefOpaque, }, { name: "externref - nil", tableIndex: 0, offset: 4, exp: 0, }, { name: "externref - out of bounds", tableIndex: 0, offset: 5, expError: true, }, { name: "funcref - non nil", tableIndex: 1, offset: 4, exp: funcrefOpaque, }, { name: "funcref - nil", tableIndex: 1, offset: 1, exp: 0, }, { name: "funcref - out of bounds", tableIndex: 1, offset: 1000, expError: true, }, } for _, tt := range tests { tc := tt t.Run(tc.name, func(t *testing.T) { env := newCompilerEnvironment() for _, table := range tables { env.addTable(table) } compiler := env.requireNewCompiler(t, &wasm.FunctionType{}, newCompiler, &wazeroir.CompilationResult{ HasTable: true, }) err := compiler.compilePreamble() require.NoError(t, err) err = compiler.compileConstI32(operationPtr(wazeroir.NewOperationConstI32(tc.offset))) require.NoError(t, err) err = compiler.compileTableGet(operationPtr(wazeroir.NewOperationTableGet(tc.tableIndex))) require.NoError(t, err) code := asm.CodeSegment{} defer func() { require.NoError(t, code.Unmap()) }() // Generate the code under test. err = compiler.compileReturnFunction() require.NoError(t, err) _, err = compiler.compile(code.NextCodeSection()) require.NoError(t, err) // Run code. env.exec(code.Bytes()) if tc.expError { require.Equal(t, nativeCallStatusCodeInvalidTableAccess, env.compilerStatus()) } else { require.Equal(t, nativeCallStatusCodeReturned, env.compilerStatus()) require.Equal(t, uint64(1), env.stackPointer()) require.Equal(t, uint64(tc.exp), env.stackTopAsUint64()) } }) } } func TestCompiler_compileRefFunc(t *testing.T) { env := newCompilerEnvironment() compiler := env.requireNewCompiler(t, &wasm.FunctionType{}, newCompiler, &wazeroir.CompilationResult{}) err := compiler.compilePreamble() require.NoError(t, err) me := env.moduleEngine() const numFuncs = 20 for i := 0; i < numFuncs; i++ { me.functions = append(me.functions, function{moduleInstance: &wasm.ModuleInstance{}}) } for i := 0; i < numFuncs; i++ { i := i t.Run(strconv.Itoa(i), func(t *testing.T) { compiler := env.requireNewCompiler(t, &wasm.FunctionType{}, newCompiler, &wazeroir.CompilationResult{}) err := compiler.compilePreamble() require.NoError(t, err) err = compiler.compileRefFunc(operationPtr(wazeroir.NewOperationRefFunc(uint32(i)))) require.NoError(t, err) code := asm.CodeSegment{} defer func() { require.NoError(t, code.Unmap()) }() // Generate the code under test. err = compiler.compileReturnFunction() require.NoError(t, err) _, err = compiler.compile(code.NextCodeSection()) require.NoError(t, err) // Run code. env.exec(code.Bytes()) require.Equal(t, nativeCallStatusCodeReturned, env.compilerStatus()) require.Equal(t, uint64(1), env.stackPointer()) require.Equal(t, uintptr(unsafe.Pointer(&me.functions[i])), uintptr(env.stackTopAsUint64())) }) } }