Skip to content

Arm64: [PAC-RET] Add Pointer Authentication support for Arm64#125436

Open
SwapnilGaikwad wants to merge 69 commits intodotnet:mainfrom
SwapnilGaikwad:github-add-pac
Open

Arm64: [PAC-RET] Add Pointer Authentication support for Arm64#125436
SwapnilGaikwad wants to merge 69 commits intodotnet:mainfrom
SwapnilGaikwad:github-add-pac

Conversation

@SwapnilGaikwad
Copy link
Copy Markdown
Contributor

@SwapnilGaikwad SwapnilGaikwad commented Mar 11, 2026

This PR adds support for Pointer Authentication (PAC) on Arm64. Pointer Authentication (PAC) is an Armv8.3+ security feature designed to mitigate Return-Oriented Programming (ROP) attacks by cryptographically signing return addresses. While using PAC, we store a signed return address, instead of the plain address, on the stack and later authenticate it before returning from a function. It ensures control flow returns to the intended caller.

More details on PAC and its role in software security can be found (here).

  • The current implementation of PAC is turned off by default, but can be turned on by setting DOTNET_JitPacEnabled=1.
  • PAC protects link register (LR) by signing it in the prolog (using paciasp) before it is split, using the current SP as the modifier. It then authenticates the LR in the epilog (using autiasp) before the function returns. If the signature is invalid, the execution fails with SIGILL.
  • When the runtime needs to read or overwrite a return address during hijacking for GC, it now strips the PAC (using xpaclri) and re-signs the new target address before storing it back.

ToDos

  • Disable PAC by default before merge.
  • Restore the original frame layout that used pre-indexed variant of stp to store FP/LR.
  • Authenticate the return address instead of stripping in return address hijacking and unwinding.
  • Identify increased binary size for System.*.dll
  • Determine performance regressions using benchmarks such as OrchirdCMS.

Contributes to #109457

This PR adds support for Pointer Authentication (PAC) on Arm64. Pointer Authentication (PAC) is an Armv8.3+ security feature designed to mitigate Return-Oriented Programming (ROP) attacks by cryptographically signing return addresses. While using PAC, we store a signed return address, instead of the plain address, on the stack and later authenticate it before returning from a function. It ensures control flow returns to the intended caller.

More details on PAC and its role in software security can be found ([here](https://llsoftsec.github.io/llsoftsecbook/#sec:pointer-authentication)).

- The current implementation of PAC is turned off by default, but can be turned on by setting DOTNET_JitPacEnabled=1.
- PAC protects link register (LR) by signing it in the prolog (using `paciasp`) before it is split, using the current SP as the modifier. It then authenticates the LR in the epilog (using `autiasp`) before the function returns. If the signature is invalid, the execution fails with `SIGILL`.
- - When the runtime needs to read or overwrite a return address during hijacking for GC, it now strips the PAC (using `xpaclri`) and re-signs the new target address before storing it back.
- To simply tracking the SP in return address hijacking, we avoid using the pre-indexed variant of storing FP/LR on stack (e.g., `stp fp,lr,[sp,-#framesz]! `) to simply tracking the SP in return address hijacking. We obtain the value of SP at the time of signing the LR from the location of the current FP.  We can't use this approach when the pre-indexed `stp` is used because we don't know the`#framesz`.
- The updated prolog/epilog sequences generated by the JIT now look like:

 // Prolog
 sub     sp, sp, #framesz
 paciasp                 ; sign LR with A-key + SP
 stp     fp, lr, [sp]

 // Epilog
 ldp     fp, lr, [sp]
 autiasp                 ; authenticate LR
 add     sp, sp, #framesz
 ret

ToDos:
[] Restore the original frame layout that used pre-indexed variant of `stp` to store FP/LR.
[] Authenticate the return address instead of stripping in return address hijacking and unwinding.
[] Identify increased binary size for System.*.dll
[] Determine performance regressions using benchmarks such as OrchirdCMS.
@dotnet-policy-service dotnet-policy-service Bot added the community-contribution Indicates that the PR has been added by a community member label Mar 11, 2026
@jkotas jkotas added area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI arch-arm64 and removed area-NativeAOT-coreclr labels Mar 11, 2026
@dotnet-policy-service
Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

@SwapnilGaikwad
Copy link
Copy Markdown
Contributor Author

If you would like to start getting the changes in fast, it may be best to peel it into separate PRs per area. The JIT changes would be the first one. We can certainly keep iterating on one large PR if you prefer. It will likely take longer to get it in since different parts need to reviewed and signed off by different people.

  • JIT changes (src\coreclr\jit...)
  • Regular CoreCLR changes (src\coreclr...)
  • NativeAOT changes (src\coreclr\nativeaot...)

Sure, happy to peel this PR into smaller ones. I'll start creating them sequentially and tag them here.

Created #127838 with JIT changes. Marked PAC to default disabled value (JitPacEnabled=0) as tests would need other changes to pass correctly.

Comment thread src/coreclr/jit/unwindarm64.cpp
Comment thread src/coreclr/nativeaot/Runtime/StackFrameIterator.cpp Outdated

#if defined(TARGET_ARM64)
// We strip the PC here as it's not being used to branch execution to.
m_ControlPC = PacStripPtr(m_ControlPC);
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 would expect the stripping to be always done when we are converting return address to a PC.

It is hard to see that stripping in the middle like here is done correctly. We should avoid fields that are mix of stripped and signed values depending on the code location.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

To avoid a location dependent value, I updated the patch to always save plain return address in m_RegDisplay instead of stripping it on-demand at use sites. Not sure if this is an ideal approach but it avoids confusion.
Happy to incorporate if you suggest any other approach.

Comment thread src/coreclr/nativeaot/Runtime/windows/CoffNativeCodeManager.cpp Outdated
Comment thread src/coreclr/vm/threadsuspend.cpp Outdated
Comment thread src/coreclr/vm/excep.cpp
*pSpForPacSign = 0;

// In prolog or epilog while the current frame is still being established or torn down
// retrieving correct SP is complex. We conservatively bail-out in this case.
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.

This is a potential perf regression (longer GC pauses). It will need either fixing or targeted testing to prove that it is ok.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed!


if (HasPacInUnwindInfo(pUnwindDataBlob, unwindDataBlobSize))
{
*pSpForArm64PacSign = pRegisterSet->GetSP();
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.

The regular CoreCLR JIT version parses the unwind info to compute the signing SP. Do we need to do the same here? Or is there something why it is not needed here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You're right. I missed this, it relied on older version of the patch where I had updated prolog to do paciasp right before saving FP/LR. The HasPacInUnwindInfo() should look similar to GetPacSignInfo(). I'll update this. I'm surprised how it wasn't flagged up on Windows CI workflows.

Comment thread src/coreclr/vm/excep.cpp
…ckinfo

This covers a usecase where SP adjustments are done before PAC instruction.
e.g.,

```
sub sp, sp, #0x30
paciasp
stp fp, lr, [sp, #0x50]!
```
Comment thread src/coreclr/jit/codegenarm64.cpp Outdated
Comment on lines +5703 to +5705
#ifdef TARGET_WINDOWS
GetEmitter()->emitIns(INS_autib1716);
#else
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.

This should use TargetOS::IsWindows.

(I assume you meant to push this to #127838)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This should use TargetOS::IsWindows.

Cool, I'll update it.

(I assume you meant to push this to #127838)

This is a full PR but we are going to peel parts of it into separate PRs to simply reviews from area owners and faster merging. However, the partial versions won't be able to pass tests, e.g., PAC with return address hijacking. Thus, I'm maintaining this PR with all the changes and when they pass CI here, I'm moving them to their respective part.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I put a note on #127838 that the full changes are here and can be looked at for test output. However, I understand that it may cause extra work for the reviewers. To avoid this, I will try to update both the PRs simultaneously. Not sure if that would help. Feel free to suggest anything that could be improved 🙂

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

arch-arm64 area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI community-contribution Indicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants