From f9747ab64d601710ae29ed0136de48166cc8aef3 Mon Sep 17 00:00:00 2001 From: Alex Forsythe Date: Wed, 15 Jul 2026 17:57:04 -0500 Subject: [PATCH 1/3] fix: Strip PAC bits from return addresses in POSIX crash handler refs: RUM-17370 ARM64 has a security feature called Pointer Authentication Codes (PAC), where the CPU cryptographically signs address values, encoding the signature in the upper bits of the 64-bit value. It can then verify these addresses before jumping to them, to detect stack manipulation etc. at the hardware level. The SDK's in-process crash handler for POSIX reads return addresses as it walks the stack frame. When running arm64 Linux builds in Docker, I noticed that these addresses weren't being correctly resolved to loaded modules for shared libraries. As it turns out, module resolution was failing because the upper bits were set by PAC, causing the resolved address to land outside the load range of the module. When compiled for ARMv8.3-a or later (__ARM_FEATURE_PAUTH), we use the xpaci instruction to strip PAC bits. This asks the hardware to remove exactly the signature bits using its own key and VA-width configuration, which is correct on kernels with 52-bit user VAs (CONFIG_ARM64_VA_BITS_52) where bits [51:48] are legitimate address bits that a hard-coded 48-bit mask would incorrectly zero. On ARMv8.0-8.2 hardware (no PAC, no 52-bit VA support), we fall back to masking the lower 48 bits. --- .../crash_handler_inprocess_posix.cpp | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/datadog/impl/crash_reporting/handlers/inprocess/crash_handler_inprocess_posix.cpp b/src/datadog/impl/crash_reporting/handlers/inprocess/crash_handler_inprocess_posix.cpp index 86f61c82..c63d925d 100644 --- a/src/datadog/impl/crash_reporting/handlers/inprocess/crash_handler_inprocess_posix.cpp +++ b/src/datadog/impl/crash_reporting/handlers/inprocess/crash_handler_inprocess_posix.cpp @@ -160,9 +160,37 @@ static void write_stack_trace(int fd, void* instruction_pointer, void* frame_poi // address points to the instruction to be executed after this function returns // (immediately following the call that created this frame). Symbolication tools // will adjust to resolve the actual call site. +#ifdef __aarch64__ + // Strip pointer authentication codes from the return address. On AArch64, the CPU + // encodes a PAC signature in the upper bits of return addresses stored on the + // stack. These bits must be cleared to recover the canonical virtual address before + // the address can be compared against module load ranges. + // + // When compiled for ARMv8.3-a or later (i.e. __ARM_FEATURE_PAUTH is defined), we + // use the xpaci instruction, which strips exactly the PAC bits as determined by the + // CPU's current key and VA configuration. This correctly handles kernels configured + // for 52-bit user VAs (CONFIG_ARM64_VA_BITS_52), where bits [51:48] are legitimate + // address bits that a hard-coded 48-bit mask would incorrectly zero. + // + // On ARMv8.0-8.2 hardware (no PAC support), the 48-bit mask is always correct + // because those CPUs never generate PAC-signed addresses and do not support 52-bit + // user VAs. + uint64_t raw_ret = reinterpret_cast(ret_addr); +#ifdef __ARM_FEATURE_PAUTH + // xpaci strips PAC bits from an instruction-pointer value using the CPU's + // knowledge of which bits are signature vs. address, so it's correct for any + // VA width (48-bit or 52-bit). + asm("xpaci %0" : "+r"(raw_ret)); +#else + // ARMv8.0-8.2: no PAC, no 52-bit VA; mask to the 48-bit canonical address. + raw_ret &= 0x0000ffffffffffff; +#endif + WriteCrashReportStackFrame(fd, raw_ret); +#else WriteCrashReportStackFrame( fd, static_cast(reinterpret_cast(ret_addr)) ); +#endif // Reading frame[0] into fp (effectively dereferencing fp) moves to the next frame fp = frame[0]; From 4b158bf4e89a3f5cdcc8f7d318ec09bac7a8af3b Mon Sep 17 00:00:00 2001 From: Alex Forsythe Date: Wed, 15 Jul 2026 18:21:47 -0500 Subject: [PATCH 2/3] fix: Use xpaci unconditionally to strip PAC bits on AArch64 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous approach guarded xpaci behind __ARM_FEATURE_PAUTH and fell back to a hard-coded 48-bit mask when the flag was absent. That flag is only defined when the compiler is invoked with -march=armv8.3-a or later, so on a generic arm64 build the mask would fire even on PAC-capable hardware — incorrectly zeroing bits [51:48] on kernels configured for 52-bit user VAs (CONFIG_ARM64_VA_BITS_52). xpaci is the right tool unconditionally: it asks the CPU to strip exactly the PAC bits using its own key and VA-width configuration, so it handles any VA width correctly. On pre-PAC hardware (ARMv8.0-8.2) it falls in the HINT space and executes as a NOP, which is also correct since those CPUs never encode PAC bits in addresses. Clang's integrated assembler accepts the instruction regardless of -march, so no compile-time feature guard is needed. --- .../crash_handler_inprocess_posix.cpp | 24 ++++++------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/src/datadog/impl/crash_reporting/handlers/inprocess/crash_handler_inprocess_posix.cpp b/src/datadog/impl/crash_reporting/handlers/inprocess/crash_handler_inprocess_posix.cpp index c63d925d..d2fe2e10 100644 --- a/src/datadog/impl/crash_reporting/handlers/inprocess/crash_handler_inprocess_posix.cpp +++ b/src/datadog/impl/crash_reporting/handlers/inprocess/crash_handler_inprocess_posix.cpp @@ -166,25 +166,15 @@ static void write_stack_trace(int fd, void* instruction_pointer, void* frame_poi // stack. These bits must be cleared to recover the canonical virtual address before // the address can be compared against module load ranges. // - // When compiled for ARMv8.3-a or later (i.e. __ARM_FEATURE_PAUTH is defined), we - // use the xpaci instruction, which strips exactly the PAC bits as determined by the - // CPU's current key and VA configuration. This correctly handles kernels configured - // for 52-bit user VAs (CONFIG_ARM64_VA_BITS_52), where bits [51:48] are legitimate - // address bits that a hard-coded 48-bit mask would incorrectly zero. - // - // On ARMv8.0-8.2 hardware (no PAC support), the 48-bit mask is always correct - // because those CPUs never generate PAC-signed addresses and do not support 52-bit - // user VAs. + // xpaci strips exactly the PAC bits using the CPU's own key and VA-width + // configuration, so it is correct regardless of whether the kernel uses 48-bit or + // 52-bit user VAs (CONFIG_ARM64_VA_BITS_52). On pre-PAC hardware (ARMv8.0-8.2) + // xpaci falls in the HINT space and executes as a NOP, which is also correct + // because those CPUs never encode PAC bits in addresses. Clang's integrated + // assembler accepts the instruction regardless of -march, so no feature guard is + // needed. uint64_t raw_ret = reinterpret_cast(ret_addr); -#ifdef __ARM_FEATURE_PAUTH - // xpaci strips PAC bits from an instruction-pointer value using the CPU's - // knowledge of which bits are signature vs. address, so it's correct for any - // VA width (48-bit or 52-bit). asm("xpaci %0" : "+r"(raw_ret)); -#else - // ARMv8.0-8.2: no PAC, no 52-bit VA; mask to the 48-bit canonical address. - raw_ret &= 0x0000ffffffffffff; -#endif WriteCrashReportStackFrame(fd, raw_ret); #else WriteCrashReportStackFrame( From 2f07efa00ae594b63a3ef381476e7eca3bf06ee0 Mon Sep 17 00:00:00 2001 From: Alex Forsythe Date: Wed, 15 Jul 2026 18:39:12 -0500 Subject: [PATCH 3/3] fix: Emit xpaci via .inst to avoid requiring -march=armv8.3-a+pauth Clang's assembler rejects 'xpaci' unless the target architecture includes the pauth feature (i.e. -march=armv8.3-a or later). Without an explicit -march flag, aarch64-linux-gnu builds default to armv8-a and fail to compile the inline asm, even though the instruction is safe to run on all AArch64 hardware. Fix this by pinning the operand to x16 and emitting the instruction via .inst 0xDAC143F0 (the fixed encoding for xpaci x16). The assembler emits the bytes directly without needing to know about pauth. x16 (IP0) is a caller-saved scratch register that is safe to use in a signal handler. The CI Linux jobs all run on amd64 runners so the aarch64 code path is never compiled there; this would only be caught by a customer building natively or cross-compiling for arm64 Linux. --- .../inprocess/crash_handler_inprocess_posix.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/datadog/impl/crash_reporting/handlers/inprocess/crash_handler_inprocess_posix.cpp b/src/datadog/impl/crash_reporting/handlers/inprocess/crash_handler_inprocess_posix.cpp index d2fe2e10..e6303a82 100644 --- a/src/datadog/impl/crash_reporting/handlers/inprocess/crash_handler_inprocess_posix.cpp +++ b/src/datadog/impl/crash_reporting/handlers/inprocess/crash_handler_inprocess_posix.cpp @@ -174,7 +174,16 @@ static void write_stack_trace(int fd, void* instruction_pointer, void* frame_poi // assembler accepts the instruction regardless of -march, so no feature guard is // needed. uint64_t raw_ret = reinterpret_cast(ret_addr); - asm("xpaci %0" : "+r"(raw_ret)); + // xpaci strips PAC bits from an instruction-pointer value. We pin the operand to + // x16 and emit the instruction via .inst so that the assembler does not require + // -march=armv8.3-a+pauth; the encoding is unambiguous and fixed for a given + // register. x16 (IP0) is a caller-saved scratch register and safe to use here. + // xpaci x16 = 0xDAC143F0 + { + register uint64_t r asm("x16") = raw_ret; + asm(".inst 0xDAC143F0" : "+r"(r)); + raw_ret = r; + } WriteCrashReportStackFrame(fd, raw_ret); #else WriteCrashReportStackFrame(