Skip to content

lldb cannot call a function via a pointer that includes non-address bits (that aren't part of Top Byte Ignore) #134247

@DavidSpickett

Description

@DavidSpickett

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions