blob: f9cc8a5dda2addd38b1b7782f6d289383c341d63 [file] [log] [blame]
// 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 "base/profiler/native_unwinder_mac.h"
#include <mach-o/compact_unwind_encoding.h>
#include <mach/mach.h>
#include <mach/vm_map.h>
#include <sys/ptrace.h>
#include "base/logging.h"
#include "base/profiler/module_cache.h"
#include "base/profiler/native_unwinder.h"
#include "base/profiler/profile_builder.h"
extern "C" {
void _sigtramp(int, int, struct sigset*);
namespace base {
namespace {
// Extracts the "frame offset" for a given frame from the compact unwind info.
// A frame offset indicates the location of saved non-volatile registers in
// relation to the frame pointer. See |mach-o/compact_unwind_encoding.h| for
// details.
uint32_t GetFrameOffset(int compact_unwind_info) {
// The frame offset lives in bytes 16-23. This shifts it down by the number of
// leading zeroes in the mask, then masks with (1 << number of one bits in the
// mask) - 1, turning 0x00FF0000 into 0x000000FF. Adapted from |EXTRACT_BITS|
// in libunwind's CompactUnwinder.hpp.
return (
(compact_unwind_info >> __builtin_ctz(UNWIND_X86_64_RBP_FRAME_OFFSET)) &
(((1 << __builtin_popcount(UNWIND_X86_64_RBP_FRAME_OFFSET))) - 1));
// True if the unwind from |leaf_frame_module| may trigger a crash bug in
// unw_init_local. If so, the stack walk should be aborted at the leaf frame.
bool MayTriggerUnwInitLocalCrash(const ModuleCache::Module* leaf_frame_module) {
// The issue here is a bug in unw_init_local that, in some unwinds, results in
// attempts to access memory at the address immediately following the address
// range of the library. When the library is the last of the mapped libraries
// that address is in a different memory region. Starting with 10.13.4 beta
// releases it appears that this region is sometimes either unmapped or mapped
// without read access, resulting in crashes on the attempted access. It's not
// clear what circumstances result in this situation; attempts to reproduce on
// a 10.13.4 beta did not trigger the issue.
// The workaround is to check if the memory address that would be accessed is
// readable, and if not, abort the stack walk before calling unw_init_local.
// As of 2018/03/19 about 0.1% of non-idle stacks on the UI and GPU main
// threads have a leaf frame in the last library. Since the issue appears to
// only occur some of the time it's expected that the quantity of lost samples
// will be lower than 0.1%, possibly significantly lower.
// TODO(lgrey): Add references above to LLVM/Radar bugs on unw_init_local once
// filed.
uint64_t unused;
vm_size_t size = sizeof(unused);
return vm_read_overwrite(
leaf_frame_module->GetBaseAddress() + leaf_frame_module->GetSize(),
sizeof(unused), reinterpret_cast<vm_address_t>(&unused),
&size) != 0;
// Check if the cursor contains a valid-looking frame pointer for frame pointer
// unwinds. If the stack frame has a frame pointer, stepping the cursor will
// involve indexing memory access off of that pointer. In that case,
// sanity-check the frame pointer register to ensure it's within bounds.
// Additionally, the stack frame might be in a prologue or epilogue, which can
// cause a crash when the unwinder attempts to access non-volatile registers
// that have not yet been pushed, or have already been popped from the
// stack. libwunwind will try to restore those registers using an offset from
// the frame pointer. However, since we copy the stack from RSP up, any
// locations below the stack pointer are before the beginning of the stack
// buffer. Account for this by checking that the expected location is above the
// stack pointer, and rejecting the sample if it isn't.
bool HasValidRbp(unw_cursor_t* unwind_cursor, uintptr_t stack_top) {
unw_proc_info_t proc_info;
unw_get_proc_info(unwind_cursor, &proc_info);
if ((proc_info.format & UNWIND_X86_64_MODE_MASK) ==
unw_word_t rsp, rbp;
unw_get_reg(unwind_cursor, UNW_X86_64_RSP, &rsp);
unw_get_reg(unwind_cursor, UNW_X86_64_RBP, &rbp);
uint32_t offset = GetFrameOffset(proc_info.format) * sizeof(unw_word_t);
if (rbp < offset || (rbp - offset) < rsp || rbp > stack_top)
return false;
return true;
const ModuleCache::Module* GetLibSystemKernelModule(ModuleCache* module_cache) {
const ModuleCache::Module* module =
DCHECK_EQ(FilePath("libsystem_kernel.dylib"), module->GetDebugBasename());
return module;
void GetSigtrampRange(uintptr_t* start, uintptr_t* end) {
auto address = reinterpret_cast<uintptr_t>(&_sigtramp);
DCHECK(address != 0);
*start = address;
unw_context_t context;
unw_cursor_t cursor;
unw_proc_info_t info;
// Set the context's RIP to the beginning of sigtramp,
// +1 byte to work around a bug in 10.11 ([16] = address + 1;
unw_init_local(&cursor, &context);
unw_get_proc_info(&cursor, &info);
DCHECK_EQ(info.start_ip, address);
*end = info.end_ip;
} // namespace
NativeUnwinderMac::NativeUnwinderMac(ModuleCache* module_cache)
: libsystem_kernel_module_(GetLibSystemKernelModule(module_cache)) {
GetSigtrampRange(&sigtramp_start_, &sigtramp_end_);
bool NativeUnwinderMac::CanUnwindFrom(const Frame* current_frame) const {
return current_frame->module && current_frame->module->IsNative();
UnwindResult NativeUnwinderMac::TryUnwind(x86_thread_state64_t* thread_context,
uintptr_t stack_top,
ModuleCache* module_cache,
std::vector<Frame>* stack) const {
// We expect the frame correponding to the |thread_context| register state to
// exist within |stack|.
DCHECK_GT(stack->size(), 0u);
// There isn't an official way to create a unw_context other than to create it
// from the current state of the current thread's stack. Since we're walking a
// different thread's stack we must forge a context. The unw_context is just a
// copy of the 16 main registers followed by the instruction pointer, nothing
// more. Coincidentally, the first 17 items of the x86_thread_state64_t type
// are exactly those registers in exactly the same order, so just bulk copy
// them over.
unw_context_t unwind_context;
memcpy(&unwind_context, thread_context, sizeof(uintptr_t) * 17);
// Avoid an out-of-bounds read bug in libunwind that can crash us in some
// circumstances. If we're subject to that case, just record the first frame
// and bail. See MayTriggerUnwInitLocalCrash for details.
if (stack->back().module && MayTriggerUnwInitLocalCrash(stack->back().module))
return UnwindResult::ABORTED;
unw_cursor_t unwind_cursor;
unw_init_local(&unwind_cursor, &unwind_context);
for (;;) {
Optional<UnwindResult> result =
CheckPreconditions(&stack->back(), &unwind_cursor, stack_top);
if (result.has_value())
return *result;
unw_word_t prev_rsp;
unw_get_reg(&unwind_cursor, UNW_REG_SP, &prev_rsp);
int step_result = UnwindStep(&unwind_context, &unwind_cursor,
stack->size() == 1, module_cache);
unw_word_t rip;
unw_get_reg(&unwind_cursor, UNW_REG_IP, &rip);
unw_word_t rsp;
unw_get_reg(&unwind_cursor, UNW_REG_SP, &rsp);
bool successfully_unwound;
result = CheckPostconditions(step_result, prev_rsp, rsp, stack_top,
if (successfully_unwound) {
stack->emplace_back(rip, module_cache->GetModuleForAddress(rip));
// Save the relevant register state back into the thread context.
unw_word_t rbp;
unw_get_reg(&unwind_cursor, UNW_X86_64_RBP, &rbp);
thread_context->__rip = rip;
thread_context->__rsp = rsp;
thread_context->__rbp = rbp;
if (result.has_value())
return *result;
return UnwindResult::COMPLETED;
// Checks preconditions for attempting an unwind. If any conditions fail,
// returns corresponding UnwindResult. Otherwise returns nullopt.
Optional<UnwindResult> NativeUnwinderMac::CheckPreconditions(
const Frame* current_frame,
unw_cursor_t* unwind_cursor,
uintptr_t stack_top) const {
if (!current_frame->module) {
// There's no loaded module containing the instruction pointer. This is
// due to either executing code that is not in a module (e.g. V8
// runtime-generated code), or to a previous bad unwind.
// The bad unwind scenario can occur in frameless (non-DWARF) unwinding,
// which works by fetching the function's stack size from the unwind
// encoding or stack, and adding it to the stack pointer to determine the
// function's return address.
// If we're in a function prologue or epilogue, the actual stack size may
// be smaller than it will be during the normal course of execution. When
// libunwind adds the expected stack size, it will look for the return
// address in the wrong place. This check ensures we don't continue trying
// to unwind using the resulting bad IP value.
return UnwindResult::ABORTED;
if (!current_frame->module->IsNative()) {
// This is a non-native module associated with the auxiliary unwinder
// (e.g. corresponding to a frame in V8 generated code). Report as
// UNRECOGNIZED_FRAME to allow that unwinder to unwind the frame.
return UnwindResult::UNRECOGNIZED_FRAME;
// Don't continue if we're in sigtramp. Unwinding this from another thread
// is very fragile. It's a complex DWARF unwind that needs to restore the
// entire thread context which was saved by the kernel when the interrupt
// occurred.
if (current_frame->instruction_pointer >= sigtramp_start_ &&
current_frame->instruction_pointer < sigtramp_end_) {
return UnwindResult::ABORTED;
// Don't continue if rbp appears to be invalid (due to a previous bad
// unwind).
if (!HasValidRbp(unwind_cursor, stack_top))
return UnwindResult::ABORTED;
return nullopt;
// Attempts to unwind the current frame using unw_step, and returns its return
// value.
int NativeUnwinderMac::UnwindStep(unw_context_t* unwind_context,
unw_cursor_t* unwind_cursor,
bool at_first_frame,
ModuleCache* module_cache) const {
int step_result = unw_step(unwind_cursor);
if (step_result == 0 && at_first_frame) {
// libunwind is designed to be triggered by user code on their own thread,
// if it hits a library that has no unwind info for the function that is
// being executed, it just stops. This isn't a problem in the normal case,
// but in the case where this is the first frame unwind, it's quite
// possible that the stack being walked is stopped in a function that
// bridges to the kernel and thus is missing the unwind info.
// For now, just unwind the single case where the thread is stopped in a
// function in libsystem_kernel.
uint64_t& rsp = unwind_context->data[7];
uint64_t& rip = unwind_context->data[16];
if (module_cache->GetModuleForAddress(rip) == libsystem_kernel_module_) {
rip = *reinterpret_cast<uint64_t*>(rsp);
rsp += 8;
// Reset the cursor.
unw_init_local(unwind_cursor, unwind_context);
// Mock a successful step_result.
return 1;
return step_result;
// Checks postconditions after attempting an unwind. If any conditions fail,
// returns corresponding UnwindResult. Otherwise returns nullopt. Sets
// *|successfully_unwound| if the unwind succeeded (and hence the frame should
// be recorded).
Optional<UnwindResult> NativeUnwinderMac::CheckPostconditions(
int step_result,
unw_word_t prev_rsp,
unw_word_t rsp,
uintptr_t stack_top,
bool* successfully_unwound) const {
const bool stack_pointer_was_moved_and_is_valid =
rsp > prev_rsp && rsp < stack_top;
*successfully_unwound =
step_result > 0 ||
// libunwind considers the unwind complete and returns 0 if no unwind
// info was found for the current instruction pointer. It performs this
// check both before *and* after stepping the cursor. In the former case
// no action is taken, but in the latter case an unwind was successfully
// performed prior to the check. Distinguish these cases by checking
// whether the stack pointer was moved by unw_step. If so, record the
// new frame to enable non-native unwinders to continue the unwinding.
(step_result == 0 && stack_pointer_was_moved_and_is_valid);
if (step_result < 0)
return UnwindResult::ABORTED;
// libunwind returns 0 if it can't continue because no unwind info was found
// for the current instruction pointer. This could be due to unwinding past
// the entry point, in which case the unwind would be complete. It could
// also be due to unwinding to a function that simply doesn't have unwind
// info, in which case the unwind should be aborted. Or it could be due to
// unwinding to code not in a module, in which case the unwind might be
// continuable by a non-native unwinder. We don't have a good way to
// distinguish these cases, so return UNRECOGNIZED_FRAME to at least
// signify that we couldn't unwind further.
if (step_result == 0)
return UnwindResult::UNRECOGNIZED_FRAME;
// If we succeeded but didn't advance the stack pointer, or got an invalid
// new stack pointer, abort.
if (!stack_pointer_was_moved_and_is_valid)
return UnwindResult::ABORTED;
return nullopt;
std::unique_ptr<Unwinder> CreateNativeUnwinder(ModuleCache* module_cache) {
return std::make_unique<NativeUnwinderMac>(module_cache);
} // namespace base