Merge "libunwindstack: support for Armv8.3-A Pointer Authentication"
This commit is contained in:
commit
1270608dbf
17 changed files with 288 additions and 57 deletions
|
@ -26,7 +26,9 @@
|
|||
|
||||
#include <unwindstack/DwarfError.h>
|
||||
#include <unwindstack/DwarfLocation.h>
|
||||
#include <unwindstack/Elf.h>
|
||||
#include <unwindstack/Log.h>
|
||||
#include <unwindstack/MachineArm64.h>
|
||||
|
||||
#include "DwarfCfa.h"
|
||||
#include "DwarfEncoding.h"
|
||||
|
@ -204,8 +206,12 @@ template <typename AddressType>
|
|||
bool DwarfCfa<AddressType>::LogInstruction(uint32_t indent, uint64_t cfa_offset, uint8_t op,
|
||||
uint64_t* cur_pc) {
|
||||
const auto* cfa = &DwarfCfaInfo::kTable[op];
|
||||
if (cfa->name[0] == '\0') {
|
||||
log(indent, "Illegal");
|
||||
if (cfa->name[0] == '\0' || (arch_ != ARCH_ARM64 && op == 0x2d)) {
|
||||
if (op == 0x2d) {
|
||||
log(indent, "Illegal (Only valid on aarch64)");
|
||||
} else {
|
||||
log(indent, "Illegal");
|
||||
}
|
||||
log(indent, "Raw Data: 0x%02x", op);
|
||||
return true;
|
||||
}
|
||||
|
@ -514,6 +520,24 @@ bool DwarfCfa<AddressType>::cfa_gnu_negative_offset_extended(dwarf_loc_regs_t* l
|
|||
return true;
|
||||
}
|
||||
|
||||
template <typename AddressType>
|
||||
bool DwarfCfa<AddressType>::cfa_aarch64_negate_ra_state(dwarf_loc_regs_t* loc_regs) {
|
||||
// Only supported on aarch64.
|
||||
if (arch_ != ARCH_ARM64) {
|
||||
last_error_.code = DWARF_ERROR_ILLEGAL_VALUE;
|
||||
return false;
|
||||
}
|
||||
|
||||
auto cfa_location = loc_regs->find(Arm64Reg::ARM64_PREG_RA_SIGN_STATE);
|
||||
if (cfa_location == loc_regs->end()) {
|
||||
(*loc_regs)[Arm64Reg::ARM64_PREG_RA_SIGN_STATE] = {.type = DWARF_LOCATION_PSEUDO_REGISTER,
|
||||
.values = {1}};
|
||||
} else {
|
||||
cfa_location->second.values[0] ^= 1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const DwarfCfaInfo::Info DwarfCfaInfo::kTable[64] = {
|
||||
{
|
||||
// 0x00 DW_CFA_nop
|
||||
|
@ -699,7 +723,13 @@ const DwarfCfaInfo::Info DwarfCfaInfo::kTable[64] = {
|
|||
{"", 0, 0, {}, {}}, // 0x2a illegal cfa
|
||||
{"", 0, 0, {}, {}}, // 0x2b illegal cfa
|
||||
{"", 0, 0, {}, {}}, // 0x2c illegal cfa
|
||||
{"", 0, 0, {}, {}}, // 0x2d DW_CFA_GNU_window_save (Treat as illegal)
|
||||
{
|
||||
"DW_CFA_AARCH64_negate_ra_state", // 0x2d DW_CFA_AARCH64_negate_ra_state
|
||||
3,
|
||||
0,
|
||||
{},
|
||||
{},
|
||||
},
|
||||
{
|
||||
"DW_CFA_GNU_args_size", // 0x2e DW_CFA_GNU_args_size
|
||||
2,
|
||||
|
|
|
@ -31,6 +31,9 @@
|
|||
|
||||
namespace unwindstack {
|
||||
|
||||
// Forward declarations.
|
||||
enum ArchEnum : uint8_t;
|
||||
|
||||
// DWARF Standard home: http://dwarfstd.org/
|
||||
// This code is based on DWARF 4: http://http://dwarfstd.org/doc/DWARF4.pdf
|
||||
// See section 6.4.2.1 for a description of the DW_CFA_xxx values.
|
||||
|
@ -72,7 +75,8 @@ class DwarfCfa {
|
|||
typedef typename std::make_signed<AddressType>::type SignedType;
|
||||
|
||||
public:
|
||||
DwarfCfa(DwarfMemory* memory, const DwarfFde* fde) : memory_(memory), fde_(fde) {}
|
||||
DwarfCfa(DwarfMemory* memory, const DwarfFde* fde, ArchEnum arch)
|
||||
: memory_(memory), fde_(fde), arch_(arch) {}
|
||||
virtual ~DwarfCfa() = default;
|
||||
|
||||
bool GetLocationInfo(uint64_t pc, uint64_t start_offset, uint64_t end_offset,
|
||||
|
@ -99,6 +103,7 @@ class DwarfCfa {
|
|||
DwarfErrorData last_error_;
|
||||
DwarfMemory* memory_;
|
||||
const DwarfFde* fde_;
|
||||
ArchEnum arch_;
|
||||
|
||||
AddressType cur_pc_;
|
||||
const dwarf_loc_regs_t* cie_loc_regs_ = nullptr;
|
||||
|
@ -128,6 +133,7 @@ class DwarfCfa {
|
|||
bool cfa_val_offset_sf(dwarf_loc_regs_t*);
|
||||
bool cfa_val_expression(dwarf_loc_regs_t*);
|
||||
bool cfa_gnu_negative_offset_extended(dwarf_loc_regs_t*);
|
||||
bool cfa_aarch64_negate_ra_state(dwarf_loc_regs_t*);
|
||||
|
||||
using process_func = bool (DwarfCfa::*)(dwarf_loc_regs_t*);
|
||||
constexpr static process_func kCallbackTable[64] = {
|
||||
|
@ -221,8 +227,9 @@ class DwarfCfa {
|
|||
nullptr,
|
||||
// 0x2c illegal cfa
|
||||
nullptr,
|
||||
// 0x2d DW_CFA_GNU_window_save (Treat this as illegal)
|
||||
nullptr,
|
||||
// 0x2d DW_CFA_AARCH64_negate_ra_state (aarch64 only)
|
||||
// DW_CFA_GNU_window_save on other architectures.
|
||||
&DwarfCfa::cfa_aarch64_negate_ra_state,
|
||||
// 0x2e DW_CFA_GNU_args_size
|
||||
&DwarfCfa::cfa_nop,
|
||||
// 0x2f DW_CFA_GNU_negative_offset_extended
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
#include <unwindstack/DwarfMemory.h>
|
||||
#include <unwindstack/DwarfSection.h>
|
||||
#include <unwindstack/DwarfStructs.h>
|
||||
#include <unwindstack/Elf.h>
|
||||
#include <unwindstack/Log.h>
|
||||
#include <unwindstack/Memory.h>
|
||||
#include <unwindstack/Regs.h>
|
||||
|
@ -49,7 +50,7 @@ bool DwarfSection::Step(uint64_t pc, Regs* regs, Memory* process_memory, bool* f
|
|||
|
||||
// Now get the location information for this pc.
|
||||
dwarf_loc_regs_t loc_regs;
|
||||
if (!GetCfaLocationInfo(pc, fde, &loc_regs)) {
|
||||
if (!GetCfaLocationInfo(pc, fde, &loc_regs, regs->Arch())) {
|
||||
return false;
|
||||
}
|
||||
loc_regs.cie = fde->cie;
|
||||
|
@ -464,6 +465,13 @@ bool DwarfSectionImpl<AddressType>::EvalRegister(const DwarfLocation* loc, uint3
|
|||
eval_info->return_address_undefined = true;
|
||||
}
|
||||
break;
|
||||
case DWARF_LOCATION_PSEUDO_REGISTER: {
|
||||
if (!eval_info->regs_info.regs->SetPseudoRegister(reg, loc->values[0])) {
|
||||
last_error_.code = DWARF_ERROR_ILLEGAL_VALUE;
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
@ -491,6 +499,10 @@ bool DwarfSectionImpl<AddressType>::Eval(const DwarfCie* cie, Memory* regular_me
|
|||
// Always set the dex pc to zero when evaluating.
|
||||
cur_regs->set_dex_pc(0);
|
||||
|
||||
// Reset necessary pseudo registers before evaluation.
|
||||
// This is needed for ARM64, for example.
|
||||
regs->ResetPseudoRegisters();
|
||||
|
||||
EvalInfo<AddressType> eval_info{.loc_regs = &loc_regs,
|
||||
.cie = cie,
|
||||
.regular_memory = regular_memory,
|
||||
|
@ -527,8 +539,10 @@ bool DwarfSectionImpl<AddressType>::Eval(const DwarfCie* cie, Memory* regular_me
|
|||
|
||||
AddressType* reg_ptr;
|
||||
if (reg >= cur_regs->total_regs()) {
|
||||
// Skip this unknown register.
|
||||
continue;
|
||||
if (entry.second.type != DWARF_LOCATION_PSEUDO_REGISTER) {
|
||||
// Skip this unknown register.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
reg_ptr = eval_info.regs_info.Save(reg);
|
||||
|
@ -554,8 +568,8 @@ bool DwarfSectionImpl<AddressType>::Eval(const DwarfCie* cie, Memory* regular_me
|
|||
|
||||
template <typename AddressType>
|
||||
bool DwarfSectionImpl<AddressType>::GetCfaLocationInfo(uint64_t pc, const DwarfFde* fde,
|
||||
dwarf_loc_regs_t* loc_regs) {
|
||||
DwarfCfa<AddressType> cfa(&memory_, fde);
|
||||
dwarf_loc_regs_t* loc_regs, ArchEnum arch) {
|
||||
DwarfCfa<AddressType> cfa(&memory_, fde, arch);
|
||||
|
||||
// Look for the cached copy of the cie data.
|
||||
auto reg_entry = cie_loc_regs_.find(fde->cie_offset);
|
||||
|
@ -576,8 +590,9 @@ bool DwarfSectionImpl<AddressType>::GetCfaLocationInfo(uint64_t pc, const DwarfF
|
|||
}
|
||||
|
||||
template <typename AddressType>
|
||||
bool DwarfSectionImpl<AddressType>::Log(uint8_t indent, uint64_t pc, const DwarfFde* fde) {
|
||||
DwarfCfa<AddressType> cfa(&memory_, fde);
|
||||
bool DwarfSectionImpl<AddressType>::Log(uint8_t indent, uint64_t pc, const DwarfFde* fde,
|
||||
ArchEnum arch) {
|
||||
DwarfCfa<AddressType> cfa(&memory_, fde, arch);
|
||||
|
||||
// Always print the cie information.
|
||||
const DwarfCie* cie = fde->cie;
|
||||
|
|
|
@ -30,7 +30,10 @@
|
|||
namespace unwindstack {
|
||||
|
||||
RegsArm64::RegsArm64()
|
||||
: RegsImpl<uint64_t>(ARM64_REG_LAST, Location(LOCATION_REGISTER, ARM64_REG_LR)) {}
|
||||
: RegsImpl<uint64_t>(ARM64_REG_LAST, Location(LOCATION_REGISTER, ARM64_REG_LR)) {
|
||||
ResetPseudoRegisters();
|
||||
pac_mask_ = 0;
|
||||
}
|
||||
|
||||
ArchEnum RegsArm64::Arch() {
|
||||
return ARCH_ARM64;
|
||||
|
@ -45,6 +48,23 @@ uint64_t RegsArm64::sp() {
|
|||
}
|
||||
|
||||
void RegsArm64::set_pc(uint64_t pc) {
|
||||
// If the target is aarch64 then the return address may have been
|
||||
// signed using the Armv8.3-A Pointer Authentication extension. The
|
||||
// original return address can be restored by stripping out the
|
||||
// authentication code using a mask or xpaclri. xpaclri is a NOP on
|
||||
// pre-Armv8.3-A architectures.
|
||||
if ((0 != pc) && IsRASigned()) {
|
||||
if (pac_mask_) {
|
||||
pc &= ~pac_mask_;
|
||||
#if defined(__aarch64__)
|
||||
} else {
|
||||
register uint64_t x30 __asm("x30") = pc;
|
||||
// This is XPACLRI.
|
||||
asm("hint 0x7" : "+r"(x30));
|
||||
pc = x30;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
regs_[ARM64_REG_PC] = pc;
|
||||
}
|
||||
|
||||
|
@ -144,6 +164,37 @@ bool RegsArm64::StepIfSignalHandler(uint64_t elf_offset, Elf* elf, Memory* proce
|
|||
return true;
|
||||
}
|
||||
|
||||
void RegsArm64::ResetPseudoRegisters(void) {
|
||||
// DWARF for AArch64 says RA_SIGN_STATE should be initialized to 0.
|
||||
this->SetPseudoRegister(Arm64Reg::ARM64_PREG_RA_SIGN_STATE, 0);
|
||||
}
|
||||
|
||||
bool RegsArm64::SetPseudoRegister(uint16_t id, uint64_t value) {
|
||||
if ((id >= Arm64Reg::ARM64_PREG_FIRST) && (id < Arm64Reg::ARM64_PREG_LAST)) {
|
||||
pseudo_regs_[id - Arm64Reg::ARM64_PREG_FIRST] = value;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool RegsArm64::GetPseudoRegister(uint16_t id, uint64_t* value) {
|
||||
if ((id >= Arm64Reg::ARM64_PREG_FIRST) && (id < Arm64Reg::ARM64_PREG_LAST)) {
|
||||
*value = pseudo_regs_[id - Arm64Reg::ARM64_PREG_FIRST];
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool RegsArm64::IsRASigned() {
|
||||
uint64_t value;
|
||||
auto result = this->GetPseudoRegister(Arm64Reg::ARM64_PREG_RA_SIGN_STATE, &value);
|
||||
return (result && (value != 0));
|
||||
}
|
||||
|
||||
void RegsArm64::SetPACMask(uint64_t mask) {
|
||||
pac_mask_ = mask;
|
||||
}
|
||||
|
||||
Regs* RegsArm64::Clone() {
|
||||
return new RegsArm64(*this);
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ enum DwarfLocationEnum : uint8_t {
|
|||
DWARF_LOCATION_REGISTER,
|
||||
DWARF_LOCATION_EXPRESSION,
|
||||
DWARF_LOCATION_VAL_EXPRESSION,
|
||||
DWARF_LOCATION_PSEUDO_REGISTER,
|
||||
};
|
||||
|
||||
struct DwarfLocation {
|
||||
|
|
|
@ -31,6 +31,7 @@
|
|||
namespace unwindstack {
|
||||
|
||||
// Forward declarations.
|
||||
enum ArchEnum : uint8_t;
|
||||
class Memory;
|
||||
class Regs;
|
||||
template <typename AddressType>
|
||||
|
@ -90,13 +91,14 @@ class DwarfSection {
|
|||
|
||||
virtual bool Eval(const DwarfCie*, Memory*, const dwarf_loc_regs_t&, Regs*, bool*) = 0;
|
||||
|
||||
virtual bool Log(uint8_t indent, uint64_t pc, const DwarfFde* fde) = 0;
|
||||
virtual bool Log(uint8_t indent, uint64_t pc, const DwarfFde* fde, ArchEnum arch) = 0;
|
||||
|
||||
virtual void GetFdes(std::vector<const DwarfFde*>* fdes) = 0;
|
||||
|
||||
virtual const DwarfFde* GetFdeFromPc(uint64_t pc) = 0;
|
||||
|
||||
virtual bool GetCfaLocationInfo(uint64_t pc, const DwarfFde* fde, dwarf_loc_regs_t* loc_regs) = 0;
|
||||
virtual bool GetCfaLocationInfo(uint64_t pc, const DwarfFde* fde, dwarf_loc_regs_t* loc_regs,
|
||||
ArchEnum arch) = 0;
|
||||
|
||||
virtual uint64_t GetCieOffsetFromFde32(uint32_t pointer) = 0;
|
||||
|
||||
|
@ -140,9 +142,10 @@ class DwarfSectionImpl : public DwarfSection {
|
|||
bool Eval(const DwarfCie* cie, Memory* regular_memory, const dwarf_loc_regs_t& loc_regs,
|
||||
Regs* regs, bool* finished) override;
|
||||
|
||||
bool GetCfaLocationInfo(uint64_t pc, const DwarfFde* fde, dwarf_loc_regs_t* loc_regs) override;
|
||||
bool GetCfaLocationInfo(uint64_t pc, const DwarfFde* fde, dwarf_loc_regs_t* loc_regs,
|
||||
ArchEnum arch) override;
|
||||
|
||||
bool Log(uint8_t indent, uint64_t pc, const DwarfFde* fde) override;
|
||||
bool Log(uint8_t indent, uint64_t pc, const DwarfFde* fde, ArchEnum arch) override;
|
||||
|
||||
protected:
|
||||
bool GetNextCieOrFde(const DwarfFde** fde_entry);
|
||||
|
|
|
@ -60,6 +60,13 @@ enum Arm64Reg : uint16_t {
|
|||
|
||||
ARM64_REG_SP = ARM64_REG_R31,
|
||||
ARM64_REG_LR = ARM64_REG_R30,
|
||||
|
||||
// Pseudo registers. These are not machine registers.
|
||||
|
||||
// AARCH64 Return address signed state pseudo-register
|
||||
ARM64_PREG_RA_SIGN_STATE = 34,
|
||||
ARM64_PREG_FIRST = ARM64_PREG_RA_SIGN_STATE,
|
||||
ARM64_PREG_LAST,
|
||||
};
|
||||
|
||||
} // namespace unwindstack
|
||||
|
|
|
@ -64,6 +64,10 @@ class Regs {
|
|||
uint64_t dex_pc() { return dex_pc_; }
|
||||
void set_dex_pc(uint64_t dex_pc) { dex_pc_ = dex_pc; }
|
||||
|
||||
virtual void ResetPseudoRegisters() {}
|
||||
virtual bool SetPseudoRegister(uint16_t, uint64_t) { return false; }
|
||||
virtual bool GetPseudoRegister(uint16_t, uint64_t*) { return false; }
|
||||
|
||||
virtual bool StepIfSignalHandler(uint64_t elf_offset, Elf* elf, Memory* process_memory) = 0;
|
||||
|
||||
virtual bool SetPcFromReturnAddress(Memory* process_memory) = 0;
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
#include <functional>
|
||||
|
||||
#include <unwindstack/Elf.h>
|
||||
#include <unwindstack/MachineArm64.h>
|
||||
#include <unwindstack/Regs.h>
|
||||
|
||||
namespace unwindstack {
|
||||
|
@ -48,11 +49,25 @@ class RegsArm64 : public RegsImpl<uint64_t> {
|
|||
void set_pc(uint64_t pc) override;
|
||||
void set_sp(uint64_t sp) override;
|
||||
|
||||
void ResetPseudoRegisters() override;
|
||||
|
||||
bool SetPseudoRegister(uint16_t id, uint64_t value) override;
|
||||
|
||||
bool GetPseudoRegister(uint16_t id, uint64_t* value) override;
|
||||
|
||||
bool IsRASigned();
|
||||
|
||||
void SetPACMask(uint64_t mask);
|
||||
|
||||
Regs* Clone() override final;
|
||||
|
||||
static Regs* Read(void* data);
|
||||
|
||||
static Regs* CreateFromUcontext(void* ucontext);
|
||||
|
||||
protected:
|
||||
uint64_t pseudo_regs_[Arm64Reg::ARM64_PREG_LAST - Arm64Reg::ARM64_PREG_FIRST];
|
||||
uint64_t pac_mask_;
|
||||
};
|
||||
|
||||
} // namespace unwindstack
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
#include <unwindstack/DwarfLocation.h>
|
||||
#include <unwindstack/DwarfMemory.h>
|
||||
#include <unwindstack/DwarfStructs.h>
|
||||
#include <unwindstack/Elf.h>
|
||||
#include <unwindstack/Log.h>
|
||||
|
||||
#include "DwarfCfa.h"
|
||||
|
@ -57,7 +58,7 @@ class DwarfCfaLogTest : public ::testing::Test {
|
|||
fde_.pc_end = 0x2000;
|
||||
fde_.pc_end = 0x10000;
|
||||
fde_.cie = &cie_;
|
||||
cfa_.reset(new DwarfCfa<TypeParam>(dmem_.get(), &fde_));
|
||||
cfa_.reset(new DwarfCfa<TypeParam>(dmem_.get(), &fde_, ARCH_UNKNOWN));
|
||||
}
|
||||
|
||||
MemoryFake memory_;
|
||||
|
@ -72,8 +73,8 @@ TYPED_TEST_SUITE_P(DwarfCfaLogTest);
|
|||
|
||||
TYPED_TEST_P(DwarfCfaLogTest, cfa_illegal) {
|
||||
for (uint8_t i = 0x17; i < 0x3f; i++) {
|
||||
if (i == 0x2e || i == 0x2f) {
|
||||
// Skip gnu extension ops.
|
||||
if (i == 0x2d || i == 0x2e || i == 0x2f) {
|
||||
// Skip gnu extension ops and aarch64 specialized op.
|
||||
continue;
|
||||
}
|
||||
this->memory_.SetMemory(0x2000, std::vector<uint8_t>{i});
|
||||
|
@ -763,6 +764,26 @@ TYPED_TEST_P(DwarfCfaLogTest, cfa_register_override) {
|
|||
ASSERT_EQ("", GetFakeLogBuf());
|
||||
}
|
||||
|
||||
TYPED_TEST_P(DwarfCfaLogTest, cfa_aarch64_negate_ra_state) {
|
||||
// Verify that if the cfa op is handled properly depending on aarch.
|
||||
this->memory_.SetMemory(0x2000, std::vector<uint8_t>{0x2d});
|
||||
|
||||
ASSERT_TRUE(this->cfa_->Log(0, this->fde_.pc_start, 0x2000, 0x2001));
|
||||
std::string expected = "4 unwind Illegal (Only valid on aarch64)\n";
|
||||
expected += "4 unwind Raw Data: 0x2d\n";
|
||||
ASSERT_EQ(expected, GetFakeLogPrint());
|
||||
ASSERT_EQ("", GetFakeLogBuf());
|
||||
|
||||
ResetLogs();
|
||||
this->cfa_.reset(new DwarfCfa<TypeParam>(this->dmem_.get(), &this->fde_, ARCH_ARM64));
|
||||
|
||||
ASSERT_TRUE(this->cfa_->Log(0, this->fde_.pc_start, 0x2000, 0x2001));
|
||||
expected = "4 unwind DW_CFA_AARCH64_negate_ra_state\n";
|
||||
expected += "4 unwind Raw Data: 0x2d\n";
|
||||
ASSERT_EQ(expected, GetFakeLogPrint());
|
||||
ASSERT_EQ("", GetFakeLogBuf());
|
||||
}
|
||||
|
||||
REGISTER_TYPED_TEST_SUITE_P(DwarfCfaLogTest, cfa_illegal, cfa_nop, cfa_offset, cfa_offset_extended,
|
||||
cfa_offset_extended_sf, cfa_restore, cfa_restore_extended, cfa_set_loc,
|
||||
cfa_advance_loc, cfa_advance_loc1, cfa_advance_loc2, cfa_advance_loc4,
|
||||
|
@ -771,7 +792,8 @@ REGISTER_TYPED_TEST_SUITE_P(DwarfCfaLogTest, cfa_illegal, cfa_nop, cfa_offset, c
|
|||
cfa_def_cfa_register, cfa_def_cfa_offset, cfa_def_cfa_offset_sf,
|
||||
cfa_def_cfa_expression, cfa_expression, cfa_val_offset,
|
||||
cfa_val_offset_sf, cfa_val_expression, cfa_gnu_args_size,
|
||||
cfa_gnu_negative_offset_extended, cfa_register_override);
|
||||
cfa_gnu_negative_offset_extended, cfa_register_override,
|
||||
cfa_aarch64_negate_ra_state);
|
||||
|
||||
typedef ::testing::Types<uint32_t, uint64_t> DwarfCfaLogTestTypes;
|
||||
INSTANTIATE_TYPED_TEST_SUITE_P(Libunwindstack, DwarfCfaLogTest, DwarfCfaLogTestTypes);
|
||||
|
|
|
@ -25,7 +25,9 @@
|
|||
#include <unwindstack/DwarfLocation.h>
|
||||
#include <unwindstack/DwarfMemory.h>
|
||||
#include <unwindstack/DwarfStructs.h>
|
||||
#include <unwindstack/Elf.h>
|
||||
#include <unwindstack/Log.h>
|
||||
#include <unwindstack/MachineArm64.h>
|
||||
|
||||
#include "DwarfCfa.h"
|
||||
|
||||
|
@ -55,7 +57,7 @@ class DwarfCfaTest : public ::testing::Test {
|
|||
fde_.pc_start = 0x2000;
|
||||
fde_.cie = &cie_;
|
||||
|
||||
cfa_.reset(new DwarfCfa<TypeParam>(dmem_.get(), &fde_));
|
||||
cfa_.reset(new DwarfCfa<TypeParam>(dmem_.get(), &fde_, ARCH_UNKNOWN));
|
||||
}
|
||||
|
||||
MemoryFake memory_;
|
||||
|
@ -70,8 +72,8 @@ TYPED_TEST_SUITE_P(DwarfCfaTest);
|
|||
|
||||
TYPED_TEST_P(DwarfCfaTest, cfa_illegal) {
|
||||
for (uint8_t i = 0x17; i < 0x3f; i++) {
|
||||
if (i == 0x2e || i == 0x2f) {
|
||||
// Skip gnu extension ops.
|
||||
if (i == 0x2d || i == 0x2e || i == 0x2f) {
|
||||
// Skip gnu extension ops and aarch64 specialized op.
|
||||
continue;
|
||||
}
|
||||
this->memory_.SetMemory(0x2000, std::vector<uint8_t>{i});
|
||||
|
@ -952,6 +954,57 @@ TYPED_TEST_P(DwarfCfaTest, cfa_register_override) {
|
|||
ASSERT_EQ("", GetFakeLogBuf());
|
||||
}
|
||||
|
||||
TYPED_TEST_P(DwarfCfaTest, cfa_aarch64_negate_ra_state) {
|
||||
this->memory_.SetMemory(0x2000, std::vector<uint8_t>{0x2d});
|
||||
dwarf_loc_regs_t loc_regs;
|
||||
|
||||
ASSERT_FALSE(this->cfa_->GetLocationInfo(this->fde_.pc_start, 0x2000, 0x2001, &loc_regs));
|
||||
ASSERT_EQ(DWARF_ERROR_ILLEGAL_VALUE, this->cfa_->LastErrorCode());
|
||||
ASSERT_EQ(0x2001U, this->dmem_->cur_offset());
|
||||
|
||||
ASSERT_EQ("", GetFakeLogPrint());
|
||||
ASSERT_EQ("", GetFakeLogBuf());
|
||||
|
||||
ResetLogs();
|
||||
this->cfa_.reset(new DwarfCfa<TypeParam>(this->dmem_.get(), &this->fde_, ARCH_ARM64));
|
||||
ASSERT_TRUE(this->cfa_->GetLocationInfo(this->fde_.pc_start, 0x2000, 0x2001, &loc_regs));
|
||||
ASSERT_EQ(0x2001U, this->dmem_->cur_offset());
|
||||
|
||||
auto location = loc_regs.find(Arm64Reg::ARM64_PREG_RA_SIGN_STATE);
|
||||
ASSERT_NE(loc_regs.end(), location);
|
||||
ASSERT_EQ(DWARF_LOCATION_PSEUDO_REGISTER, location->second.type);
|
||||
ASSERT_EQ(1U, location->second.values[0]);
|
||||
|
||||
ASSERT_EQ("", GetFakeLogPrint());
|
||||
ASSERT_EQ("", GetFakeLogBuf());
|
||||
|
||||
// Verify that the value is set to 0 after another evaluation.
|
||||
ResetLogs();
|
||||
ASSERT_TRUE(this->cfa_->GetLocationInfo(this->fde_.pc_start, 0x2000, 0x2001, &loc_regs));
|
||||
ASSERT_EQ(0x2001U, this->dmem_->cur_offset());
|
||||
|
||||
location = loc_regs.find(Arm64Reg::ARM64_PREG_RA_SIGN_STATE);
|
||||
ASSERT_NE(loc_regs.end(), location);
|
||||
ASSERT_EQ(DWARF_LOCATION_PSEUDO_REGISTER, location->second.type);
|
||||
ASSERT_EQ(0U, location->second.values[0]);
|
||||
|
||||
ASSERT_EQ("", GetFakeLogPrint());
|
||||
ASSERT_EQ("", GetFakeLogBuf());
|
||||
|
||||
// Verify that the value is set to 1 again after a third op.
|
||||
ResetLogs();
|
||||
ASSERT_TRUE(this->cfa_->GetLocationInfo(this->fde_.pc_start, 0x2000, 0x2001, &loc_regs));
|
||||
ASSERT_EQ(0x2001U, this->dmem_->cur_offset());
|
||||
|
||||
location = loc_regs.find(Arm64Reg::ARM64_PREG_RA_SIGN_STATE);
|
||||
ASSERT_NE(loc_regs.end(), location);
|
||||
ASSERT_EQ(DWARF_LOCATION_PSEUDO_REGISTER, location->second.type);
|
||||
ASSERT_EQ(1U, location->second.values[0]);
|
||||
|
||||
ASSERT_EQ("", GetFakeLogPrint());
|
||||
ASSERT_EQ("", GetFakeLogBuf());
|
||||
}
|
||||
|
||||
REGISTER_TYPED_TEST_SUITE_P(DwarfCfaTest, cfa_illegal, cfa_nop, cfa_offset, cfa_offset_extended,
|
||||
cfa_offset_extended_sf, cfa_restore, cfa_restore_extended, cfa_set_loc,
|
||||
cfa_advance_loc1, cfa_advance_loc2, cfa_advance_loc4, cfa_undefined,
|
||||
|
@ -960,7 +1013,7 @@ REGISTER_TYPED_TEST_SUITE_P(DwarfCfaTest, cfa_illegal, cfa_nop, cfa_offset, cfa_
|
|||
cfa_def_cfa_offset_sf, cfa_def_cfa_expression, cfa_expression,
|
||||
cfa_val_offset, cfa_val_offset_sf, cfa_val_expression,
|
||||
cfa_gnu_args_size, cfa_gnu_negative_offset_extended,
|
||||
cfa_register_override);
|
||||
cfa_register_override, cfa_aarch64_negate_ra_state);
|
||||
|
||||
typedef ::testing::Types<uint32_t, uint64_t> DwarfCfaTestTypes;
|
||||
INSTANTIATE_TYPED_TEST_SUITE_P(Libunwindstack, DwarfCfaTest, DwarfCfaTestTypes);
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
|
||||
#include <unwindstack/DwarfError.h>
|
||||
#include <unwindstack/DwarfSection.h>
|
||||
#include <unwindstack/Elf.h>
|
||||
|
||||
#include "DwarfEncoding.h"
|
||||
|
||||
|
@ -505,7 +506,7 @@ TYPED_TEST_P(DwarfSectionImplTest, GetCfaLocationInfo_cie_not_cached) {
|
|||
this->memory_.SetMemory(0x6000, std::vector<uint8_t>{0x09, 0x04, 0x03});
|
||||
|
||||
dwarf_loc_regs_t loc_regs;
|
||||
ASSERT_TRUE(this->section_->GetCfaLocationInfo(0x100, &fde, &loc_regs));
|
||||
ASSERT_TRUE(this->section_->GetCfaLocationInfo(0x100, &fde, &loc_regs, ARCH_UNKNOWN));
|
||||
ASSERT_EQ(2U, loc_regs.size());
|
||||
|
||||
auto entry = loc_regs.find(2);
|
||||
|
@ -535,7 +536,7 @@ TYPED_TEST_P(DwarfSectionImplTest, GetCfaLocationInfo_cie_cached) {
|
|||
this->memory_.SetMemory(0x6000, std::vector<uint8_t>{0x09, 0x04, 0x03});
|
||||
|
||||
dwarf_loc_regs_t loc_regs;
|
||||
ASSERT_TRUE(this->section_->GetCfaLocationInfo(0x100, &fde, &loc_regs));
|
||||
ASSERT_TRUE(this->section_->GetCfaLocationInfo(0x100, &fde, &loc_regs, ARCH_UNKNOWN));
|
||||
ASSERT_EQ(2U, loc_regs.size());
|
||||
|
||||
auto entry = loc_regs.find(6);
|
||||
|
@ -560,7 +561,7 @@ TYPED_TEST_P(DwarfSectionImplTest, Log) {
|
|||
|
||||
this->memory_.SetMemory(0x5000, std::vector<uint8_t>{0x00});
|
||||
this->memory_.SetMemory(0x6000, std::vector<uint8_t>{0xc2});
|
||||
ASSERT_TRUE(this->section_->Log(2, 0x1000, &fde));
|
||||
ASSERT_TRUE(this->section_->Log(2, 0x1000, &fde, ARCH_UNKNOWN));
|
||||
|
||||
ASSERT_EQ(
|
||||
"4 unwind DW_CFA_nop\n"
|
||||
|
|
|
@ -20,8 +20,10 @@
|
|||
#include <gtest/gtest.h>
|
||||
|
||||
#include <unwindstack/DwarfSection.h>
|
||||
#include <unwindstack/Elf.h>
|
||||
|
||||
#include "MemoryFake.h"
|
||||
#include "RegsFake.h"
|
||||
|
||||
namespace unwindstack {
|
||||
|
||||
|
@ -35,13 +37,14 @@ class MockDwarfSection : public DwarfSection {
|
|||
MOCK_METHOD(bool, Eval, (const DwarfCie*, Memory*, const dwarf_loc_regs_t&, Regs*, bool*),
|
||||
(override));
|
||||
|
||||
MOCK_METHOD(bool, Log, (uint8_t, uint64_t, const DwarfFde*), (override));
|
||||
MOCK_METHOD(bool, Log, (uint8_t, uint64_t, const DwarfFde*, ArchEnum arch), (override));
|
||||
|
||||
MOCK_METHOD(void, GetFdes, (std::vector<const DwarfFde*>*), (override));
|
||||
|
||||
MOCK_METHOD(const DwarfFde*, GetFdeFromPc, (uint64_t), (override));
|
||||
|
||||
MOCK_METHOD(bool, GetCfaLocationInfo, (uint64_t, const DwarfFde*, dwarf_loc_regs_t*), (override));
|
||||
MOCK_METHOD(bool, GetCfaLocationInfo,
|
||||
(uint64_t, const DwarfFde*, dwarf_loc_regs_t*, ArchEnum arch), (override));
|
||||
|
||||
MOCK_METHOD(uint64_t, GetCieOffsetFromFde32, (uint32_t), (override));
|
||||
|
||||
|
@ -56,8 +59,11 @@ class DwarfSectionTest : public ::testing::Test {
|
|||
|
||||
MemoryFake memory_;
|
||||
std::unique_ptr<MockDwarfSection> section_;
|
||||
static RegsFake regs_;
|
||||
};
|
||||
|
||||
RegsFake DwarfSectionTest::regs_(10);
|
||||
|
||||
TEST_F(DwarfSectionTest, Step_fail_fde) {
|
||||
EXPECT_CALL(*section_, GetFdeFromPc(0x1000)).WillOnce(::testing::Return(nullptr));
|
||||
|
||||
|
@ -73,7 +79,7 @@ TEST_F(DwarfSectionTest, Step_fail_cie_null) {
|
|||
EXPECT_CALL(*section_, GetFdeFromPc(0x1000)).WillOnce(::testing::Return(&fde));
|
||||
|
||||
bool finished;
|
||||
ASSERT_FALSE(section_->Step(0x1000, nullptr, nullptr, &finished));
|
||||
ASSERT_FALSE(section_->Step(0x1000, ®s_, nullptr, &finished));
|
||||
}
|
||||
|
||||
TEST_F(DwarfSectionTest, Step_fail_cfa_location) {
|
||||
|
@ -83,11 +89,11 @@ TEST_F(DwarfSectionTest, Step_fail_cfa_location) {
|
|||
fde.cie = &cie;
|
||||
|
||||
EXPECT_CALL(*section_, GetFdeFromPc(0x1000)).WillOnce(::testing::Return(&fde));
|
||||
EXPECT_CALL(*section_, GetCfaLocationInfo(0x1000, &fde, ::testing::_))
|
||||
EXPECT_CALL(*section_, GetCfaLocationInfo(0x1000, &fde, ::testing::_, ::testing::_))
|
||||
.WillOnce(::testing::Return(false));
|
||||
|
||||
bool finished;
|
||||
ASSERT_FALSE(section_->Step(0x1000, nullptr, nullptr, &finished));
|
||||
ASSERT_FALSE(section_->Step(0x1000, ®s_, nullptr, &finished));
|
||||
}
|
||||
|
||||
TEST_F(DwarfSectionTest, Step_pass) {
|
||||
|
@ -97,19 +103,19 @@ TEST_F(DwarfSectionTest, Step_pass) {
|
|||
fde.cie = &cie;
|
||||
|
||||
EXPECT_CALL(*section_, GetFdeFromPc(0x1000)).WillOnce(::testing::Return(&fde));
|
||||
EXPECT_CALL(*section_, GetCfaLocationInfo(0x1000, &fde, ::testing::_))
|
||||
EXPECT_CALL(*section_, GetCfaLocationInfo(0x1000, &fde, ::testing::_, ::testing::_))
|
||||
.WillOnce(::testing::Return(true));
|
||||
|
||||
MemoryFake process;
|
||||
EXPECT_CALL(*section_, Eval(&cie, &process, ::testing::_, nullptr, ::testing::_))
|
||||
EXPECT_CALL(*section_, Eval(&cie, &process, ::testing::_, ®s_, ::testing::_))
|
||||
.WillOnce(::testing::Return(true));
|
||||
|
||||
bool finished;
|
||||
ASSERT_TRUE(section_->Step(0x1000, nullptr, &process, &finished));
|
||||
ASSERT_TRUE(section_->Step(0x1000, ®s_, &process, &finished));
|
||||
}
|
||||
|
||||
static bool MockGetCfaLocationInfo(::testing::Unused, const DwarfFde* fde,
|
||||
dwarf_loc_regs_t* loc_regs) {
|
||||
dwarf_loc_regs_t* loc_regs, ArchEnum) {
|
||||
loc_regs->pc_start = fde->pc_start;
|
||||
loc_regs->pc_end = fde->pc_end;
|
||||
return true;
|
||||
|
@ -123,17 +129,17 @@ TEST_F(DwarfSectionTest, Step_cache) {
|
|||
fde.cie = &cie;
|
||||
|
||||
EXPECT_CALL(*section_, GetFdeFromPc(0x1000)).WillOnce(::testing::Return(&fde));
|
||||
EXPECT_CALL(*section_, GetCfaLocationInfo(0x1000, &fde, ::testing::_))
|
||||
EXPECT_CALL(*section_, GetCfaLocationInfo(0x1000, &fde, ::testing::_, ::testing::_))
|
||||
.WillOnce(::testing::Invoke(MockGetCfaLocationInfo));
|
||||
|
||||
MemoryFake process;
|
||||
EXPECT_CALL(*section_, Eval(&cie, &process, ::testing::_, nullptr, ::testing::_))
|
||||
EXPECT_CALL(*section_, Eval(&cie, &process, ::testing::_, ®s_, ::testing::_))
|
||||
.WillRepeatedly(::testing::Return(true));
|
||||
|
||||
bool finished;
|
||||
ASSERT_TRUE(section_->Step(0x1000, nullptr, &process, &finished));
|
||||
ASSERT_TRUE(section_->Step(0x1000, nullptr, &process, &finished));
|
||||
ASSERT_TRUE(section_->Step(0x1500, nullptr, &process, &finished));
|
||||
ASSERT_TRUE(section_->Step(0x1000, ®s_, &process, &finished));
|
||||
ASSERT_TRUE(section_->Step(0x1000, ®s_, &process, &finished));
|
||||
ASSERT_TRUE(section_->Step(0x1500, ®s_, &process, &finished));
|
||||
}
|
||||
|
||||
TEST_F(DwarfSectionTest, Step_cache_not_in_pc) {
|
||||
|
@ -143,26 +149,26 @@ TEST_F(DwarfSectionTest, Step_cache_not_in_pc) {
|
|||
fde0.pc_end = 0x2000;
|
||||
fde0.cie = &cie;
|
||||
EXPECT_CALL(*section_, GetFdeFromPc(0x1000)).WillOnce(::testing::Return(&fde0));
|
||||
EXPECT_CALL(*section_, GetCfaLocationInfo(0x1000, &fde0, ::testing::_))
|
||||
EXPECT_CALL(*section_, GetCfaLocationInfo(0x1000, &fde0, ::testing::_, ::testing::_))
|
||||
.WillOnce(::testing::Invoke(MockGetCfaLocationInfo));
|
||||
|
||||
MemoryFake process;
|
||||
EXPECT_CALL(*section_, Eval(&cie, &process, ::testing::_, nullptr, ::testing::_))
|
||||
EXPECT_CALL(*section_, Eval(&cie, &process, ::testing::_, ®s_, ::testing::_))
|
||||
.WillRepeatedly(::testing::Return(true));
|
||||
|
||||
bool finished;
|
||||
ASSERT_TRUE(section_->Step(0x1000, nullptr, &process, &finished));
|
||||
ASSERT_TRUE(section_->Step(0x1000, ®s_, &process, &finished));
|
||||
|
||||
DwarfFde fde1{};
|
||||
fde1.pc_start = 0x500;
|
||||
fde1.pc_end = 0x800;
|
||||
fde1.cie = &cie;
|
||||
EXPECT_CALL(*section_, GetFdeFromPc(0x600)).WillOnce(::testing::Return(&fde1));
|
||||
EXPECT_CALL(*section_, GetCfaLocationInfo(0x600, &fde1, ::testing::_))
|
||||
EXPECT_CALL(*section_, GetCfaLocationInfo(0x600, &fde1, ::testing::_, ::testing::_))
|
||||
.WillOnce(::testing::Invoke(MockGetCfaLocationInfo));
|
||||
|
||||
ASSERT_TRUE(section_->Step(0x600, nullptr, &process, &finished));
|
||||
ASSERT_TRUE(section_->Step(0x700, nullptr, &process, &finished));
|
||||
ASSERT_TRUE(section_->Step(0x600, ®s_, &process, &finished));
|
||||
ASSERT_TRUE(section_->Step(0x700, ®s_, &process, &finished));
|
||||
}
|
||||
|
||||
} // namespace unwindstack
|
||||
|
|
|
@ -247,6 +247,14 @@ TEST_F(RegsTest, mips64_verify_sp_pc) {
|
|||
EXPECT_EQ(0xc200000000U, mips64.pc());
|
||||
}
|
||||
|
||||
TEST_F(RegsTest, arm64_strip_pac_mask) {
|
||||
RegsArm64 arm64;
|
||||
arm64.SetPseudoRegister(Arm64Reg::ARM64_PREG_RA_SIGN_STATE, 1);
|
||||
arm64.SetPACMask(0x007fff8000000000ULL);
|
||||
arm64.set_pc(0x0020007214bb3a04ULL);
|
||||
EXPECT_EQ(0x0000007214bb3a04ULL, arm64.pc());
|
||||
}
|
||||
|
||||
TEST_F(RegsTest, machine_type) {
|
||||
RegsArm arm_regs;
|
||||
EXPECT_EQ(ARCH_ARM, arm_regs.Arch());
|
||||
|
|
|
@ -55,7 +55,7 @@ static DwarfLocationEnum GetReturnAddressLocation(uint64_t rel_pc, DwarfSection*
|
|||
return DWARF_LOCATION_INVALID;
|
||||
}
|
||||
dwarf_loc_regs_t regs;
|
||||
if (!section->GetCfaLocationInfo(rel_pc, fde, ®s)) {
|
||||
if (!section->GetCfaLocationInfo(rel_pc, fde, ®s, ARCH_UNKNOWN)) {
|
||||
return DWARF_LOCATION_INVALID;
|
||||
}
|
||||
|
||||
|
|
|
@ -96,7 +96,7 @@ void DumpDwarfSection(Elf* elf, DwarfSection* section, uint64_t) {
|
|||
printf(" <%s>", name.c_str());
|
||||
}
|
||||
printf("\n");
|
||||
if (!section->Log(2, UINT64_MAX, fde)) {
|
||||
if (!section->Log(2, UINT64_MAX, fde, elf->arch())) {
|
||||
printf("Failed to process cfa information for entry at 0x%" PRIx64 "\n", fde->pc_start);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -64,7 +64,8 @@ void PrintExpression(Memory* memory, uint8_t class_type, uint64_t end, uint64_t
|
|||
}
|
||||
}
|
||||
|
||||
void PrintRegInformation(DwarfSection* section, Memory* memory, uint64_t pc, uint8_t class_type) {
|
||||
void PrintRegInformation(DwarfSection* section, Memory* memory, uint64_t pc, uint8_t class_type,
|
||||
ArchEnum arch) {
|
||||
const DwarfFde* fde = section->GetFdeFromPc(pc);
|
||||
if (fde == nullptr) {
|
||||
printf(" No fde found.\n");
|
||||
|
@ -72,7 +73,7 @@ void PrintRegInformation(DwarfSection* section, Memory* memory, uint64_t pc, uin
|
|||
}
|
||||
|
||||
dwarf_loc_regs_t regs;
|
||||
if (!section->GetCfaLocationInfo(pc, fde, ®s)) {
|
||||
if (!section->GetCfaLocationInfo(pc, fde, ®s, arch)) {
|
||||
printf(" Cannot get location information.\n");
|
||||
return;
|
||||
}
|
||||
|
@ -128,6 +129,11 @@ void PrintRegInformation(DwarfSection* section, Memory* memory, uint64_t pc, uin
|
|||
break;
|
||||
}
|
||||
|
||||
case DWARF_LOCATION_PSEUDO_REGISTER: {
|
||||
printf("%" PRId64 " (pseudo)\n", loc->values[0]);
|
||||
break;
|
||||
}
|
||||
|
||||
case DWARF_LOCATION_UNDEFINED:
|
||||
printf("undefine\n");
|
||||
break;
|
||||
|
@ -199,7 +205,7 @@ int GetInfo(const char* file, uint64_t offset, uint64_t pc) {
|
|||
DwarfSection* section = interface->eh_frame();
|
||||
if (section != nullptr) {
|
||||
printf("\neh_frame:\n");
|
||||
PrintRegInformation(section, elf.memory(), pc, elf.class_type());
|
||||
PrintRegInformation(section, elf.memory(), pc, elf.class_type(), elf.arch());
|
||||
} else {
|
||||
printf("\nno eh_frame information\n");
|
||||
}
|
||||
|
@ -207,7 +213,7 @@ int GetInfo(const char* file, uint64_t offset, uint64_t pc) {
|
|||
section = interface->debug_frame();
|
||||
if (section != nullptr) {
|
||||
printf("\ndebug_frame:\n");
|
||||
PrintRegInformation(section, elf.memory(), pc, elf.class_type());
|
||||
PrintRegInformation(section, elf.memory(), pc, elf.class_type(), elf.arch());
|
||||
printf("\n");
|
||||
} else {
|
||||
printf("\nno debug_frame information\n");
|
||||
|
@ -219,7 +225,8 @@ int GetInfo(const char* file, uint64_t offset, uint64_t pc) {
|
|||
section = gnu_debugdata_interface->eh_frame();
|
||||
if (section != nullptr) {
|
||||
printf("\ngnu_debugdata (eh_frame):\n");
|
||||
PrintRegInformation(section, gnu_debugdata_interface->memory(), pc, elf.class_type());
|
||||
PrintRegInformation(section, gnu_debugdata_interface->memory(), pc, elf.class_type(),
|
||||
elf.arch());
|
||||
printf("\n");
|
||||
} else {
|
||||
printf("\nno gnu_debugdata (eh_frame)\n");
|
||||
|
@ -228,7 +235,8 @@ int GetInfo(const char* file, uint64_t offset, uint64_t pc) {
|
|||
section = gnu_debugdata_interface->debug_frame();
|
||||
if (section != nullptr) {
|
||||
printf("\ngnu_debugdata (debug_frame):\n");
|
||||
PrintRegInformation(section, gnu_debugdata_interface->memory(), pc, elf.class_type());
|
||||
PrintRegInformation(section, gnu_debugdata_interface->memory(), pc, elf.class_type(),
|
||||
elf.arch());
|
||||
printf("\n");
|
||||
} else {
|
||||
printf("\nno gnu_debugdata (debug_frame)\n");
|
||||
|
|
Loading…
Reference in a new issue