[libunwind] [libunwind][PAC] Defang ptrauth's PC in valid CFI range abort (PR #184041)
Oliver Hunt via cfe-commits
cfe-commits at lists.llvm.org
Wed Mar 11 01:15:12 PDT 2026
https://github.com/ojhunt updated https://github.com/llvm/llvm-project/pull/184041
>From 502727b85aca7c61752f4715c4d2e8973ad51cbc Mon Sep 17 00:00:00 2001
From: Oliver Hunt <oliver at apple.com>
Date: Sun, 1 Mar 2026 13:28:10 -0800
Subject: [PATCH] [libunwind][PAC] Defang ptrauth's PC in valid CFI range abort
It turns out making the CFI check a release mode abort causes many,
if not the majority, of JITs to fail during unwinding as they do not
set up CFI sections for their generated code. As a result any JITs
that do nominally support unwinding (and catching) through their JIT
or assembly frames trip this abort.
rdar://170862047
---
libunwind/src/libunwind.cpp | 28 +++---
libunwind/test/cfi_violating_handler.pass.cpp | 91 +++++++++++++++++++
2 files changed, 102 insertions(+), 17 deletions(-)
create mode 100644 libunwind/test/cfi_violating_handler.pass.cpp
diff --git a/libunwind/src/libunwind.cpp b/libunwind/src/libunwind.cpp
index a795e68c57859..2128a60c3ac89 100644
--- a/libunwind/src/libunwind.cpp
+++ b/libunwind/src/libunwind.cpp
@@ -131,23 +131,17 @@ _LIBUNWIND_HIDDEN int __unw_set_reg(unw_cursor_t *cursor, unw_regnum_t regNum,
{
// It is only valid to set the IP within the current function. This is
// important for ptrauth, otherwise the IP cannot be correctly signed.
- // The current signature of `value` is via the schema:
- // __ptrauth(ptrauth_key_return_address, <<sp>>, 0)
- // For this to be generally usable we manually re-sign it to the
- // directly supported schema:
- // __ptrauth(ptrauth_key_return_address, 1, 0)
- unw_word_t
- __unwind_ptrauth_restricted_intptr(ptrauth_key_return_address, 1,
- 0) authenticated_value;
- unw_word_t opaque_value = (uint64_t)ptrauth_auth_and_resign(
- (void *)value, ptrauth_key_return_address, sp,
- ptrauth_key_return_address, &authenticated_value);
- memmove(reinterpret_cast<void *>(&authenticated_value),
- reinterpret_cast<void *>(&opaque_value),
- sizeof(authenticated_value));
- if (authenticated_value < info.start_ip ||
- authenticated_value > info.end_ip)
- _LIBUNWIND_ABORT("PC vs frame info mismatch");
+ //
+ // However many JITs do not configure CFI frames, so we cannot actually
+ // enforce this - at least not without an extremely expensive syscall.
+ //
+ // For the forseeable future this will need to be a debug only assertion
+ // so we just strip and assert to avoid the unnecessary auths in release
+ // builds.
+ unw_word_t stripped_value = (unw_word_t)ptrauth_strip(
+ (void *)value, ptrauth_key_return_address);
+ assert(stripped_value >= info.start_ip &&
+ stripped_value <= info.end_ip);
// PC should have been signed with the sp, so we verify that
// roundtripping does not fail. The `ptrauth_auth_and_resign` is
diff --git a/libunwind/test/cfi_violating_handler.pass.cpp b/libunwind/test/cfi_violating_handler.pass.cpp
new file mode 100644
index 0000000000000..c812da844822b
--- /dev/null
+++ b/libunwind/test/cfi_violating_handler.pass.cpp
@@ -0,0 +1,91 @@
+// -*- C++ -*-
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: target={{(aarch64|s390x|x86_64|arm64e)-.+}}
+// UNSUPPORTED: target={{.*-windows.*}}
+
+// *SAN does not like our clearly nonsense personality and handler functions
+// which is the correct response for them, but alas we have to allow it for JITs
+// because they tend to use a shared handler rather than having the handler
+// within the function bounds.
+// UNSUPPORTED: asan
+// UNSUPPORTED: msan
+
+#include <libunwind.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <unwind.h>
+
+extern "C" void exit(int);
+
+struct alignas(16) AlignmentTester {
+ __attribute__((noinline)) void testAlignment() {
+ uintptr_t thisValue = (uintptr_t)this;
+ if (thisValue % 16 != 0) {
+ fprintf(stderr, "Test bug: unaligned handler frame\n");
+ __builtin_trap();
+ }
+ }
+};
+
+#if defined(__x86_64__)
+// Use an intermediary function on x86_64 as the handlers are dispatched with
+// the expectation that they will perform some stack correction, most notably
+// planting down a restored base pointer
+extern "C" int randomUnrelatedFunctionImpl();
+__attribute__((naked)) int randomUnrelatedFunction() {
+ asm("pushq %rbp \n\t"
+ "movq %rsp, %rbp\n\t"
+ "jmp _randomUnrelatedFunctionImpl\n\t");
+}
+extern "C" __attribute__((noinline)) int randomUnrelatedFunctionImpl() {
+#else
+static int randomUnrelatedFunction() {
+#endif
+ AlignmentTester tester;
+ tester.testAlignment();
+ fprintf(stderr,
+ "Successfully dispatched to handler unrelated to actual function\n");
+ exit(0);
+ return 0;
+}
+
+extern "C" int __gxx_personality_v0(int, _Unwind_Action actions, uint64_t,
+ _Unwind_Exception *,
+ _Unwind_Context *context) {
+
+ if (actions & 1) // Search
+ return _URC_HANDLER_FOUND;
+
+ // Having claimed to have found a handler, we just set an unrelated
+ // the PC to an unrelated function
+#if defined(__PTRAUTH__) || __has_feature(ptrauth_calls)
+ uintptr_t sp = _Unwind_GetCFA(context);
+ uintptr_t handler = (uintptr_t)ptrauth_auth_and_resign(
+ (void *)&randomUnrelatedFunction, ptrauth_key_function_pointer, 0,
+ ptrauth_key_return_address, (void *)sp);
+#else
+ uintptr_t handler = (uintptr_t)&randomUnrelatedFunction;
+#endif
+ _Unwind_SetIP(context, handler);
+ return _URC_INSTALL_CONTEXT;
+}
+
+__attribute__((noinline)) static int throwAThing() { throw 1; }
+
+int main(int, const char **) {
+ try {
+ throwAThing();
+ } catch (int) {
+ fprintf(stderr, "Failed: did not call the overridden personality\n");
+ return 1;
+ }
+ fprintf(stderr, "Failed: did not trigger exception handler\n");
+ return 1;
+}
More information about the cfe-commits
mailing list