Skip to content

Add support for aarch64-unknown-linux-pauthtest#755

Open
jchlanda wants to merge 1 commit into
rust-lang:masterfrom
jchlanda:jakub/pauthtest
Open

Add support for aarch64-unknown-linux-pauthtest#755
jchlanda wants to merge 1 commit into
rust-lang:masterfrom
jchlanda:jakub/pauthtest

Conversation

@jchlanda

@jchlanda jchlanda commented Apr 20, 2026

Copy link
Copy Markdown

This PR adds support for aarch64-unknown-linux-pauthtest, a target that enables Pointer Authentication Code (PAC) support in Rust on AArch64 ELF based Linux systems. It uses the aarch64-unknown-linux-pauthtest LLVM target and a pointer-authentication-enabled sysroot with a custom musl as a reference libc implementation. Dynamic linking is required, with a dynamic linker acting as the ELF interpreter that can resolve pauth relocations and enforce pointer authentication constraints.

Please consult a rust-lang PR for the details on the target: rust-lang/rust#155722

// as uw::_Unwind_FindEnclosingFunction creates a new context so
// the SP used for signing here would belong to a different frame
// that the one used for auth-resign. Hence return a raw value.
self.ip()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is ip not signed? It is directly the result of _Unwind_GetIP. It should be possible to pass that to _Unwind_FindEnclosingFunction. How else would you even be able to use _Unwind_FindEnclosingFunction otherwise?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is ip not signed? It is directly the result of _Unwind_GetIP.

A raw ip is expected. _Unwind_GetIP authenticates the pointer and returns a stripped version (via ptrauth_auth_data here).

It should be possible to pass that to _Unwind_FindEnclosingFunction. How
else would you even be able to use _Unwind_FindEnclosingFunction otherwise?

This is the crux of the issue. _Unwind_FindEnclosingFunction is fundamentally incompatible with the pointer authentication model. The problem is that _Unwind_FindEnclosingFunction does not follow PAC semantics, it blindly creates a fresh unwind cursor and then resets the instruction pointer here: __unw_set_reg(cursor, UNW_REG_IP, pc);. As if to say: "treat this pc as if it belonged to this new cursor's frame".

__unw_set_reg does apply PAC logic internally (authenticates) under the assumption that the ip behaves like a return address for the current frame.

This cannot be fixed on the Rust side. _Unwind_FindEnclosingFunction constructs a new cursor (and therefore a new sp), so there is no way to correctly sign pointer: any signing we would perform would use the wrong context and would still fail authentication inside libunwind.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds like a bug in libunwind to me. What purpose does _Unwind_FindEnclosingFunction have at all if you can't pass the result of _Unwind_GetIP to it?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure, the documentation seems to be pretty scarce, and one I found is rather quite lax about possible outcomes of the call, explicitly allowing failure.

I personally don't read it as a bug. To me it is more that _Unwind_FindEnclosingFunction was designed before pointer authentication existed, and its API implicitly assumes that instruction pointers are context-free values. But in PAC-aware code PC is only meaningful in the context of the (original) SP.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does _Unwind_FindEnclosingFunction need an authenticated pointer? It doesn't dereference the pointer, only looks it up in a side table with the result not having any security relevance. The only useful thing you can do with it afaik is looking up debuginfo for the function. Due to inlining and outlining you can't guarantee that it points to any particular function.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please let me know if my description of why uw::_Unwind_FindEnclosingFunction is incompatible with pointer authentication is insufficient. Alternatively, if you have any ideas on how it should be addressed, I would be very keen to hear them.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bjorn3 If you have any questions related to how pointer authentication is working in terms of libunwind, I would be happy to answer - I was previously working on that from Linux perspective, while most work was done by folks from Apple.

I confirm @jchlanda 's reasoning in the thread above and I think that it should be good enough to just return self.ip() for pauthtest target.

Also note that the signing/auehtntication behavior from libunwind is mostly shared between Apple's arm64e and Linux's pauthtest (when not platform-specific, e.g. personality pointers are stored and signed differently on Darwin and Linux). So, while I do not believe that we have any bug in libunwind's behavior and API design, even if we do - it would require a big discussion involving folks from Apple. And at this point, it's probably better just to conform to status-quo here in backtrace-rs.

So, we would very much appreciate your feedback on this

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think using self.ip() here will break the internal frame omission for backtraces:

backtrace-rs/src/capture.rs

Lines 300 to 303 in 06b2cc5

// clear inner frames, and start with call site.
if frame.symbol_address() as usize == ip {
frames.clear();
}

And in tokio: https://github.com/tokio-rs/tokio/blob/dac81bf8c8de0a3e35f1626643674ba9faf9569c/tokio/src/runtime/task/trace/trace_impl.rs#L16

I think it would also effectively break the scoped-trace crate: https://github.com/jswrenn/scoped-trace/blob/e299c46905a2063957414c34d76264281852461b/src/lib.rs#L126

Is there really no way to retain this support? Even if just stripping the pointer signature? _Unwind_FindEnclosingFunction can be fixed to accept arbitrary pointer signatures without compromising security, right?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will need to dig into it again to provide a more detailed answer, but for now, a couple of quick observations:

  • returning self.ip() is exactly what happens on Apple, so the precedence is already there;
  • I'm not familiar with neither Tokio nor scoped-trace, so I can't comment on their relative importance in the wider ecosystem. What I can say with confidence, though, is that the current implementation (uw::_Unwind_FindEnclosingFunction(self.ip())) causes every program compiled for aarch64-unknown-linux-pauthtest that relies on backtrace-rs to encounter a pointer authentication failure. As a first approximation, that's every program that panics.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bjorn3 I spent some time looking into this issue again.

To sum up, there are two stages:

  1. backtrace-rs obtains the instruction pointer.
    This is done with _Unwind_GetIP(context). On PAC-enabled AArch64, libunwind authenticates the stored PC and returns an instruction pointer value associated with the original frame context (including the frame's SP-based PAC discriminator). That value is what we keep in Frame::ip().
uint64_t Registers_arm64::getIP() const {
  uint64_t value = _registers.__pc;

  value = (uint64_t)ptrauth_auth_and_resign(
      (void *)_registers.__pc,
      ptrauth_key_return_address,
      &_registers.__pc,
      ptrauth_key_return_address,
      getSP());

  return value;
}
  1. backtrace-rs calls _Unwind_FindEnclosingFunction.
    The expectation is that libunwind returns the start address of the enclosing function. This call is implemented as:
    unw_cursor_t cursor;
    unw_context_t uc;
    unw_proc_info_t info;
    __unw_getcontext(&uc);
    __unw_init_local(&cursor, &uc);
    __unw_set_reg(&cursor, UNW_REG_IP, (unw_word_t)(intptr_t)pc);
    if (__unw_get_proc_info(&cursor, &info) == UNW_ESUCCESS)
        return (void *)(intptr_t) info.start_ip;

The root cause of the issue is in this snippet: the cursor is not the original frame that produced pc. It is a brand new unwind cursor initialized from the current thread context.

On PAC-enabled AArch64, the instruction pointer returned by _Unwind_GetIP() is not simply an arbitrary integer address. Although it is suitable for use as a code address by callers, it is still a value whose PAC validity depends on the original frame context (in particular, the SP discriminator used by the PAC scheme). When _Unwind_FindEnclosingFunction() imports this IP into its newly created cursor, it goes through __unw_set_reg(), which calls the AArch64 implementation of Registers_arm64::setIP(value):

value = (uint64_t)ptrauth_auth_and_resign(
    (void *)value,
    ptrauth_key_return_address,
    getSP(),
    ptrauth_key_return_address,
    &_registers.__pc);

_registers.__pc = value;

getSP() here returns the SP stored in the newly created cursor. It is not the SP belonging to the frame from which _Unwind_GetIP() obtained the instruction pointer. Therefore, setIP() attempts to authenticate an IP that belongs to the original frame using the SP of a different frame. The authentication step fails because the PAC discriminator does not match; the issue is not that the IP lacks a signature, but that it is being reintroduced into a different unwind context.

Is there really no way to retain this support?

Not from backtrace-rs, as far as I can see. By the time we call _Unwind_FindEnclosingFunction, all we have is the instruction pointer returned by _Unwind_GetIP(). _Unwind_FindEnclosingFunction immediately constructs its own unwind cursor and, as we know now, this must fail authentication - since the original SP is no longer available. And because that cursor is created entirely inside libunwind, backtrace-rs has no opportunity to provide the original SP that was associated with the instruction pointer returned by _Unwind_GetIP(). From our side, there doesn't appear to be any way to make this call succeed.

Even if just stripping the pointer signature?

_Unwind_GetIP() already returns the public instruction pointer representation expected by callers. The problem is not that backtrace-rs passes an unstripped/signed pointer incorrectly; the problem is that _Unwind_FindEnclosingFunction() tries to import that pointer into a new unwind context with a different SP, eventually calls Registers_arm64::setIP(), which authenticates/re-signs the supplied value using the SP of the newly-created cursor.

_Unwind_FindEnclosingFunction can be fixed to accept arbitrary pointer
signatures without compromising security, right?

I think it should be possible. I earlier said that _Unwind_FindEnclosingFunction is fundamentally incompatible with PAC enabled arch, that is probably not fully correct, I should have said: the current implementation in LLVM's libunwind doesn't work for this use case.

For example, if libunwind implemented _Unwind_FindEnclosingFunction without synthesizing a fresh unwind cursor, or otherwise avoided routing through Registers_arm64::setIP(), then the problem would not manifest. I am not in position to say if either of those is possible to implement.

And finally, from the perspective of backtrace-rs, the current implementation aborts on aarch64-unknown-linux-pauthtest, and we don't have enough information (most notably the original frame's SP) to make this call succeed with the current API.

Therefore, the only implementation that works reliably for pauthtest today is to use the instruction pointer already returned by _Unwind_GetIP() (self.ip()), which is also what backtrace-rs already does on Apple targets.

Comment thread src/backtrace/libunwind.rs Outdated
// clause, and if this is fixed that test in theory can be run on macOS!
if cfg!(target_vendor = "apple") {
self.ip()
} else if cfg!(target_env = "pauthtest") {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this not check for the respective target feature being enabled? Not every target with pointer authentication enabled will use that target env, right?

@jchlanda jchlanda Apr 23, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right.
We're in the middle of preparing an upstream patch that introduces a pointer authentication aware target. It just so happens that in there I ended up using (incorrectly) the target environment as the gating mechanism.

We have a follow up task to resolve the target to a set of features, just like it's done in clang where it solves platform/environment specifics on the driver level and later on everything is based on language flags that are there regardless of the platform. And finally are resolved to a concrete signing schema.

This will become a bit more clear once the PR lands and I submit a ticket.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was decided that using target_env = "musl" and target_abi = "pauthtest"is the way forward, I've updated the check to be target_abi = "pauthtest".

The rust-lang commit introducing the change: rust-lang/rust@229e0f5

@jchlanda
jchlanda marked this pull request as ready for review May 12, 2026 17:19
@jchlanda

Copy link
Copy Markdown
Author

Although the parent change (introduction of aarch64-unknown-linux-pauthtest) has not landed yet, it seems this should not prevent this PR from being marked as ready for review.

@jchlanda

Copy link
Copy Markdown
Author

@davidtwco, @folkertdev, @tgross35, @madsmtm FWI this is a follow up to rust-lang/rust#155722

@workingjubilee

Copy link
Copy Markdown
Member

Hello. That PR needs to land first for this to be accepted, but it can be reviewed now? Noted. Drawing up my queue again.

@jchlanda

jchlanda commented May 27, 2026

Copy link
Copy Markdown
Author

Hello. That PR needs to land first for this to be accepted, but it can be reviewed now? Noted. Drawing up my queue again.

Yeap, it's ready for review. If you feel like going on a review spree, there is plenty more pointer auth stuff:

Thank you!

@jchlanda

jchlanda commented Jul 1, 2026

Copy link
Copy Markdown
Author

Hi all (@davidtwco, @folkertdev, @tgross35, @madsmtm, @bjorn3, @workingjubilee),

The PR introducing the pauthtest target has been merged into main. I'd be very grateful if we could move this one forward as well.

Thank you!

@tgross35

tgross35 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

No need to ping me on pauthtest events - you already have reviewers on your PRs, I'm not following pauthtest anything special, and I don't really do reviews for backtrace or cc. I believe that applies to most of the people in that list.

@jchlanda

jchlanda commented Jul 2, 2026

Copy link
Copy Markdown
Author

No need to ping me on pauthtest events - you already have reviewers on your PRs, I'm not following pauthtest anything special, and I don't really do reviews for backtrace or cc. I believe that applies to most of the people in that list.

Feel free to ignore it then, apologies for the noise.

@workingjubilee

Copy link
Copy Markdown
Member

And my time is uncompensated, as it is for many reviewers. This is a problem that Access Softek can address if it wants.

@jchlanda

jchlanda commented Jul 7, 2026

Copy link
Copy Markdown
Author

@bjorn3 the other PAC related rust-lang PRs have now been earmarked for merge, could we please move this one forward?

This PR adds support for `aarch64-unknown-linux-pauthtest`, a target that
enables Pointer Authentication Code (PAC) support in Rust on AArch64 ELF based
Linux systems using a pauthtest ABI (provided by LLVM) and pauthtest-enabled
sysroot with custom musl, serving as a reference libc implementation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants