From f67ef2867bc5e74cdadb3acb790c3fc6161b22e9 Mon Sep 17 00:00:00 2001 From: Andrew Kilroy Date: Thu, 17 Feb 2022 12:19:23 +0000 Subject: Unwind with pointer authentication on arm64 This patch adds the ability for libunwind to unwind a stack where the return address obtained from the arm64 link register (x30) has a pointer authentication code (PAC). Without this patch, the unw_step function terminates early, leaving the user the impression there is only a leaf frame when there are more. The reason for the premature termination is that an aarch64 specific CFI opcode 'DW_CFA_AARCH64_negate_ra_state' is not recognised, and treated as unexpected. Upon the next iteration's call of unw_step, it reports the end of the stack. For an expected callstack of loop // most recent call call3 call2 call1 main // oldest call The caller of libunwind observes: unw_step() = 1 unw_get_reg(UNW_REG_IP) = 0, pc = 0xaaaae4720798 unw_step() = 1 unw_get_reg(UNW_REG_IP) = 0, pc = 0x5faaaae47207cc // ip with PAC unw_step() = 0 // premature end For remote unwinding, this patch adds an optional callback function 'ptrauth_insn_mask' on unw_accessors_t so that unw_step can correctly strip off the PAC to leave the correct return address. This also has the benefit that an attempt to get the name of the function with unw_get_proc_name can succeed. The callback is given the original unw_addr_space_t and as_arg as given to unw_init_remote. The application needs to ensure the mask to be returned is somehow available on the as_arg and callback should return it. The callback is only needed for remote unwinding. In local unwinding, an instruction is used to strip the PAC. A description of pointer authentication is in the armv8 reference manual available here: https://developer.arm.com/documentation/ddi0487/ha/?lang=en (Version H.a, section D5.1.5 Pointer authentication in AArch64 state, page D5-4772) Also documentation for the use of DWARF with pointer authentication, in particular the RA_SIGN_STATE: https://github.com/ARM-software/abi-aa/blob/main/aadwarf64/aadwarf64.rst --- src/aarch64/Ginit.c | 6 ++++ src/aarch64/Gregs.c | 3 ++ src/dwarf/Gparser.c | 80 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+) (limited to 'src') diff --git a/src/aarch64/Ginit.c b/src/aarch64/Ginit.c index fe6e511d..17b8fcbc 100644 --- a/src/aarch64/Ginit.c +++ b/src/aarch64/Ginit.c @@ -408,6 +408,11 @@ get_static_proc_name (unw_addr_space_t as, unw_word_t ip, return _Uelf64_get_proc_name (as, getpid (), ip, buf, buf_len, offp); } +static unw_word_t empty_ptrauth_mask(unw_addr_space_t addr_space_unused, void *as_arg_unused) +{ + return 0; +} + HIDDEN void aarch64_local_addr_space_init (void) { @@ -421,6 +426,7 @@ aarch64_local_addr_space_init (void) local_addr_space.acc.access_fpreg = access_fpreg; local_addr_space.acc.resume = aarch64_local_resume; local_addr_space.acc.get_proc_name = get_static_proc_name; + local_addr_space.acc.ptrauth_insn_mask = empty_ptrauth_mask; local_addr_space.big_endian = target_is_big_endian(); unw_flush_cache (&local_addr_space, 0, 0); } diff --git a/src/aarch64/Gregs.c b/src/aarch64/Gregs.c index a8843734..5ae47254 100644 --- a/src/aarch64/Gregs.c +++ b/src/aarch64/Gregs.c @@ -88,6 +88,9 @@ tdep_access_reg (struct cursor *c, unw_regnum_t reg, unw_word_t *valp, case UNW_AARCH64_PSTATE: loc = c->dwarf.loc[reg]; break; + case UNW_AARCH64_RA_SIGN_STATE: + Debug (1, "Reading from ra sign state not supported: %u\n", reg); + return -UNW_EBADREG; case UNW_AARCH64_SP: if (write) diff --git a/src/dwarf/Gparser.c b/src/dwarf/Gparser.c index 7cfc4e50..edd34526 100644 --- a/src/dwarf/Gparser.c +++ b/src/dwarf/Gparser.c @@ -87,6 +87,13 @@ empty_rstate_stack(dwarf_stackable_reg_state_t **rs_stack) pop_rstate_stack(rs_stack); } +#ifdef UNW_TARGET_AARCH64 + +static void +aarch64_negate_ra_sign_state(dwarf_state_record_t *sr); + +#endif + /* Run a CFI program to update the register state. */ static int run_cfi_program (struct dwarf_cursor *c, dwarf_state_record_t *sr, @@ -404,6 +411,11 @@ run_cfi_program (struct dwarf_cursor *c, dwarf_state_record_t *sr, (regnum - 16) * sizeof (unw_word_t)); Debug (15, "CFA_GNU_window_save\n"); break; +#elif UNW_TARGET_AARCH64 + /* This is a specific opcode on aarch64, DW_CFA_AARCH64_negate_ra_state */ + Debug (15, "DW_CFA_AARCH64_negate_ra_state\n"); + aarch64_negate_ra_sign_state(sr); + break; #else /* FALL THROUGH */ #endif @@ -767,6 +779,61 @@ eval_location_expr (struct dwarf_cursor *c, unw_word_t stack_val, unw_addr_space return 0; } + +#ifdef UNW_TARGET_AARCH64 +#include "libunwind-aarch64.h" + +static void +aarch64_negate_ra_sign_state(dwarf_state_record_t *sr) +{ + unw_word_t ra_sign_state = sr->rs_current.reg.val[UNW_AARCH64_RA_SIGN_STATE]; + ra_sign_state ^= 0x1; + set_reg(sr, UNW_AARCH64_RA_SIGN_STATE, DWARF_WHERE_SAME, ra_sign_state); +} + +static unw_word_t +aarch64_strip_pac_remote(unw_accessors_t *a, unw_addr_space_t as, void *arg, unw_word_t old_ip) +{ + if (a->ptrauth_insn_mask) + { + unw_word_t ip, insn_mask; + + insn_mask = a->ptrauth_insn_mask(as, arg); + ip = old_ip & (~insn_mask); + + Debug(15, "stripping pac from address, before: %lx, after: %lx\n", old_ip, ip); + return ip; + } + else + { + Debug(15, "return address %lx might be signed, but no means to obtain mask\n", old_ip); + return old_ip; + } +} + +static unw_word_t +aarch64_strip_pac_local(unw_word_t in_addr) +{ + unw_word_t out_addr = in_addr; + +#ifdef __aarch64__ + // Strip the PAC with XPACLRI instruction + register unsigned long long x30 __asm__("x30") = in_addr; + __asm__("hint 0x7" : "+r" (x30)); + out_addr = x30; +#endif + + return out_addr; +} + +static unw_word_t +aarch64_get_ra_sign_state(struct dwarf_reg_state *rs) +{ + return rs->reg.val[UNW_AARCH64_RA_SIGN_STATE]; +} + +#endif + static int apply_reg_state (struct dwarf_cursor *c, struct dwarf_reg_state *rs) { @@ -892,6 +959,19 @@ apply_reg_state (struct dwarf_cursor *c, struct dwarf_reg_state *rs) ret = dwarf_get (c, c->loc[rs->ret_addr_column], &ip); if (ret < 0) return ret; +#ifdef UNW_TARGET_AARCH64 + if (aarch64_get_ra_sign_state(rs)) + { + if (c->as != unw_local_addr_space) + { + ip = aarch64_strip_pac_remote(a, as, arg, ip); + } + else + { + ip = aarch64_strip_pac_local(ip); + } + } +#endif c->ip = ip; ret = 1; } -- cgit v1.2.1