// Copyright 2019 The Chromium 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 "chrome/renderer/v8_unwinder.h" #include #include #include #include #include "base/bind.h" #include "base/callback.h" #include "base/location.h" #include "base/profiler/module_cache.h" #include "base/profiler/stack_sampling_profiler_test_util.h" #include "base/stl_util.h" #include "base/synchronization/waitable_event.h" #include "base/test/bind.h" #include "base/test/task_environment.h" #include "build/build_config.h" #include "gin/public/isolate_holder.h" #include "testing/gtest/include/gtest/gtest.h" #include "v8/include/v8.h" namespace { class TestModule : public base::ModuleCache::Module { public: TestModule(uintptr_t base_address, size_t size) : base_address_(base_address), size_(size) {} uintptr_t GetBaseAddress() const override { return base_address_; } std::string GetId() const override { return ""; } base::FilePath GetDebugBasename() const override { return base::FilePath(); } size_t GetSize() const override { return size_; } bool IsNative() const override { return true; } private: const uintptr_t base_address_; const size_t size_; }; v8::Local ToV8String(const char* str) { return v8::String::NewFromUtf8(v8::Isolate::GetCurrent(), str) .ToLocalChecked(); } v8::Local CreatePointerHolder(const void* ptr) { v8::Local object_template = v8::ObjectTemplate::New(v8::Isolate::GetCurrent()); object_template->SetInternalFieldCount(1); v8::Local holder = object_template ->NewInstance(v8::Isolate::GetCurrent()->GetCurrentContext()) .ToLocalChecked(); holder->SetAlignedPointerInInternalField(0, const_cast(ptr)); return holder; } template T* GetPointerFromHolder(v8::Local holder) { return reinterpret_cast(holder->GetAlignedPointerFromInternalField(0)); } // Sets up the environment necessary to execute V8 code. class ScopedV8Environment { public: ScopedV8Environment() : isolate_holder_(task_environment_.GetMainThreadTaskRunner(), gin::IsolateHolder::IsolateType::kBlinkMainThread) { isolate()->Enter(); v8::HandleScope handle_scope(isolate()); context_.Reset(isolate(), v8::Context::New(isolate())); v8::Local::New(isolate(), context_)->Enter(); } ~ScopedV8Environment() { { v8::HandleScope handle_scope(isolate()); v8::Local::New(isolate(), context_)->Exit(); context_.Reset(); } isolate()->Exit(); } v8::Isolate* isolate() { return isolate_holder_.isolate(); } private: base::test::TaskEnvironment task_environment_; gin::IsolateHolder isolate_holder_; v8::Persistent context_; }; // C++ function to be invoked from V8 which calls back into the provided closure // pointer (passed via a holder object) to wait for a stack sample to be taken. void WaitForSampleNative(const v8::FunctionCallbackInfo& info) { base::OnceClosure* wait_for_sample = GetPointerFromHolder(info[0].As()); if (wait_for_sample) std::move(*wait_for_sample).Run(); } // Causes a stack sample to be taken after setting up a call stack from C++ to // JavaScript and back into C++. base::FunctionAddressRange CallThroughV8( const base::RepeatingCallback& report_isolate, base::OnceClosure wait_for_sample) { const void* start_program_counter = base::GetProgramCounter(); if (wait_for_sample) { // Set up V8 runtime environment. // Allows use of natives (functions starting with '%') within JavaScript // code, which allows us to control compilation of the JavaScript function // we define. // TODO(wittman): The flag should be set only for the duration of this test // but the V8 API currently doesn't support this. http://crbug.com/v8/9210 // covers adding the necessary functionality to V8. v8::V8::SetFlagsFromString("--allow-natives-syntax"); ScopedV8Environment v8_environment; v8::Isolate* isolate = v8_environment.isolate(); report_isolate.Run(isolate); v8::HandleScope handle_scope(isolate); v8::Local context = isolate->GetCurrentContext(); // Define a V8 function WaitForSampleNative() backed by the C++ function // WaitForSampleNative(). v8::Local js_wait_for_sample_native_template = v8::FunctionTemplate::New(isolate, WaitForSampleNative); v8::Local js_wait_for_sample_native = js_wait_for_sample_native_template->GetFunction(context) .ToLocalChecked(); js_wait_for_sample_native->SetName(ToV8String("WaitForSampleNative")); context->Global() ->Set(context, ToV8String("WaitForSampleNative"), js_wait_for_sample_native) .FromJust(); // Run a script to create the V8 function waitForSample() that invokes // WaitForSampleNative(), and a function that ensures that waitForSample() // gets compiled. waitForSample() just passes the holder object for the // pointer to the wait_for_sample Closure back into the C++ code. We ensure // that the function is compiled to test walking through both builtin and // runtime-generated code. const char kWaitForSampleJs[] = R"( function waitForSample(closure_pointer_holder) { if (closure_pointer_holder) WaitForSampleNative(closure_pointer_holder); } // Set up the function to be compiled rather than interpreted. function compileWaitForSample(closure_pointer_holder) { %PrepareFunctionForOptimization(waitForSample); waitForSample(closure_pointer_holder); waitForSample(closure_pointer_holder); %OptimizeFunctionOnNextCall(waitForSample); } )"; v8::Local script = v8::Script::Compile(context, ToV8String(kWaitForSampleJs)) .ToLocalChecked(); script->Run(context).ToLocalChecked(); // Run compileWaitForSample(), using a null closure pointer to avoid // actually waiting. v8::Local js_compile_wait_for_sample = v8::Local::Cast( context->Global() ->Get(context, ToV8String("compileWaitForSample")) .ToLocalChecked()); v8::Local argv[] = {CreatePointerHolder(nullptr)}; js_compile_wait_for_sample ->Call(context, v8::Undefined(isolate), base::size(argv), argv) .ToLocalChecked(); // Run waitForSample() with the real closure pointer. argv[0] = CreatePointerHolder(&wait_for_sample); v8::Local js_wait_for_sample = v8::Local::Cast( context->Global() ->Get(context, ToV8String("waitForSample")) .ToLocalChecked()); js_wait_for_sample ->Call(context, v8::Undefined(isolate), base::size(argv), argv) .ToLocalChecked(); } // Volatile to prevent a tail call to GetProgramCounter(). const void* volatile end_program_counter = base::GetProgramCounter(); return {start_program_counter, end_program_counter}; } class UpdateModulesTestUnwinder : public V8Unwinder { public: explicit UpdateModulesTestUnwinder(v8::Isolate* isolate) : V8Unwinder(isolate) {} void SetCodePages(std::vector code_pages) { code_pages_to_provide_ = code_pages; } protected: size_t CopyCodePages(size_t capacity, v8::MemoryRange* code_pages) override { std::copy_n(code_pages_to_provide_.begin(), std::min(capacity, code_pages_to_provide_.size()), code_pages); return code_pages_to_provide_.size(); } private: std::vector code_pages_to_provide_; }; v8::MemoryRange GetEmbeddedCodeRange(v8::Isolate* isolate) { v8::MemoryRange range; isolate->GetEmbeddedCodeRange(&range.start, &range.length_in_bytes); return range; } } // namespace TEST(V8UnwinderTest, EmbeddedCodeRangeModule) { ScopedV8Environment v8_environment; V8Unwinder unwinder(v8_environment.isolate()); base::ModuleCache module_cache; unwinder.AddInitialModules(&module_cache); v8::MemoryRange embedded_code_range; v8_environment.isolate()->GetEmbeddedCodeRange( &embedded_code_range.start, &embedded_code_range.length_in_bytes); const base::ModuleCache::Module* module = module_cache.GetModuleForAddress( reinterpret_cast(embedded_code_range.start)); ASSERT_NE(nullptr, module); EXPECT_EQ(V8Unwinder::kV8EmbeddedCodeRangeBuildId, module->GetId()); } TEST(V8UnwinderTest, EmbeddedCodeRangeModulePreservedOnUpdate) { ScopedV8Environment v8_environment; UpdateModulesTestUnwinder unwinder(v8_environment.isolate()); base::ModuleCache module_cache; unwinder.AddInitialModules(&module_cache); unwinder.SetCodePages({{reinterpret_cast(1), 10}, GetEmbeddedCodeRange(v8_environment.isolate())}); unwinder.OnStackCapture(); unwinder.UpdateModules(&module_cache); v8::MemoryRange embedded_code_range; v8_environment.isolate()->GetEmbeddedCodeRange( &embedded_code_range.start, &embedded_code_range.length_in_bytes); const base::ModuleCache::Module* module = module_cache.GetModuleForAddress( reinterpret_cast(embedded_code_range.start)); ASSERT_NE(nullptr, module); EXPECT_EQ(V8Unwinder::kV8EmbeddedCodeRangeBuildId, module->GetId()); } // Checks that the embedded code range is preserved even if it wasn't included // in the code pages due to insufficient capacity. TEST(V8UnwinderTest, EmbeddedCodeRangeModulePreservedOnOverCapacityUpdate) { ScopedV8Environment v8_environment; UpdateModulesTestUnwinder unwinder(v8_environment.isolate()); base::ModuleCache module_cache; unwinder.AddInitialModules(&module_cache); const int kDefaultCapacity = v8::Isolate::kMinCodePagesBufferSize; std::vector code_pages; code_pages.reserve(kDefaultCapacity + 1); for (int i = 0; i < kDefaultCapacity + 1; ++i) code_pages.push_back({reinterpret_cast(i + 1), 1}); unwinder.SetCodePages(code_pages); unwinder.OnStackCapture(); unwinder.UpdateModules(&module_cache); v8::MemoryRange embedded_code_range; v8_environment.isolate()->GetEmbeddedCodeRange( &embedded_code_range.start, &embedded_code_range.length_in_bytes); const base::ModuleCache::Module* module = module_cache.GetModuleForAddress( reinterpret_cast(embedded_code_range.start)); ASSERT_NE(nullptr, module); EXPECT_EQ(V8Unwinder::kV8EmbeddedCodeRangeBuildId, module->GetId()); } TEST(V8UnwinderTest, UpdateModules_ModuleAdded) { ScopedV8Environment v8_environment; UpdateModulesTestUnwinder unwinder(v8_environment.isolate()); base::ModuleCache module_cache; unwinder.AddInitialModules(&module_cache); unwinder.SetCodePages({{reinterpret_cast(1), 10}, GetEmbeddedCodeRange(v8_environment.isolate())}); unwinder.OnStackCapture(); unwinder.UpdateModules(&module_cache); const base::ModuleCache::Module* module = module_cache.GetModuleForAddress(1); ASSERT_NE(nullptr, module); EXPECT_EQ(1u, module->GetBaseAddress()); EXPECT_EQ(10u, module->GetSize()); EXPECT_EQ(V8Unwinder::kV8CodeRangeBuildId, module->GetId()); EXPECT_EQ("V8 Code Range", module->GetDebugBasename().MaybeAsASCII()); } // Check that modules added before the last module are propagated to the // ModuleCache. This case takes a different code path in the implementation. TEST(V8UnwinderTest, UpdateModules_ModuleAddedBeforeLast) { ScopedV8Environment v8_environment; UpdateModulesTestUnwinder unwinder(v8_environment.isolate()); base::ModuleCache module_cache; unwinder.AddInitialModules(&module_cache); unwinder.SetCodePages({{reinterpret_cast(100), 10}, GetEmbeddedCodeRange(v8_environment.isolate())}); unwinder.OnStackCapture(); unwinder.UpdateModules(&module_cache); unwinder.SetCodePages({{reinterpret_cast(1), 10}, {reinterpret_cast(100), 10}, GetEmbeddedCodeRange(v8_environment.isolate())}); unwinder.OnStackCapture(); unwinder.UpdateModules(&module_cache); const base::ModuleCache::Module* module = module_cache.GetModuleForAddress(1); ASSERT_NE(nullptr, module); EXPECT_EQ(1u, module->GetBaseAddress()); EXPECT_EQ(10u, module->GetSize()); EXPECT_EQ(V8Unwinder::kV8CodeRangeBuildId, module->GetId()); EXPECT_EQ("V8 Code Range", module->GetDebugBasename().MaybeAsASCII()); } TEST(V8UnwinderTest, UpdateModules_ModuleRetained) { ScopedV8Environment v8_environment; UpdateModulesTestUnwinder unwinder(v8_environment.isolate()); base::ModuleCache module_cache; unwinder.AddInitialModules(&module_cache); unwinder.SetCodePages({{reinterpret_cast(1), 10}, GetEmbeddedCodeRange(v8_environment.isolate())}); unwinder.OnStackCapture(); unwinder.UpdateModules(&module_cache); // Code pages remain the same for this stack capture. unwinder.OnStackCapture(); unwinder.UpdateModules(&module_cache); const base::ModuleCache::Module* module = module_cache.GetModuleForAddress(1); ASSERT_NE(nullptr, module); EXPECT_EQ(1u, module->GetBaseAddress()); EXPECT_EQ(10u, module->GetSize()); EXPECT_EQ(V8Unwinder::kV8CodeRangeBuildId, module->GetId()); EXPECT_EQ("V8 Code Range", module->GetDebugBasename().MaybeAsASCII()); } TEST(V8UnwinderTest, UpdateModules_ModuleRetainedWithDifferentSize) { ScopedV8Environment v8_environment; UpdateModulesTestUnwinder unwinder(v8_environment.isolate()); base::ModuleCache module_cache; unwinder.AddInitialModules(&module_cache); unwinder.SetCodePages({{reinterpret_cast(1), 10}, GetEmbeddedCodeRange(v8_environment.isolate())}); unwinder.OnStackCapture(); unwinder.UpdateModules(&module_cache); // Code pages remain the same for this stack capture. unwinder.SetCodePages({{reinterpret_cast(1), 20}, GetEmbeddedCodeRange(v8_environment.isolate())}); unwinder.OnStackCapture(); unwinder.UpdateModules(&module_cache); const base::ModuleCache::Module* module = module_cache.GetModuleForAddress(11); ASSERT_NE(nullptr, module); EXPECT_EQ(1u, module->GetBaseAddress()); EXPECT_EQ(20u, module->GetSize()); } TEST(V8UnwinderTest, UpdateModules_ModuleRemoved) { ScopedV8Environment v8_environment; UpdateModulesTestUnwinder unwinder(v8_environment.isolate()); base::ModuleCache module_cache; unwinder.AddInitialModules(&module_cache); unwinder.SetCodePages({{{reinterpret_cast(1), 10}, GetEmbeddedCodeRange(v8_environment.isolate())}}); unwinder.OnStackCapture(); unwinder.UpdateModules(&module_cache); unwinder.SetCodePages({GetEmbeddedCodeRange(v8_environment.isolate())}); unwinder.OnStackCapture(); unwinder.UpdateModules(&module_cache); EXPECT_EQ(nullptr, module_cache.GetModuleForAddress(1)); } // Check that modules removed before the last module are propagated to the // ModuleCache. This case takes a different code path in the implementation. TEST(V8UnwinderTest, UpdateModules_ModuleRemovedBeforeLast) { ScopedV8Environment v8_environment; UpdateModulesTestUnwinder unwinder(v8_environment.isolate()); base::ModuleCache module_cache; unwinder.AddInitialModules(&module_cache); unwinder.SetCodePages({{{reinterpret_cast(1), 10}, {reinterpret_cast(100), 10}, GetEmbeddedCodeRange(v8_environment.isolate())}}); unwinder.OnStackCapture(); unwinder.UpdateModules(&module_cache); unwinder.SetCodePages({{reinterpret_cast(100), 10}, GetEmbeddedCodeRange(v8_environment.isolate())}); unwinder.OnStackCapture(); unwinder.UpdateModules(&module_cache); EXPECT_EQ(nullptr, module_cache.GetModuleForAddress(1)); } TEST(V8UnwinderTest, UpdateModules_CapacityExceeded) { ScopedV8Environment v8_environment; UpdateModulesTestUnwinder unwinder(v8_environment.isolate()); base::ModuleCache module_cache; unwinder.AddInitialModules(&module_cache); const int kDefaultCapacity = v8::Isolate::kMinCodePagesBufferSize; std::vector code_pages; // Create kDefaultCapacity + 2 code pages, with the last being the embedded // code page. code_pages.reserve(kDefaultCapacity + 2); for (int i = 0; i < kDefaultCapacity + 1; ++i) code_pages.push_back({reinterpret_cast(i + 1), 1}); code_pages.push_back(GetEmbeddedCodeRange(v8_environment.isolate())); // The first sample should successfully create modules up to the default // capacity. unwinder.SetCodePages(code_pages); unwinder.OnStackCapture(); unwinder.UpdateModules(&module_cache); EXPECT_NE(nullptr, module_cache.GetModuleForAddress(kDefaultCapacity)); EXPECT_EQ(nullptr, module_cache.GetModuleForAddress(kDefaultCapacity + 1)); // The capacity should be expanded by the second sample. unwinder.SetCodePages(code_pages); unwinder.OnStackCapture(); unwinder.UpdateModules(&module_cache); EXPECT_NE(nullptr, module_cache.GetModuleForAddress(kDefaultCapacity)); EXPECT_NE(nullptr, module_cache.GetModuleForAddress(kDefaultCapacity + 1)); } // Checks that the implementation can handle the capacity being exceeded by a // large amount. TEST(V8UnwinderTest, UpdateModules_CapacitySubstantiallyExceeded) { ScopedV8Environment v8_environment; UpdateModulesTestUnwinder unwinder(v8_environment.isolate()); base::ModuleCache module_cache; unwinder.AddInitialModules(&module_cache); const int kDefaultCapacity = v8::Isolate::kMinCodePagesBufferSize; const int kCodePages = kDefaultCapacity * 3; std::vector code_pages; code_pages.reserve(kCodePages); // Create kCodePages with the last being the embedded code page. for (int i = 0; i < kCodePages - 1; ++i) code_pages.push_back({reinterpret_cast(i + 1), 1}); code_pages.push_back(GetEmbeddedCodeRange(v8_environment.isolate())); // The first sample should successfully create modules up to the default // capacity. unwinder.SetCodePages(code_pages); unwinder.OnStackCapture(); unwinder.UpdateModules(&module_cache); EXPECT_NE(nullptr, module_cache.GetModuleForAddress(kDefaultCapacity)); EXPECT_EQ(nullptr, module_cache.GetModuleForAddress(kDefaultCapacity + 1)); // The capacity should be expanded by the second sample to handle all the // available modules. unwinder.SetCodePages(code_pages); unwinder.OnStackCapture(); unwinder.UpdateModules(&module_cache); EXPECT_NE(nullptr, module_cache.GetModuleForAddress(kCodePages - 1)); } TEST(V8UnwinderTest, CanUnwindFrom_V8Module) { ScopedV8Environment v8_environment; UpdateModulesTestUnwinder unwinder(v8_environment.isolate()); base::ModuleCache module_cache; unwinder.AddInitialModules(&module_cache); unwinder.SetCodePages({{reinterpret_cast(1), 10}, GetEmbeddedCodeRange(v8_environment.isolate())}); unwinder.OnStackCapture(); unwinder.UpdateModules(&module_cache); const base::ModuleCache::Module* module = module_cache.GetModuleForAddress(1); ASSERT_NE(nullptr, module); EXPECT_TRUE(unwinder.CanUnwindFrom({1, module})); } TEST(V8UnwinderTest, CanUnwindFrom_OtherModule) { ScopedV8Environment v8_environment; UpdateModulesTestUnwinder unwinder(v8_environment.isolate()); base::ModuleCache module_cache; unwinder.AddInitialModules(&module_cache); unwinder.SetCodePages({GetEmbeddedCodeRange(v8_environment.isolate())}); unwinder.OnStackCapture(); unwinder.UpdateModules(&module_cache); auto other_module = std::make_unique(1, 10); const base::ModuleCache::Module* other_module_ptr = other_module.get(); module_cache.AddCustomNativeModule(std::move(other_module)); EXPECT_FALSE(unwinder.CanUnwindFrom({1, other_module_ptr})); } TEST(V8UnwinderTest, CanUnwindFrom_NullModule) { ScopedV8Environment v8_environment; UpdateModulesTestUnwinder unwinder(v8_environment.isolate()); base::ModuleCache module_cache; unwinder.AddInitialModules(&module_cache); // Insert a non-native module to potentially exercise the Module comparator. unwinder.SetCodePages({{reinterpret_cast(1), 10}, GetEmbeddedCodeRange(v8_environment.isolate())}); unwinder.OnStackCapture(); unwinder.UpdateModules(&module_cache); EXPECT_FALSE(unwinder.CanUnwindFrom({20, nullptr})); } // Checks that unwinding from C++ through JavaScript and back into C++ succeeds. // NB: unwinding is only supported for 64 bit Windows and Intel macOS. #if (defined(OS_WIN) && defined(ARCH_CPU_64_BITS)) || \ (defined(OS_MAC) && defined(ARCH_CPU_X86_64)) #define MAYBE_UnwindThroughV8Frames UnwindThroughV8Frames #else #define MAYBE_UnwindThroughV8Frames DISABLED_UnwindThroughV8Frames #endif TEST(V8UnwinderTest, MAYBE_UnwindThroughV8Frames) { v8::Isolate* isolate = nullptr; base::WaitableEvent isolate_available; const auto set_isolate = [&](v8::Isolate* isolate_state) { isolate = isolate_state; isolate_available.Signal(); }; const auto create_v8_unwinder = [&]() -> std::unique_ptr { isolate_available.Wait(); return std::make_unique(isolate); }; base::UnwindScenario scenario(base::BindRepeating( &CallThroughV8, base::BindLambdaForTesting(set_isolate))); base::ModuleCache module_cache; std::vector sample = SampleScenario( &scenario, &module_cache, base::BindLambdaForTesting(create_v8_unwinder)); // The stack should contain a full unwind. ExpectStackContains(sample, {scenario.GetWaitForSampleAddressRange(), scenario.GetSetupFunctionAddressRange(), scenario.GetOuterFunctionAddressRange()}); // The stack should contain a frame from a JavaScript module. auto loc = std::find_if(sample.begin(), sample.end(), [&](const base::Frame& frame) { return frame.module && (frame.module->GetId() == V8Unwinder::kV8EmbeddedCodeRangeBuildId || frame.module->GetId() == V8Unwinder::kV8CodeRangeBuildId); }); EXPECT_NE(sample.end(), loc); }