// Copyright 2022 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 "base/profiler/frame_pointer_unwinder.h" #include #include "base/profiler/module_cache.h" #include "base/profiler/stack_sampling_profiler_test_util.h" #include "base/profiler/unwinder.h" #include "build/buildflag.h" #include "testing/gtest/include/gtest/gtest.h" #if BUILDFLAG(IS_APPLE) #include "base/mac/mac_util.h" #endif namespace base { namespace { constexpr uintptr_t kModuleStart = 0x1000; constexpr size_t kModuleSize = 0x1000; constexpr uintptr_t kNonNativeModuleStart = 0x4000; // Used to construct test stacks. If `relative` is true, the value should be the // address `offset` positions from the bottom of the stack (at 8-byte alignment) // Otherwise, `offset` is added to the stack as an absolute address/value. // For example, when creating a stack with bottom 0x2000, {false, 0xf00d} will // become 0xf00d, and {true, 0x3} will become 0x2018. struct StackEntrySpec { bool relative; uintptr_t offset; }; // Enables constructing a stack buffer that has pointers to itself // and provides convenience methods for calling the unwinder. struct InputStack { explicit InputStack(const std::vector& offsets) : buffer(offsets.size()) { size_t size = offsets.size(); for (size_t i = 0; i < size; ++i) { auto spec = offsets[i]; if (spec.relative) { buffer[i] = bottom() + (spec.offset * sizeof(uintptr_t)); } else { buffer[i] = spec.offset; } } } uintptr_t bottom() const { return reinterpret_cast(buffer.data()); } uintptr_t top() const { return bottom() + buffer.size() * sizeof(uintptr_t); } private: std::vector buffer; }; } // namespace class FramePointerUnwinderTest : public testing::Test { protected: FramePointerUnwinderTest() { #if BUILDFLAG(IS_APPLE) if (__builtin_available(iOS 12, *)) { #else { #endif unwinder_ = std::make_unique(); auto test_module = std::make_unique(kModuleStart, kModuleSize); module_ = test_module.get(); module_cache_.AddCustomNativeModule(std::move(test_module)); auto non_native_module = std::make_unique( kNonNativeModuleStart, kModuleSize, false); non_native_module_ = non_native_module.get(); std::vector> wrapper; wrapper.push_back(std::move(non_native_module)); module_cache()->UpdateNonNativeModules({}, std::move(wrapper)); unwinder_->Initialize(&module_cache_); } } ModuleCache* module_cache() { return &module_cache_; } ModuleCache::Module* module() { return module_; } ModuleCache::Module* non_native_module() { return non_native_module_; } Unwinder* unwinder() { return unwinder_.get(); } private: std::unique_ptr unwinder_; base::ModuleCache module_cache_; raw_ptr module_; raw_ptr non_native_module_; }; TEST_F(FramePointerUnwinderTest, FPPointsOutsideOfStack) { InputStack input({ {false, 0x1000}, {false, 0x1000}, {false, 0x1000}, {false, 0x1000}, {false, 0x1000}, }); RegisterContext context; RegisterContextStackPointer(&context) = input.bottom(); RegisterContextInstructionPointer(&context) = kModuleStart; RegisterContextFramePointer(&context) = 0x1; std::vector stack = { Frame(RegisterContextInstructionPointer(&context), module())}; EXPECT_EQ(UnwindResult::kAborted, unwinder()->TryUnwind(&context, input.top(), &stack)); EXPECT_EQ(std::vector({{kModuleStart, module()}}), stack); RegisterContextFramePointer(&context) = input.bottom() - sizeof(uintptr_t); EXPECT_EQ(UnwindResult::kAborted, unwinder()->TryUnwind(&context, input.top(), &stack)); EXPECT_EQ(std::vector({{kModuleStart, module()}}), stack); RegisterContextFramePointer(&context) = input.top(); EXPECT_EQ(UnwindResult::kAborted, unwinder()->TryUnwind(&context, input.top(), &stack)); EXPECT_EQ(std::vector({{kModuleStart, module()}}), stack); } TEST_F(FramePointerUnwinderTest, FPPointsToSelf) { InputStack input({ {true, 0}, {false, kModuleStart + 0x10}, {true, 4}, {false, kModuleStart + 0x20}, {false, 0}, {false, 0}, }); RegisterContext context; RegisterContextStackPointer(&context) = input.bottom(); RegisterContextInstructionPointer(&context) = kModuleStart; RegisterContextFramePointer(&context) = input.bottom(); std::vector stack = { Frame(RegisterContextInstructionPointer(&context), module())}; EXPECT_EQ(UnwindResult::kAborted, unwinder()->TryUnwind(&context, input.top(), &stack)); EXPECT_EQ(std::vector({ {kModuleStart, module()}, }), stack); } // Tests that two frame pointers that point to each other can't create an // infinite loop TEST_F(FramePointerUnwinderTest, FPCycle) { InputStack input({ {true, 2}, {false, kModuleStart + 0x10}, {true, 0}, {false, kModuleStart + 0x20}, {true, 4}, {false, kModuleStart + 0x30}, {false, 0}, {false, 0}, }); RegisterContext context; RegisterContextStackPointer(&context) = input.bottom(); RegisterContextInstructionPointer(&context) = kModuleStart; RegisterContextFramePointer(&context) = input.bottom(); std::vector stack = { Frame(RegisterContextInstructionPointer(&context), module())}; EXPECT_EQ(UnwindResult::kAborted, unwinder()->TryUnwind(&context, input.top(), &stack)); EXPECT_EQ(std::vector({ {kModuleStart, module()}, {kModuleStart + 0x10, module()}, }), stack); } TEST_F(FramePointerUnwinderTest, NoModuleForIP) { uintptr_t not_in_module = kModuleStart - 0x10; InputStack input({ {true, 2}, {false, not_in_module}, {true, 4}, {true, kModuleStart + 0x10}, {false, 0}, {false, 0}, }); RegisterContext context; RegisterContextStackPointer(&context) = input.bottom(); RegisterContextInstructionPointer(&context) = kModuleStart; RegisterContextFramePointer(&context) = input.bottom(); std::vector stack = { Frame(RegisterContextInstructionPointer(&context), module())}; EXPECT_EQ(UnwindResult::kAborted, unwinder()->TryUnwind(&context, input.top(), &stack)); EXPECT_EQ( std::vector({{kModuleStart, module()}, {not_in_module, nullptr}}), stack); } // Tests that testing that checking if there's space to read two values from the // stack doesn't overflow. TEST_F(FramePointerUnwinderTest, FPAdditionOverflows) { uintptr_t will_overflow = std::numeric_limits::max() - 1; InputStack input({ {true, 2}, {false, kModuleStart + 0x10}, {false, 0}, {false, 0}, }); RegisterContext context; RegisterContextStackPointer(&context) = input.bottom(); RegisterContextInstructionPointer(&context) = kModuleStart; RegisterContextFramePointer(&context) = will_overflow; std::vector stack = { Frame(RegisterContextInstructionPointer(&context), module())}; EXPECT_EQ(UnwindResult::kAborted, unwinder()->TryUnwind(&context, input.top(), &stack)); EXPECT_EQ(std::vector({ {kModuleStart, module()}, }), stack); } // Tests the happy path: a successful unwind with no non-native modules. TEST_F(FramePointerUnwinderTest, RegularUnwind) { InputStack input({ {true, 4}, // fp of frame 1 {false, kModuleStart + 0x20}, // ip of frame 1 {false, 0xaaaa}, {false, 0xaaaa}, {true, 8}, // fp of frame 2 {false, kModuleStart + 0x42}, // ip of frame 2 {false, 0xaaaa}, {false, 0xaaaa}, {false, 0}, {false, 1}, }); RegisterContext context; RegisterContextStackPointer(&context) = input.bottom(); RegisterContextInstructionPointer(&context) = kModuleStart; RegisterContextFramePointer(&context) = input.bottom(); std::vector stack = { Frame(RegisterContextInstructionPointer(&context), module())}; EXPECT_EQ(UnwindResult::kCompleted, unwinder()->TryUnwind(&context, input.top(), &stack)); EXPECT_EQ(std::vector({ {kModuleStart, module()}, {kModuleStart + 0x20, module()}, {kModuleStart + 0x42, module()}, }), stack); } // Tests that if a V8 frame is encountered, unwinding stops and // kUnrecognizedFrame is returned to facilitate continuing with the V8 unwinder. TEST_F(FramePointerUnwinderTest, NonNativeFrame) { InputStack input({ {true, 4}, // fp of frame 1 {false, kModuleStart + 0x20}, // ip of frame 1 {false, 0xaaaa}, {false, 0xaaaa}, {true, 8}, // fp of frame 2 {false, kNonNativeModuleStart + 0x42}, // ip of frame 2 {false, 0xaaaa}, {false, 0xaaaa}, {true, 12}, // fp of frame 3 {false, kModuleStart + 0x10}, // ip of frame 3 {true, 0xaaaa}, {true, 0xaaaa}, {false, 0}, {false, 1}, }); RegisterContext context; RegisterContextStackPointer(&context) = input.bottom(); RegisterContextInstructionPointer(&context) = kModuleStart; RegisterContextFramePointer(&context) = input.bottom(); std::vector stack = { Frame(RegisterContextInstructionPointer(&context), module())}; EXPECT_EQ(UnwindResult::kUnrecognizedFrame, unwinder()->TryUnwind(&context, input.top(), &stack)); EXPECT_EQ(std::vector({ {kModuleStart, module()}, {kModuleStart + 0x20, module()}, {kNonNativeModuleStart + 0x42, non_native_module()}, }), stack); } // Tests that a V8 frame with an unaligned frame pointer correctly returns // kUnrecognizedFrame and not kAborted. TEST_F(FramePointerUnwinderTest, NonNativeUnaligned) { InputStack input({ {true, 4}, // fp of frame 1 {false, kModuleStart + 0x20}, // ip of frame 1 {false, 0xaaaa}, {false, 0xaaaa}, {true, 7}, // fp of frame 2 {false, kNonNativeModuleStart + 0x42}, // ip of frame 2 {false, 0xaaaa}, {true, 10}, // fp of frame 3 {false, kModuleStart + 0x10}, // ip of frame 3 {true, 0xaaaa}, {false, 0}, {false, 1}, }); RegisterContext context; RegisterContextStackPointer(&context) = input.bottom(); RegisterContextInstructionPointer(&context) = kModuleStart; RegisterContextFramePointer(&context) = input.bottom(); std::vector stack = { Frame(RegisterContextInstructionPointer(&context), module())}; EXPECT_EQ(UnwindResult::kUnrecognizedFrame, unwinder()->TryUnwind(&context, input.top(), &stack)); } } // namespace base