RFC: AArch64 Guarded Control Stack support for LLDB

Guarded Control Stack (GCS) is a new AArch64 feature that protects programs against Return Oriented Programming (ROP) attacks, amongst others.

If you’ve heard of “shadow stacks” it’s what you’d expect for the most part.

There is a brief introduction on Arm’s blog:
Challenge Validation

(ignore Discourse not rendering the link correctly, it goes to the Arm blog, honest)

I recommend you start with that.

Of course the full details are in the architecture manual:
Challenge Validation
(search for FEAT_GCS)

It’s quite a simple extension so the purpose of this RFC is not to discuss a whole bunch of controversial changes, but instead to provide some background on how it impacts LLDB so you can be better informed reviewing the PRs later.

No Going Back, Mostly

The big thing to understand is that once you’ve enabled the GCS, you cannot return from that point unless you manually push values to the control stack.

For example to enable GCS from Linux userspace you make a prctl syscall. However if you do that using the libc’s prctl() the program will fault immediately after the syscall returns.

Consider the call stack when prctl() is first called.

Stack Control Stack
_start ?
main ?
prctl ?

The control stack is empty as it has not been enabled. As soon as we return from the syscall the control stack will have been allocated somewhere, but will still be empty.

When prctl() executes ret the processor will check the GCS, find that the top entry does not match the return address, and fault.

The solution to this is to use an inline assembly wrapper to make the syscall. Then push any locations you know you will return to, to the GCS. One of LLDB’s test cases will do this.

Expressions and GCS

Let’s apply the same logic to expression evaluation. This time the program has enabled GCS already and we’re stopped in lldb.

(lldb) p some_func()

some_func() will get put into a wrapper function to be JIT’d into the process:

<something> __lldb_expr {
  some_internal_var = some_func();
  return;
}

LLDB sets the PC to the start of __lldb_expr and breaks the address that __lldb_expr returns to so it can tell when the expression is done (this address is usually the program entry point, it doesn’t matter what it is reallly, we never execute it).

The control stack prior to the expression is:

Control Stack
_start
main

Meaning that main thinks it should return to _start and so on.

If we add the expression calls:

Control Stack
_start
main
some_func
<anything some_func calls>

Note that __lldb_expr is never called, so there is no return address for it on the GCS.

This means that the call to some_func will work as expected until it returns into __lldb_expr. When __lldb_expr returns, the processor will compare some address in main to the program entry point and fault because they don’t match.

To fix this we must push an extra return address to the GCS prior to starting the expression.

Control Stack
_start
main
_start
some_func
etc…

Now when some_func returns, __lldb_expr can return to _start and LLDB will see that the expression has finished.

Then LLDB resets the GCS pointer and the GCS is back to where it was before the expression.

Extras

Another use of GCS is to provide a fast backtrace method for unwinders and other tools, and we could certainly try the same thing. GCS could provide a last resort unwind plan for instance.

Another use case is being able to compare the GCS to the normal backtrace. The normal backtrace is “where is the program trying to go” and the GCS is “where did we expect the program to go”. This is probably only useful if you have hit a GCS fault, but it’s interesting at least.

At this point these ideas are just that, ideas. I’m focusing on enabling the feature first. When I find the time I will look into these nice to have features.

PRs

So far I have put up the docs: [lldb][Docs] Add Guarded Control Stack to AArch64 Linux page by DavidSpickett · Pull Request #117860 · llvm/llvm-project · GitHub
The NT_ARM_GCS constant we need to use: [llvm][llvm-readobj] Add NT_ARM_GCS Linux core note type by DavidSpickett · Pull Request #117545 · llvm/llvm-project · GitHub
And the start of shadow stack support: https://github.com/llvm/llvm-project/pull/117861

More to come once those are in, currently I have 7 changes to implement this.

GCS can be a bit of a mind bender but it’s also a bit magic when it finally clicks.

And as superb as @omjavaid 's feedback always is, I want everyone to be able to review if they want to.