blob: 94ee3dc19bf022581ef7f6673c966d94f392223d [file] [log] [blame]
// Copyright 2023 The Fuchsia 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 "src/lib/unwinder/cfi_unwinder.h"
#include <cinttypes>
#include <cstdint>
#include <limits>
#include <memory>
#include <utility>
#include <vector>
#include "src/lib/unwinder/cfi_module.h"
#include "src/lib/unwinder/error.h"
namespace unwinder {
namespace {
// Validates our assumptions about what registers we should have recovered in a "transition" 32 bit
// frame that was recovered from a 64 bit frame. For now, this should only ever happen via Starnix's
// custom CFI directives that get us the first restricted mode frame, which is what should be
// contained in |regs|. In this state, we should always successfully recover all of SP, LR, and PC,
// and they should all be pointing into the 32 bit restricted mode address space.
Error Validate32BitRegisters(const Registers& regs) {
if (regs.arch() != Registers::Arch::kArm32) {
return Error("New registers aren't kArm32?");
}
uint64_t val;
if (auto err = regs.GetSP(val); err.has_err()) {
return Error("32 bit registers don't have SP (r13) set: %s\n32 Bit Registers: %s",
err.msg().c_str(), regs.Describe().c_str());
} else if (val > std::numeric_limits<uint32_t>::max()) {
return Error("32 bit SP (r13) %" PRIx64 " > uint32::MAX", val);
}
if (auto err = regs.GetReturnAddress(val); err.has_err()) {
return Error("32 bit registers don't have LR (r14) set: %s\n32 Bit Registers: %s",
err.msg().c_str(), regs.Describe().c_str());
} else if (val > std::numeric_limits<uint32_t>::max()) {
return Error("32 bit LR (r14) %" PRIx64 " > uint32::MAX", val);
}
if (auto err = regs.GetPC(val); err.has_err()) {
return Error("32 bit registers don't have PC (r15) set: %s\n32 Bit Registers: %s",
err.msg().c_str(), regs.Describe().c_str());
} else if (val > std::numeric_limits<uint32_t>::max()) {
return Error("32 bit PC (r15) %" PRIx64 " > uint32::MAX", val);
}
return Success();
}
// Returns an error if the |next| registers do not have PC and LR populated with 32 bit values.
fit::result<Error, Registers> TryConvertRegistersTo32Bit(const Registers& current,
const Registers& next,
const Module* next_module) {
if (current.arch() != Registers::Arch::kArm64) {
return fit::error(Error("Current registers are not kArm64."));
} else if (next.arch() == Registers::Arch::kArm32) {
return fit::error(Error("Next registers are already kArm32, nothing to do."));
}
uint64_t pc = 0;
uint64_t ra = 0;
// As of today the only way we should ever successfully transition to a 32 bit frame is when we
// have just recovered all of the registers specified by Starnix's CFI directives to reconstruct
// the restricted mode stack. That means that we should _always_ have both PC and LR available
// from the CFI (which should be finished processing by the time this is called). Therefore if we
// cannot fetch either of them from |next| we bail out.
if (next.GetPC(pc).has_err()) {
return fit::error(Error("Next registers do not have PC set: %s", next.Describe().c_str()));
}
if (next.GetReturnAddress(ra).has_err()) {
return fit::error(Error("Next registers do not have LR set: %s", next.Describe().c_str()));
}
if (!(pc < std::numeric_limits<uint32_t>::max()) ||
!(ra < std::numeric_limits<uint32_t>::max())) {
return fit::error(Error("PC [%" PRIx64 "] or LR [%" PRIx64
"] contains address greater than 32 bit address space.",
pc, ra));
}
return next.To32Bit();
}
} // namespace
bool CfiModuleInfo::IsValidPC(uint64_t pc) const {
return ((binary && binary->IsValidPC(pc)) || (debug_info && debug_info->IsValidPC(pc)));
}
CfiUnwinder::CfiUnwinder(const std::vector<Module>& modules) : UnwinderBase(this) {
for (const auto& module : modules) {
module_map_.emplace(module.load_address,
CfiModuleInfo{.module = module, .binary = nullptr, .debug_info = nullptr});
}
}
Error CfiUnwinder::Step(Memory* stack, const Frame& current, Frame& next) {
if (auto result = Step(stack, current.regs, next.regs, current.pc_is_return_address);
result.is_error()) {
return result.error_value();
} else {
next.is_signal_frame = result.value();
}
return Success();
}
fit::result<Error, bool> CfiUnwinder::Step(Memory* stack, const Registers& current, Registers& next,
bool is_return_address) {
uint64_t pc;
if (auto err = current.GetPC(pc); err.has_err()) {
return fit::error(err);
}
Registers regs = current;
// is_return_address indicates whether pc in the current registers is a return address from a
// previous "Step". If it is, we need to subtract 1 to find the call site because "call" could
// be the last instruction of a nonreturn function and now the PC is pointing outside of the
// valid code boundary.
//
// Subtracting 1 is sufficient here because in CfiParser::ParseInstructions, we scan CFI until
// pc > pc_limit. So it's still correct even if pc_limit is not pointing to the beginning of an
// instruction.
if (is_return_address) {
pc -= 1;
regs.SetPC(pc);
}
// We might have a PC in a 32 bit module. If we do, we'll need to convert the registers to the 32
// bit arch so all the named registers correspond to their 32 bit register numbers instead of 64
// bit. Checking that PC < UINT32_MAX is just a heuristic and doesn't actually indicate
// anything about address size for the module.
if (regs.arch() != Registers::Arch::kArm32 && pc < std::numeric_limits<uint32_t>::max()) {
Module* next_module = nullptr;
if (auto err = GetModuleForPc(pc, &next_module); err.has_err()) {
return fit::error(err);
};
if (next_module->size == Module::AddressSize::k32Bit) {
// In the error case, the message is probably only useful for developing and debugging the
// unwinder itself and will happen frequently enough that we shouldn't log it, but for
// debugging purposes can be displayed if needed. The validation step in the success case is a
// fatal error since we have strict expectations of what registers are restored by Starnix,
// which is currently the only way we should ever transition to code in a 32 bit address
// space.
if (auto maybe_32bit = TryConvertRegistersTo32Bit(current, regs, next_module);
maybe_32bit.is_ok()) {
// Both PC and LR in |next| appear to be 32 bit addresses, now validate that the converted
// 32 bit registers actually have everything that we expect to get from Starnix's CFI: PC,
// LR, and SP should all be populated and have 32 bit addresses, if this fails at this
// point, it's an error.
if (auto err = Validate32BitRegisters(*maybe_32bit); err.has_err()) {
return fit::error(err);
}
// Validations succeeded, |next| is a 32 bit frame recovered from Starnix restricted mode.
// It's entirely possible at this point for the 32 bit binary to have CFI instructions for
// this 32 bit PC value, so we continue on.
regs = *maybe_32bit;
}
}
}
CfiModuleInfo* cfi;
if (auto err = GetCfiModuleInfoForPc(pc, &cfi); err.has_err()) {
return fit::error(err);
}
auto result = cfi->binary->Step(stack, regs, next);
if (result.is_error()) {
return result;
}
return fit::ok(result.value());
}
void CfiUnwinder::AsyncStep(AsyncMemory* stack, const Frame& current,
fit::callback<void(Error, Registers)> cb) {
// TODO(https://fxbug.dev/316047562): Make CFI work on RISC-V.
if (current.regs.arch() == Registers::Arch::kRiscv64) {
return cb(Error("RISC-V is not supported with the CFI Unwinder."),
Registers(current.regs.arch()));
}
AsyncStep(stack, current.regs, current.pc_is_return_address, std::move(cb));
}
void CfiUnwinder::AsyncStep(AsyncMemory* stack, Registers current, bool is_return_address,
fit::callback<void(Error, Registers)> cb) {
uint64_t pc;
if (auto err = current.GetPC(pc); err.has_err()) {
return cb(err, Registers(current.arch()));
}
// is_return_address indicates whether pc in the current registers is a return address from a
// previous "Step". If it is, we need to subtract 1 to find the call site because "call" could
// be the last instruction of a nonreturn function and now the PC is pointing outside of the
// valid code boundary.
//
// Subtracting 1 is sufficient here because in CfiParser::ParseInstructions, we scan CFI until
// pc > pc_limit. So it's still correct even if pc_limit is not pointing to the beginning of an
// instruction.
if (is_return_address) {
pc -= 1;
current.SetPC(pc);
}
CfiModuleInfo* cfi_info;
if (auto err = GetCfiModuleInfoForPc(pc, &cfi_info); err.has_err()) {
return cb(err, Registers(current.arch()));
}
if (cfi_info->debug_info) {
// Try stepping with the debug_info if it is available. This could contain both .debug_frame and
// .eh_frame sections in the case of a fully unstripped binary, or just a .debug_frame section
// in the case of a separated debug_info binary. Both have to fail for us to try again with the
// "binary" file, which will only contain an .eh_frame section.
cfi_info->debug_info->AsyncStep(
stack, current,
[cfi_info, stack, current, cb = std::move(cb)](Error err, Registers regs) mutable {
if (err.has_err()) {
// debug_info didn't work, try again with the binary module instead. If this fails it's
// a fatal error for this unwinder.
if (cfi_info->binary) {
return cfi_info->binary->AsyncStep(
stack, current, [e = err, cb = std::move(cb)](Error err, Registers regs) mutable {
if (err.has_err()) {
// Propagate both errors up.
return cb(Error("debug_info:" + e.msg() + ";binary:" + err.msg()),
std::move(regs));
}
// Using the binary worked.
cb(err, std::move(regs));
});
} else {
return cb(Error("debug_info:" + err.msg() + ";binary not present."), regs);
}
}
// Unwinding with the debug_info module worked, issue the callback.
cb(err, std::move(regs));
});
} else if (cfi_info->binary) {
// No debug_info available, unwind with the binary module.
cfi_info->binary->AsyncStep(stack, current, std::move(cb));
} else {
return cb(Error("Module has no associated memory."), Registers(current.arch()));
}
}
bool CfiUnwinder::IsValidPC(uint64_t pc) {
CfiModuleInfo* cfi;
return GetCfiModuleInfoForPc(pc, &cfi).ok();
}
Error CfiUnwinder::GetCfiModuleInfoForPc(uint64_t pc, CfiModuleInfo** out) {
auto module_it = module_map_.upper_bound(pc);
if (module_it == module_map_.begin()) {
return Error("%#" PRIx64 " is not covered by any module", pc);
}
module_it--;
uint64_t module_address = module_it->first;
auto& module_info = module_it->second;
if (!module_info.binary && module_info.module.binary_memory) {
module_info.binary = std::make_unique<CfiModule>(module_info.module.binary_memory,
module_address, module_info.module);
// Loading the main binary file should always contain either an eh_frame section or a
// debug_frame section.
if (auto err = module_info.binary->Load(); err.has_err()) {
return err;
}
}
if (!module_info.debug_info && module_info.module.debug_info_memory) {
module_info.debug_info = std::make_unique<CfiModule>(module_info.module.debug_info_memory,
module_address, module_info.module);
// A split debug info file may contain neither eh_frame nor debug_frame sections, it is not an
// error if this fails to load.
if (auto err = module_info.debug_info->Load(); err.has_err()) {
// Reset the pointer to null to indicate that it should not be used for look ups later.
module_info.debug_info.reset();
}
}
if (!module_info.IsValidPC(pc)) {
return Error("%#" PRIx64 " is not a valid PC in module %#" PRIx64, pc, module_address);
}
*out = &module_info;
return Success();
}
Error CfiUnwinder::GetModuleForPc(uint64_t pc, Module** out) {
auto module_it = module_map_.upper_bound(pc);
if (module_it == module_map_.begin()) {
return Error("%#" PRIx64 " is not covered by any module", pc);
}
module_it--;
auto& module_info = module_it->second;
// The actual low-level module object is owned by the CfiModuleInfo instance we have found, it's
// always valid at this point. It's up to callers to determine whether the binary or debug_info
// memory they want is valid before using them. The load address, mode, and address size will
// always be safe to read even if the ELF is invalid.
*out = &module_info.module;
return Success();
}
} // namespace unwinder