// Copyright 2018 the V8 project authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "include/v8.h" #include "src/api-inl.h" #include "src/builtins/builtins.h" #include "src/heap/spaces.h" #include "src/isolate.h" #include "src/objects/code-inl.h" #include "test/cctest/cctest.h" namespace v8 { namespace internal { namespace test_unwinder { static void* unlimited_stack_base = std::numeric_limits::max(); TEST(Unwind_BadState_Fail) { UnwindState unwind_state; // Fields are intialized to nullptr. RegisterState register_state; bool unwound = v8::Unwinder::TryUnwindV8Frames(unwind_state, ®ister_state, unlimited_stack_base); CHECK(!unwound); // The register state should not change when unwinding fails. CHECK_NULL(register_state.fp); CHECK_NULL(register_state.sp); CHECK_NULL(register_state.pc); } TEST(Unwind_BuiltinPCInMiddle_Success) { LocalContext env; v8::Isolate* isolate = env->GetIsolate(); Isolate* i_isolate = reinterpret_cast(isolate); UnwindState unwind_state = isolate->GetUnwindState(); RegisterState register_state; uintptr_t stack[3]; void* stack_base = stack + arraysize(stack); stack[0] = reinterpret_cast(stack + 2); // saved FP (rbp). stack[1] = 202; // Return address into C++ code. stack[2] = 303; // The SP points here in the caller's frame. register_state.sp = stack; register_state.fp = stack; // Put the current PC inside of a valid builtin. Code builtin = i_isolate->builtins()->builtin(Builtins::kStringEqual); const uintptr_t offset = 40; CHECK_LT(offset, builtin->InstructionSize()); register_state.pc = reinterpret_cast(builtin->InstructionStart() + offset); bool unwound = v8::Unwinder::TryUnwindV8Frames(unwind_state, ®ister_state, stack_base); CHECK(unwound); CHECK_EQ(reinterpret_cast(stack + 2), register_state.fp); CHECK_EQ(reinterpret_cast(stack + 2), register_state.sp); CHECK_EQ(reinterpret_cast(202), register_state.pc); } // The unwinder should be able to unwind even if we haven't properly set up the // current frame, as long as there is another JS frame underneath us (i.e. as // long as the PC isn't in JSEntry). This test puts the PC at the start // of a JS builtin and creates a fake JSEntry frame before it on the stack. The // unwinder should be able to unwind to the C++ frame before the JSEntry frame. TEST(Unwind_BuiltinPCAtStart_Success) { LocalContext env; v8::Isolate* isolate = env->GetIsolate(); Isolate* i_isolate = reinterpret_cast(isolate); UnwindState unwind_state = isolate->GetUnwindState(); RegisterState register_state; const size_t code_length = 40; uintptr_t code[code_length] = {0}; unwind_state.code_range.start = code; unwind_state.code_range.length_in_bytes = code_length * sizeof(uintptr_t); uintptr_t stack[6]; void* stack_base = stack + arraysize(stack); stack[0] = 101; // Return address into JS code. It doesn't matter that this is not actually in // JSEntry, because we only check that for the top frame. stack[1] = reinterpret_cast(code + 10); stack[2] = reinterpret_cast(stack + 5); // saved FP (rbp). stack[3] = 303; // Return address into C++ code. stack[4] = 404; stack[5] = 505; register_state.sp = stack; register_state.fp = stack + 2; // FP to the JSEntry frame. // Put the current PC at the start of a valid builtin, so that we are setting // up the frame. Code builtin = i_isolate->builtins()->builtin(Builtins::kStringEqual); register_state.pc = reinterpret_cast(builtin->InstructionStart()); bool unwound = v8::Unwinder::TryUnwindV8Frames(unwind_state, ®ister_state, stack_base); CHECK(unwound); CHECK_EQ(reinterpret_cast(stack + 5), register_state.fp); CHECK_EQ(reinterpret_cast(stack + 4), register_state.sp); CHECK_EQ(reinterpret_cast(303), register_state.pc); } const char* foo_source = R"( function foo(a, b) { let x = a * b; let y = x ^ b; let z = y / a; return x + y - z; } foo(1, 2); foo(1, 2); %OptimizeFunctionOnNextCall(foo); foo(1, 2); )"; // Check that we can unwind when the pc is within an optimized code object on // the V8 heap. TEST(Unwind_CodeObjectPCInMiddle_Success) { FLAG_allow_natives_syntax = true; LocalContext env; v8::Isolate* isolate = env->GetIsolate(); Isolate* i_isolate = reinterpret_cast(isolate); HandleScope scope(i_isolate); UnwindState unwind_state = isolate->GetUnwindState(); RegisterState register_state; uintptr_t stack[3]; void* stack_base = stack + arraysize(stack); stack[0] = reinterpret_cast(stack + 2); // saved FP (rbp). stack[1] = 202; // Return address into C++ code. stack[2] = 303; // The SP points here in the caller's frame. register_state.sp = stack; register_state.fp = stack; // Create an on-heap code object. Make sure we run the function so that it is // compiled and not just marked for lazy compilation. CompileRun(foo_source); v8::Local local_foo = v8::Local::Cast( env.local()->Global()->Get(env.local(), v8_str("foo")).ToLocalChecked()); Handle foo = Handle::cast(v8::Utils::OpenHandle(*local_foo)); // Put the current PC inside of the created code object. AbstractCode abstract_code = foo->abstract_code(); // We don't produce optimized code when run with --no-opt. if (!abstract_code->IsCode() && FLAG_opt == false) return; CHECK(abstract_code->IsCode()); Code code = abstract_code->GetCode(); // We don't want the offset too early or it could be the `push rbp` // instruction (which is not at the start of generated code, because the lazy // deopt check happens before frame setup). const uintptr_t offset = code->InstructionSize() - 20; CHECK_LT(offset, code->InstructionSize()); Address pc = code->InstructionStart() + offset; register_state.pc = reinterpret_cast(pc); // Check that the created code is within the code range that we get from the // API. Address start = reinterpret_cast
(unwind_state.code_range.start); CHECK(pc >= start && pc < start + unwind_state.code_range.length_in_bytes); bool unwound = v8::Unwinder::TryUnwindV8Frames(unwind_state, ®ister_state, stack_base); CHECK(unwound); CHECK_EQ(reinterpret_cast(stack + 2), register_state.fp); CHECK_EQ(reinterpret_cast(stack + 2), register_state.sp); CHECK_EQ(reinterpret_cast(202), register_state.pc); } // If the PC is within JSEntry but we haven't set up the frame yet, then we // cannot unwind. TEST(Unwind_JSEntryBeforeFrame_Fail) { LocalContext env; v8::Isolate* isolate = env->GetIsolate(); UnwindState unwind_state = isolate->GetUnwindState(); RegisterState register_state; const size_t code_length = 40; uintptr_t code[code_length] = {0}; unwind_state.code_range.start = code; unwind_state.code_range.length_in_bytes = code_length * sizeof(uintptr_t); // Pretend that it takes 5 instructions to set up the frame in JSEntry. unwind_state.js_entry_stub.code.start = code + 10; unwind_state.js_entry_stub.code.length_in_bytes = 10 * sizeof(uintptr_t); uintptr_t stack[10]; void* stack_base = stack + arraysize(stack); stack[0] = 101; stack[1] = 111; stack[2] = 121; stack[3] = 131; stack[4] = 141; stack[5] = 151; stack[6] = 100; // Return address into C++ code. stack[7] = 303; // The SP points here in the caller's frame. stack[8] = 404; stack[9] = 505; register_state.sp = stack + 5; register_state.fp = stack + 9; // Put the current PC inside of JSEntry, before the frame is set up. register_state.pc = code + 12; bool unwound = v8::Unwinder::TryUnwindV8Frames(unwind_state, ®ister_state, stack_base); CHECK(!unwound); // The register state should not change when unwinding fails. CHECK_EQ(reinterpret_cast(stack + 9), register_state.fp); CHECK_EQ(reinterpret_cast(stack + 5), register_state.sp); CHECK_EQ(code + 12, register_state.pc); // Change the PC to a few instructions later, after the frame is set up. register_state.pc = code + 16; unwound = v8::Unwinder::TryUnwindV8Frames(unwind_state, ®ister_state, stack_base); // TODO(petermarshall): More precisely check position within JSEntry rather // than just assuming the frame is unreadable. CHECK(!unwound); // The register state should not change when unwinding fails. CHECK_EQ(reinterpret_cast(stack + 9), register_state.fp); CHECK_EQ(reinterpret_cast(stack + 5), register_state.sp); CHECK_EQ(code + 16, register_state.pc); } TEST(Unwind_OneJSFrame_Success) { LocalContext env; v8::Isolate* isolate = env->GetIsolate(); UnwindState unwind_state = isolate->GetUnwindState(); RegisterState register_state; // Use a fake code range so that we can initialize it to 0s. const size_t code_length = 40; uintptr_t code[code_length] = {0}; unwind_state.code_range.start = code; unwind_state.code_range.length_in_bytes = code_length * sizeof(uintptr_t); // Our fake stack has two frames - one C++ frame and one JS frame (on top). // The stack grows from high addresses to low addresses. uintptr_t stack[10]; void* stack_base = stack + arraysize(stack); stack[0] = 101; stack[1] = 111; stack[2] = 121; stack[3] = 131; stack[4] = 141; stack[5] = reinterpret_cast(stack + 9); // saved FP (rbp). stack[6] = 100; // Return address into C++ code. stack[7] = 303; // The SP points here in the caller's frame. stack[8] = 404; stack[9] = 505; register_state.sp = stack; register_state.fp = stack + 5; // Put the current PC inside of the code range so it looks valid. register_state.pc = code + 30; bool unwound = v8::Unwinder::TryUnwindV8Frames(unwind_state, ®ister_state, stack_base); CHECK(unwound); CHECK_EQ(reinterpret_cast(stack + 9), register_state.fp); CHECK_EQ(reinterpret_cast(stack + 7), register_state.sp); CHECK_EQ(reinterpret_cast(100), register_state.pc); } // Creates a fake stack with two JS frames on top of a C++ frame and checks that // the unwinder correctly unwinds past the JS frames and returns the C++ frame's // details. TEST(Unwind_TwoJSFrames_Success) { LocalContext env; v8::Isolate* isolate = env->GetIsolate(); UnwindState unwind_state = isolate->GetUnwindState(); RegisterState register_state; // Use a fake code range so that we can initialize it to 0s. const size_t code_length = 40; uintptr_t code[code_length] = {0}; unwind_state.code_range.start = code; unwind_state.code_range.length_in_bytes = code_length * sizeof(uintptr_t); // Our fake stack has three frames - one C++ frame and two JS frames (on top). // The stack grows from high addresses to low addresses. uintptr_t stack[10]; void* stack_base = stack + arraysize(stack); stack[0] = 101; stack[1] = 111; stack[2] = reinterpret_cast(stack + 5); // saved FP (rbp). // The fake return address is in the JS code range. stack[3] = reinterpret_cast(code + 10); stack[4] = 141; stack[5] = reinterpret_cast(stack + 9); // saved FP (rbp). stack[6] = 100; // Return address into C++ code. stack[7] = 303; // The SP points here in the caller's frame. stack[8] = 404; stack[9] = 505; register_state.sp = stack; register_state.fp = stack + 2; // Put the current PC inside of the code range so it looks valid. register_state.pc = code + 30; bool unwound = v8::Unwinder::TryUnwindV8Frames(unwind_state, ®ister_state, stack_base); CHECK(unwound); CHECK_EQ(reinterpret_cast(stack + 9), register_state.fp); CHECK_EQ(reinterpret_cast(stack + 7), register_state.sp); CHECK_EQ(reinterpret_cast(100), register_state.pc); } // If the PC is in JSEntry then the frame might not be set up correctly, meaning // we can't unwind the stack properly. TEST(Unwind_JSEntry_Fail) { LocalContext env; v8::Isolate* isolate = env->GetIsolate(); Isolate* i_isolate = reinterpret_cast(isolate); UnwindState unwind_state = isolate->GetUnwindState(); RegisterState register_state; Code js_entry = i_isolate->heap()->builtin(Builtins::kJSEntry); byte* start = reinterpret_cast(js_entry->InstructionStart()); register_state.pc = start + 10; bool unwound = v8::Unwinder::TryUnwindV8Frames(unwind_state, ®ister_state, unlimited_stack_base); CHECK(!unwound); // The register state should not change when unwinding fails. CHECK_NULL(register_state.fp); CHECK_NULL(register_state.sp); CHECK_EQ(start + 10, register_state.pc); } TEST(Unwind_StackBounds_Basic) { LocalContext env; v8::Isolate* isolate = env->GetIsolate(); UnwindState unwind_state = isolate->GetUnwindState(); RegisterState register_state; const size_t code_length = 10; uintptr_t code[code_length] = {0}; unwind_state.code_range.start = code; unwind_state.code_range.length_in_bytes = code_length * sizeof(uintptr_t); uintptr_t stack[3]; stack[0] = reinterpret_cast(stack + 2); // saved FP (rbp). stack[1] = 202; // Return address into C++ code. stack[2] = 303; // The SP points here in the caller's frame. register_state.sp = stack; register_state.fp = stack; register_state.pc = code; void* wrong_stack_base = reinterpret_cast( reinterpret_cast(stack) - sizeof(uintptr_t)); bool unwound = v8::Unwinder::TryUnwindV8Frames(unwind_state, ®ister_state, wrong_stack_base); CHECK(!unwound); // Correct the stack base and unwinding should succeed. void* correct_stack_base = stack + arraysize(stack); unwound = v8::Unwinder::TryUnwindV8Frames(unwind_state, ®ister_state, correct_stack_base); CHECK(unwound); } TEST(Unwind_StackBounds_WithUnwinding) { LocalContext env; v8::Isolate* isolate = env->GetIsolate(); UnwindState unwind_state = isolate->GetUnwindState(); RegisterState register_state; // Use a fake code range so that we can initialize it to 0s. const size_t code_length = 40; uintptr_t code[code_length] = {0}; unwind_state.code_range.start = code; unwind_state.code_range.length_in_bytes = code_length * sizeof(uintptr_t); // Our fake stack has two frames - one C++ frame and one JS frame (on top). // The stack grows from high addresses to low addresses. uintptr_t stack[11]; void* stack_base = stack + arraysize(stack); stack[0] = 101; stack[1] = 111; stack[2] = 121; stack[3] = 131; stack[4] = 141; stack[5] = reinterpret_cast(stack + 9); // saved FP (rbp). stack[6] = reinterpret_cast(code + 20); // JS code. stack[7] = 303; // The SP points here in the caller's frame. stack[8] = 404; stack[9] = reinterpret_cast(stack) + (12 * sizeof(uintptr_t)); // saved FP (OOB). stack[10] = reinterpret_cast(code + 20); // JS code. register_state.sp = stack; register_state.fp = stack + 5; // Put the current PC inside of the code range so it looks valid. register_state.pc = code + 30; // Unwind will fail because stack[9] FP points outside of the stack. bool unwound = v8::Unwinder::TryUnwindV8Frames(unwind_state, ®ister_state, stack_base); CHECK(!unwound); // Change the return address so that it is not in range. stack[10] = 202; unwound = v8::Unwinder::TryUnwindV8Frames(unwind_state, ®ister_state, stack_base); CHECK(!unwound); } TEST(PCIsInV8_BadState_Fail) { UnwindState unwind_state; void* pc = nullptr; CHECK(!v8::Unwinder::PCIsInV8(unwind_state, pc)); } TEST(PCIsInV8_ValidStateNullPC_Fail) { LocalContext env; v8::Isolate* isolate = env->GetIsolate(); UnwindState unwind_state = isolate->GetUnwindState(); void* pc = nullptr; CHECK(!v8::Unwinder::PCIsInV8(unwind_state, pc)); } void TestRangeBoundaries(const UnwindState& unwind_state, byte* range_start, size_t range_length) { void* pc = range_start - 1; CHECK(!v8::Unwinder::PCIsInV8(unwind_state, pc)); pc = range_start; CHECK(v8::Unwinder::PCIsInV8(unwind_state, pc)); pc = range_start + 1; CHECK(v8::Unwinder::PCIsInV8(unwind_state, pc)); pc = range_start + range_length - 1; CHECK(v8::Unwinder::PCIsInV8(unwind_state, pc)); pc = range_start + range_length; CHECK(!v8::Unwinder::PCIsInV8(unwind_state, pc)); pc = range_start + range_length + 1; CHECK(!v8::Unwinder::PCIsInV8(unwind_state, pc)); } TEST(PCIsInV8_InCodeOrEmbeddedRange) { LocalContext env; v8::Isolate* isolate = env->GetIsolate(); UnwindState unwind_state = isolate->GetUnwindState(); byte* code_range_start = const_cast( reinterpret_cast(unwind_state.code_range.start)); size_t code_range_length = unwind_state.code_range.length_in_bytes; TestRangeBoundaries(unwind_state, code_range_start, code_range_length); byte* embedded_range_start = const_cast( reinterpret_cast(unwind_state.embedded_code_range.start)); size_t embedded_range_length = unwind_state.embedded_code_range.length_in_bytes; TestRangeBoundaries(unwind_state, embedded_range_start, embedded_range_length); } // PCIsInV8 doesn't check if the PC is in JSEntrydirectly. It's assumed that the // CodeRange or EmbeddedCodeRange contain JSEntry. TEST(PCIsInV8_InJSEntryRange) { LocalContext env; v8::Isolate* isolate = env->GetIsolate(); Isolate* i_isolate = reinterpret_cast(isolate); UnwindState unwind_state = isolate->GetUnwindState(); Code js_entry = i_isolate->heap()->builtin(Builtins::kJSEntry); byte* start = reinterpret_cast(js_entry->InstructionStart()); size_t length = js_entry->InstructionSize(); void* pc = start; CHECK(v8::Unwinder::PCIsInV8(unwind_state, pc)); pc = start + 1; CHECK(v8::Unwinder::PCIsInV8(unwind_state, pc)); pc = start + length - 1; CHECK(v8::Unwinder::PCIsInV8(unwind_state, pc)); } // Large code objects can be allocated in large object space. Check that this is // inside the CodeRange. TEST(PCIsInV8_LargeCodeObject) { FLAG_allow_natives_syntax = true; LocalContext env; v8::Isolate* isolate = env->GetIsolate(); Isolate* i_isolate = reinterpret_cast(isolate); HandleScope scope(i_isolate); UnwindState unwind_state = isolate->GetUnwindState(); // Create a big function that ends up in CODE_LO_SPACE. const int instruction_size = Page::kPageSize + 1; STATIC_ASSERT(instruction_size > kMaxRegularHeapObjectSize); std::unique_ptr instructions(new byte[instruction_size]); CodeDesc desc; desc.buffer = instructions.get(); desc.buffer_size = instruction_size; desc.instr_size = instruction_size; desc.reloc_size = 0; desc.constant_pool_size = 0; desc.unwinding_info = nullptr; desc.unwinding_info_size = 0; desc.origin = nullptr; Handle self_ref; Handle foo_code = i_isolate->factory()->NewCode(desc, Code::WASM_FUNCTION, self_ref); CHECK(i_isolate->heap()->InSpace(*foo_code, CODE_LO_SPACE)); byte* start = reinterpret_cast(foo_code->InstructionStart()); void* pc = start; CHECK(v8::Unwinder::PCIsInV8(unwind_state, pc)); } } // namespace test_unwinder } // namespace internal } // namespace v8