[libunwind][arm64e] Mach-O Support unwinding PAuth_LR frames (#202772)

Under PAuth_LR, compact unwind entries marked UNWIND_ARM64_MODE_FRAME_PAUTH_LR are signed with the PC of the pacibsppc at the start of the range. The compiler may emit those entries with UNWIND_IS_NOT_FUNCTION_START when the pacibsppc is not the first instruction in the function; the unwinder must scan backwards past such entries to find the actual function start (and its LSDA pointer).

When unwinding a PAuth_LR frame, authenticate and re-sign using setIPPAuthLR(), which calls __builtin_ptrauth_auth_with_pc_and_resign when available, falling back to inline asm (autib171615 + pacib) for compilers that lack the builtin.

To reduce the size of Compact Unwind entries for shrinkwrapped frames, new offset bits encode the distance in multiples of 4 bytes from the range start to the pacibsppc. The compiler emits paired FRAMELESS/FRAMELESS+PAUTH_LR entries that the linker merges into a single FRAME_PAUTH_LR | (offset/4) << 12 encoding.

rdar://161907302&169746432&171118419
This commit is contained in:
Jon Roelofs 2026-07-24 14:17:11 -07:00 committed by GitHub
parent 8ba8c16cde
commit 8e34b2cbb0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 170 additions and 58 deletions

View file

@ -39,16 +39,14 @@
//
typedef uint32_t compact_unwind_encoding_t;
// architecture independent bits
// clang-format off
enum {
UNWIND_IS_NOT_FUNCTION_START = 0x80000000,
UNWIND_HAS_LSDA = 0x40000000,
UNWIND_PERSONALITY_MASK = 0x30000000,
};
// clang-format on
//
// x86
@ -67,6 +65,7 @@ enum {
// 3-bits register count
// 10-bits register permutation
//
// clang-format off
enum {
UNWIND_X86_MODE_MASK = 0x0F000000,
UNWIND_X86_MODE_EBP_FRAME = 0x01000000,
@ -84,7 +83,9 @@ enum {
UNWIND_X86_DWARF_SECTION_OFFSET = 0x00FFFFFF,
};
// clang-format on
// clang-format off
enum {
UNWIND_X86_REG_NONE = 0,
UNWIND_X86_REG_EBX = 1,
@ -94,6 +95,7 @@ enum {
UNWIND_X86_REG_ESI = 5,
UNWIND_X86_REG_EBP = 6,
};
// clang-format on
//
// For x86 there are four modes for the compact unwind encoding:
@ -182,9 +184,6 @@ enum {
//}
//
//
// x86_64
//
@ -202,6 +201,7 @@ enum {
// 3-bits register count
// 10-bits register permutation
//
// clang-format off
enum {
UNWIND_X86_64_MODE_MASK = 0x0F000000,
UNWIND_X86_64_MODE_RBP_FRAME = 0x01000000,
@ -219,7 +219,9 @@ enum {
UNWIND_X86_64_DWARF_SECTION_OFFSET = 0x00FFFFFF,
};
// clang-format on
// clang-format off
enum {
UNWIND_X86_64_REG_NONE = 0,
UNWIND_X86_64_REG_RBX = 1,
@ -229,6 +231,8 @@ enum {
UNWIND_X86_64_REG_R15 = 5,
UNWIND_X86_64_REG_RBP = 6,
};
// clang-format on
//
// For x86_64 there are four modes for the compact unwind encoding:
// UNWIND_X86_64_MODE_RBP_FRAME:
@ -267,7 +271,6 @@ enum {
// function.
//
// ARM64
//
// 1-bit: start
@ -283,11 +286,16 @@ enum {
// DWARF:
// 24-bits offset of DWARF FDE in __eh_frame section
//
// clang-format off
enum {
UNWIND_ARM64_MODE_MASK = 0x0F000000,
UNWIND_ARM64_MODE_FRAMELESS = 0x02000000,
UNWIND_ARM64_MODE_DWARF = 0x03000000,
UNWIND_ARM64_MODE_FRAME = 0x04000000,
UNWIND_ARM64_MODE_FRAME_PAUTH_LR = 0x05000000,
UNWIND_ARM64_PAUTH_LR_OFFSET_MASK = 0x00FFF000,
UNWIND_ARM64_PAUTH_LR_OFFSET_SHIFT = 12,
UNWIND_ARM64_FRAME_X19_X20_PAIR = 0x00000001,
UNWIND_ARM64_FRAME_X21_X22_PAIR = 0x00000002,
@ -302,6 +310,8 @@ enum {
UNWIND_ARM64_FRAMELESS_STACK_SIZE_MASK = 0x00FFF000,
UNWIND_ARM64_DWARF_SECTION_OFFSET = 0x00FFFFFF,
};
// clang-format on
//
// For arm64 there are three modes for the compact unwind encoding:
// UNWIND_ARM64_MODE_FRAME:
// This is a standard arm64 prolog where FP/LR are immediately pushed on the
@ -323,11 +333,13 @@ enum {
// This mode is never used in object files. It is only generated by the
// linker in final linked images which have only DWARF unwind info for a
// function.
//
// For arm64e there is one more mode for compact unwind encoding:
// UNWIND_ARM64_MODE_FRAME_PAUTH_LR:
// An arm64e prologue, where LR is signed in the range with a `pacibsppc`.
// The UNWIND_ARM64_PAUTH_LR_OFFSET_MASK bits specify an unsigned positive
// offset (encoded as a multiple of 4 bytes) from the beginning of the range
// to the `pacibsppc`, with 0 indicating that the `pacibsppc` is the first
// instruction in the range.
////////////////////////////////////////////////////////////////////////////////
//

View file

@ -520,6 +520,7 @@ int CompactUnwinder_arm64<A>::stepWithCompactEncoding(
A &addressSpace, Registers_arm64 &registers) {
switch (compactEncoding & UNWIND_ARM64_MODE_MASK) {
case UNWIND_ARM64_MODE_FRAME:
case UNWIND_ARM64_MODE_FRAME_PAUTH_LR:
return stepWithCompactEncodingFrame(compactEncoding, functionStart,
addressSpace, registers);
case UNWIND_ARM64_MODE_FRAMELESS:
@ -619,7 +620,7 @@ int CompactUnwinder_arm64<A>::stepWithCompactEncodingFrameless(
template <typename A>
int CompactUnwinder_arm64<A>::stepWithCompactEncodingFrame(
compact_unwind_encoding_t encoding, uint64_t, A &addressSpace,
compact_unwind_encoding_t encoding, uint64_t functionStart, A &addressSpace,
Registers_arm64 &registers) {
Registers_arm64::reg_t savedRegisterLoc = registers.getFP() - 8;
@ -698,7 +699,21 @@ int CompactUnwinder_arm64<A>::stepWithCompactEncodingFrame(
registers.setSP(fp + 16);
// pop return address into pc
registers.setIP(addressSpace.get64(fp + 8));
Registers_arm64::reg_t linkRegister = addressSpace.get64(fp + 8);
// authenticate lr, if needed
if ((encoding & UNWIND_ARM64_MODE_MASK) == UNWIND_ARM64_MODE_FRAME_PAUTH_LR) {
// Offset from the beginning of the range to the pacibsppc, in words.
uint32_t offset = (encoding & UNWIND_ARM64_PAUTH_LR_OFFSET_MASK) >>
UNWIND_ARM64_PAUTH_LR_OFFSET_SHIFT;
// Address of the pacibsppc
uint64_t pacibsppcAddr = functionStart + offset * 4;
registers.setIPPAuthLR(linkRegister, pacibsppcAddr);
} else {
registers.setIP(linkRegister);
}
return UNW_STEP_SUCCESS;
}

View file

@ -300,16 +300,28 @@ int DwarfInstructions<A, R>::stepWithDwarf(
isSignalFrame = cieInfo.isSignalFrame;
#if defined(_LIBUNWIND_TARGET_AARCH64) && \
!defined(_LIBUNWIND_TARGET_AARCH64_AUTHENTICATED_UNWINDING)
#if defined(_LIBUNWIND_TARGET_AARCH64)
// There are two ways of return address signing: pac-ret (enabled via
// -mbranch-protection=pac-ret) and ptrauth-returns (enabled as part of
// Apple's arm64e or experimental pauthtest ABI on Linux). The code
// below handles signed RA for pac-ret, while ptrauth-returns uses
// different logic.
// below handles signed RA for ptrauth-returns, while pac-ret uses pacm
// instructions from the hint space.
//
// TODO: unify logic for both cases, see
// https://github.com/llvm/llvm-project/issues/160110
//
#if defined(_LIBUNWIND_TARGET_AARCH64_AUTHENTICATED_UNWINDING)
if (getReturnAddressSignStatus(addressSpace, registers, cfa, prolog) ==
RASignedWithPC) {
newRegisters.setIPPAuthLR(returnAddress, prolog.ptrAuthDiversifier);
} else {
newRegisters.setIP(returnAddress);
}
// Simulate the step by replacing the register set with the new ones.
registers = newRegisters;
return UNW_STEP_SUCCESS;
#else
// If the target is aarch64 then the return address may have been signed
// using the v8.3 pointer authentication extensions. The original
// return address needs to be authenticated before the return address is
@ -352,6 +364,7 @@ int DwarfInstructions<A, R>::stepWithDwarf(
#endif
}
#endif
#endif
#if defined(_LIBUNWIND_IS_NATIVE_ONLY) && defined(_LIBUNWIND_TARGET_ARM) && \
defined(__ARM_FEATURE_PAUTH)

View file

@ -1896,11 +1896,9 @@ public:
#if defined(_LIBUNWIND_TARGET_AARCH64_AUTHENTICATED_UNWINDING)
// Note the value of the PC was signed to its address in the register state
// but everyone else expects it to be signed by the SP, so convert on return.
value = (uint64_t)ptrauth_auth_and_resign((void *)_registers.__pc,
ptrauth_key_return_address,
&_registers.__pc,
ptrauth_key_return_address,
getSP());
value = (uint64_t)ptrauth_auth_and_resign(
(void *)_registers.__pc, ptrauth_key_return_address, &_registers.__pc,
ptrauth_key_return_address, _registers.__sp);
#endif
return value;
}
@ -1909,14 +1907,37 @@ public:
// Note the value which was set should have been signed with the SP.
// We then resign with the slot we are being stored in to so that both SP
// and LR can't be spoofed at the same time.
value = (uint64_t)ptrauth_auth_and_resign((void *)value,
ptrauth_key_return_address,
getSP(),
ptrauth_key_return_address,
&_registers.__pc);
value = (uint64_t)ptrauth_auth_and_resign(
(void *)value, ptrauth_key_return_address, _registers.__sp,
ptrauth_key_return_address, &_registers.__pc);
#endif
_registers.__pc = value;
}
__attribute__((target("pauth-lr"))) void setIPPAuthLR(uint64_t value,
uint64_t signing_pc) {
#if defined(_LIBUNWIND_TARGET_AARCH64_AUTHENTICATED_UNWINDING)
#ifndef __has_builtin
#define __has_builtin(x) 0
#endif
#if __has_builtin(__builtin_ptrauth_auth_with_pc_and_resign)
value = (uint64_t)ptrauth_auth_with_pc_and_resign(
(void *)value, ptrauth_key_return_address, _registers.__sp, signing_pc,
ptrauth_key_return_address, &_registers.__pc);
#else
register uint64_t x17 __asm("x17") = value;
register uint64_t x16 __asm("x16") = _registers.__sp;
register uint64_t x15 __asm("x15") = signing_pc;
uint64_t resignDisc = (uint64_t)&_registers.__pc;
asm("autib171615\n\t"
"pacib %0, %3"
: "+r"(x17)
: "r"(x16), "r"(x15), "r"(resignDisc));
value = x17;
#endif
#endif
_registers.__pc = value;
}
uint64_t getFP() const { return _registers.__fp; }
void setFP(uint64_t value) { _registers.__fp = value; }

View file

@ -1911,6 +1911,7 @@ bool UnwindCursor<A, R>::getInfoFromCompactEncodingSection(
// do a binary search of second level page index
uint32_t encoding = 0;
pint_t funcStart = 0;
pint_t rangeStart = 0;
pint_t funcEnd = 0;
pint_t lsda = 0;
pint_t personality = 0;
@ -1950,21 +1951,39 @@ bool UnwindCursor<A, R>::getInfoFromCompactEncodingSection(
}
}
encoding = pageIndex.encoding(low);
funcStart = pageIndex.functionOffset(low) + sects.dso_base;
rangeStart = pageIndex.functionOffset(low) + sects.dso_base;
// If UNWIND_IS_NOT_FUNCTION_START is set, walk backwards to find the actual
// function start.
funcStart = rangeStart;
if (encoding & UNWIND_IS_NOT_FUNCTION_START) {
assert(low != 0 &&
"UNWIND_IS_NOT_FUNCTION_START comact unwind must be preceded by a "
"~UNWIND_IS_NOT_FUNCTION_START entry for the same function");
uint32_t backIndex = low;
do {
--backIndex;
} while (backIndex > 0 &&
(pageIndex.encoding(backIndex) & UNWIND_IS_NOT_FUNCTION_START));
funcStart = pageIndex.functionOffset(backIndex) + sects.dso_base;
}
if (pc < funcStart) {
if (log)
fprintf(
stderr,
"\tpc not in table, pc=0x%llX, funcStart=0x%llX, funcEnd=0x%llX\n",
(uint64_t) pc, (uint64_t) funcStart, (uint64_t) funcEnd);
fprintf(stderr,
"\tpc not in table, pc=0x%llX, funcStart=0x%llX, "
"rangeStart=0x%llX, funcEnd=0x%llX\n",
(uint64_t)pc, (uint64_t)funcStart, (uint64_t)rangeStart,
(uint64_t)funcEnd);
return false;
}
if (pc > funcEnd) {
if (log)
fprintf(
stderr,
"\tpc not in table, pc=0x%llX, funcStart=0x%llX, funcEnd=0x%llX\n",
(uint64_t) pc, (uint64_t) funcStart, (uint64_t) funcEnd);
fprintf(stderr,
"\tpc not in table, pc=0x%llX, funcStart=0x%llX, "
"rangeStart=0x%llX, funcEnd=0x%llX\n",
(uint64_t)pc, (uint64_t)funcStart, (uint64_t)rangeStart,
(uint64_t)funcEnd);
return false;
}
} else if (pageKind == UNWIND_SECOND_LEVEL_COMPRESSED) {
@ -1998,19 +2017,55 @@ bool UnwindCursor<A, R>::getInfoFromCompactEncodingSection(
high = mid;
}
}
funcStart = pageIndex.functionOffset(low) + firstLevelFunctionOffset
+ sects.dso_base;
rangeStart = pageIndex.functionOffset(low) + firstLevelFunctionOffset +
sects.dso_base;
if (low < last)
funcEnd =
pageIndex.functionOffset(low + 1) + firstLevelFunctionOffset
+ sects.dso_base;
else
funcEnd = firstLevelNextPageFunctionOffset + sects.dso_base;
auto encodingAtIndex = [&](uint32_t idx) -> uint32_t {
uint16_t encIdx = pageIndex.encodingIndex(idx);
if (encIdx < sectionHeader.commonEncodingsArrayCount()) {
return _addressSpace.get32(
sects.compact_unwind_section +
sectionHeader.commonEncodingsArraySectionOffset() +
encIdx * sizeof(uint32_t));
} else {
uint16_t pageEncIdx =
encIdx - (uint16_t)sectionHeader.commonEncodingsArrayCount();
return _addressSpace.get32(secondLevelAddr +
pageHeader.encodingsPageOffset() +
pageEncIdx * sizeof(uint32_t));
}
};
encoding = encodingAtIndex(low);
// If UNWIND_IS_NOT_FUNCTION_START is set, walk backwards to find the actual
// function start.
funcStart = rangeStart;
if (encoding & UNWIND_IS_NOT_FUNCTION_START) {
assert(low != 0 &&
"UNWIND_IS_NOT_FUNCTION_START comact unwind must be preceded by a "
"~UNWIND_IS_NOT_FUNCTION_START entry for the same function.");
uint32_t backIndex = low;
do {
--backIndex;
} while (backIndex > 0 &&
(encodingAtIndex(backIndex) & UNWIND_IS_NOT_FUNCTION_START));
funcStart = pageIndex.functionOffset(backIndex) +
firstLevelFunctionOffset + sects.dso_base;
}
if (pc < funcStart) {
_LIBUNWIND_DEBUG_LOG("malformed __unwind_info, pc=0x%llX "
"not in second level compressed unwind table. "
"funcStart=0x%llX",
(uint64_t) pc, (uint64_t) funcStart);
"funcStart=0x%llX, rangeStart=0x%llX",
(uint64_t)pc, (uint64_t)funcStart,
(uint64_t)rangeStart);
return false;
}
if (pc > funcEnd) {
@ -2020,21 +2075,6 @@ bool UnwindCursor<A, R>::getInfoFromCompactEncodingSection(
(uint64_t) pc, (uint64_t) funcEnd);
return false;
}
uint16_t encodingIndex = pageIndex.encodingIndex(low);
if (encodingIndex < sectionHeader.commonEncodingsArrayCount()) {
// encoding is in common table in section header
encoding = _addressSpace.get32(
sects.compact_unwind_section +
sectionHeader.commonEncodingsArraySectionOffset() +
encodingIndex * sizeof(uint32_t));
} else {
// encoding is in page specific table
uint16_t pageEncodingIndex =
encodingIndex - (uint16_t)sectionHeader.commonEncodingsArrayCount();
encoding = _addressSpace.get32(secondLevelAddr +
pageHeader.encodingsPageOffset() +
pageEncodingIndex * sizeof(uint32_t));
}
} else {
_LIBUNWIND_DEBUG_LOG(
"malformed __unwind_info at 0x%0llX bad second level page",
@ -2111,7 +2151,18 @@ bool UnwindCursor<A, R>::getInfoFromCompactEncodingSection(
fprintf(stderr, "getInfoFromCompactEncodingSection(pc=0x%llX), "
"encoding=0x%08X, lsda=0x%08llX for funcStart=0x%llX\n",
(uint64_t) pc, encoding, (uint64_t) lsda, (uint64_t) funcStart);
// For ARM64 PAuth_LR frames, start_ip should be the pacibsppc address
// (rangeStart). For all other cases, start_ip should be the function start.
#if defined(_LIBUNWIND_TARGET_AARCH64)
if ((encoding & UNWIND_ARM64_MODE_MASK) == UNWIND_ARM64_MODE_FRAME_PAUTH_LR) {
_info.start_ip = rangeStart;
} else {
_info.start_ip = funcStart;
}
#else
_info.start_ip = funcStart;
#endif
_info.end_ip = funcEnd;
_info.lsda = lsda;
// We use memmove to copy the personality function as we have already manually