-
Notifications
You must be signed in to change notification settings - Fork 15.2k
Description
While looking into debug support for https://discourse.llvm.org/t/rfc-structure-protection-a-family-of-uaf-mitigation-techniques/85555/7 I found that I cannot call a function via a pointer if it has non-address bits that the processor still pays attention to.
The main use case of this is AArch64 pointer authentication. The existing test case lldb/test/API/linux/aarch64/non_address_bit_code_break/main.c
can be used to show the problem:
#include <stdint.h>
void foo(void) {}
typedef void (*FooPtr)(void);
int main() {
FooPtr fnptr = foo;
// Set top byte.
fnptr = (FooPtr)((uintptr_t)fnptr | (uintptr_t)0xff << 56);
// Then apply a PAuth signature to it.
__asm__ __volatile__("pacdza %0" : "=r"(fnptr) : "r"(fnptr));
// fnptr is now:
// <8 bit top byte tag><pointer signature><virtual address>
foo(); // Set break point at this line.
return 0;
}
Once we hit the breakpoint, fnptr
will have a top byte tag and a pointer signature. The top byte tag isn't a problem because the hardware will ignore it (I'm on AArch64 with Top Byte Ignore enabled), but the signature will not be ignored.
This leads to a fault when you try to call the function:
(lldb) p fnptr()
error: Expression execution was interrupted: signal SIGSEGV: address not mapped to object (fault address=0x1faaaaaaaa0714).
The process has been returned to the state before expression evaluation.
That address is the value of fnptr
, minus the top byte, presumably because the hardware or kernel removed that for us.
LLDB does know that there are non address bits, and can remove them for other commands:
(lldb) p fnptr
(FooPtr) 0xff1faaaaaaaa0714 (actual=0x0000aaaaaaaa0714 test.o`foo at test.c:3:17)
(lldb) memory read fnptr
0xaaaaaaaa0714: 1f 20 03 d5 c0 03 5f d6 fd 7b be a9 fd 03 00 91 . ...._..{......
0xaaaaaaaa0724: 00 00 00 90 00 50 1c 91 e0 0f 00 f9 e0 0f 40 f9 .....P........@.
(lldb) process status -v
<...>
Addressable code address mask: 0xff7f000000000000
Addressable data address mask: 0xff7f000000000000
Number of bits used in addressing (code): 49
So at first I thought we just needed to add an abi->FixCodeAddress
call somewhere, but nothing helped.
Then I realised, the problem is that we insert this fnptr
into the JIT'd code as a symbol, it's not something we take the address of normally and we may not even care to check if it will be used for a function call.
log enable lldb expr
shows:
lldb == [UserExpression::Evaluate] Parsing expression fnptr() ==
lldb ClangUserExpression::ScanContext()
lldb Parsing the following code:
#line 1 "<lldb wrapper prefix>"
<...>
void
$__lldb_expr(void *$__lldb_arg)
{
using $__lldb_local_vars::fnptr;
;
#line 1 "<user expression 0>"
fnptr()
;
#line 1 "<lldb wrapper suffix>"
}
<...>
lldb Examining _ZN18$__lldb_local_varsL5fnptrE, DeclForGlobalValue returns 0x0000AB432C2BB4F8
lldb MaybeHandleVariable (@"_ZN18$__lldb_local_varsL5fnptrE" = external constant ptr, align 8)
lldb Type of "fnptr" is [clang "FooPtr &", llvm "ptr"] [size 8, align 8]
lldb Adding value for (NamedDecl*)0x0000AB432C2BB4F8 [fnptr - fnptr] to the structure
lldb Placed at 0x0
lldb Element arrangement:
lldb Arg: "ptr %"$__lldb_arg""
lldb "fnptr" ("fnptr") placed at 0
lldb Replacing [@"_ZN18$__lldb_local_varsL5fnptrE" = external constant ptr, align 8]
So we are placing the raw value of fnptr
into the JIT context and trying to call that, which causes the fault.
(lldb) setting set target.process.unwind-on-error-in-expressions false
(lldb) p fnptr()
error: Expression execution was interrupted: signal SIGSEGV: address not mapped to object (fault address=0x50aaaaaaaa0714).
The process has been left at the point where it was interrupted, use "thread return -x" to return to the state before expression evaluation.
Process 226746 stopped
* thread #1, name = 'test.o', stop reason = signal SIGSEGV: address not mapped to object (fault address=0x50aaaaaaaa0714)
frame #0: 0x0000aaaaaaaa0714 test.o`foo at test.c:3:17
1 #include <stdint.h>
2
-> 3 void foo(void) {}
<...>
(lldb) bt
* thread #1, name = 'test.o', stop reason = signal SIGSEGV: address not mapped to object (fault address=0x50aaaaaaaa0714)
* frame #0: 0x0000aaaaaaaa0714 test.o`foo at test.c:3:17
frame #1: 0x0000fffff7ff508c $__lldb_expr`$__lldb_expr($__lldb_arg=0x0000fffff7ff3000) at <user expression 0>:1
frame #2: 0x0000aaaaaaaa0600 test.o`abort + 16
(lldb) up
frame #1: 0x0000fffff7ff508c $__lldb_expr`$__lldb_expr($__lldb_arg=0x0000fffff7ff3000) at <user expression 0>:1
(lldb) dis
$__lldb_expr`$__lldb_expr:
<...>
0xfffff7ff5088 <+88>: blr x8
-> 0xfffff7ff508c <+92>: ldr x19, [sp, #0x10]
The same issue applies if you're trying to emulate the effect of Pointer Authentication and use the target.process.virtual-addressable-bits
setting to ignore bits.
In a program like the one above we have no way to know if a pointer is signed, but we might with a program compiled for an ABI that always does that.
We could check whether the type is a function pointer when we set up the JIT context, and fix it then. Though you could break that, by casting something into a function pointer, but at that point you're asking for trouble anyway.