Skip to content

Arm64: [PAC-RET] NativeAOT changes#128950

Open
SwapnilGaikwad wants to merge 31 commits into
dotnet:mainfrom
SwapnilGaikwad:github-aot-pac
Open

Arm64: [PAC-RET] NativeAOT changes#128950
SwapnilGaikwad wants to merge 31 commits into
dotnet:mainfrom
SwapnilGaikwad:github-aot-pac

Conversation

@SwapnilGaikwad

Copy link
Copy Markdown
Contributor

This PR covers the final subset of changes from #125436 related to NativeAOT as suggested in comment.
It follows the previous work from- #127949, #127838 and #128147.

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

cc: @dotnet/arm64-contrib @a74nh @jkotas @dhartglassMSFT

As suggested in [comment](dotnet#125436 (comment)), this PR covers subset of changes from dotnet#125436 related to NativeAOT.

More details on PAC and its role in software security can be found ([here](https://llsoftsec.github.io/llsoftsecbook/#sec:pointer-authentication)).
@dotnet-policy-service dotnet-policy-service Bot added the community-contribution Indicates that the PR has been added by a community member label Jun 3, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @agocke, @dotnet/ilc-contrib
See info in area-owners.md if you want to be subscribed.

Comment thread src/coreclr/inc/daccess.h Outdated
@jkotas

jkotas commented Jun 11, 2026

Copy link
Copy Markdown
Member

/azp run runtime-nativeaot-outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@jkotas

jkotas commented Jun 12, 2026

Copy link
Copy Markdown
Member

Tests failing on linux-arm64 with

Debug Assertion Violation

Expression: 'IsUnwindable((PTR_VOID)pRegisterSet->IP)'

File: /__w/1/s/src/coreclr/nativeaot/Runtime/unix/UnixNativeCodeManager.cpp, Line: 1476

Comment thread src/coreclr/tools/aot/ILCompiler.RyuJit/JitInterface/CorInfoImpl.RyuJit.cs Outdated
@jkotas

jkotas commented Jun 28, 2026

Copy link
Copy Markdown
Member

Tests are crashing on linux-arm64 and osx-arm64

Add assert to ensure PAC is being emitted before stack adjustment

Change-Id: Ifb636d2bb3fd3c78d33274ea7786291b5ee83fd6
Change-Id: Ic64dd02db14e691d3ea88212544059e9d9892062
@SwapnilGaikwad

Copy link
Copy Markdown
Contributor Author

Tests are crashing on linux-arm64 and osx-arm64

These crashes came from changes to bail out if we interrupt on RET when pac is enabled. Previously PR was handling RET incorrect in TrailingEpilogueInstructionsCount that led to hijacking the current frame instead of the parent frame on return, now moved the check to PAC specific part.

return false;
}

// At RET, FP/LR/SP have already been restored to the caller state so

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 check be in TrailingEpilogueInstructionsCount so that all epilog-related checks are together?

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.

True, it was in there before and returned 1 on RET instruction (cannot return 0/-1 as it means not in in epilog/unknown). However when PAC is disabled, it allowed hijacking the current frame when interrupted on the RET instruction. That caused the crashes that we saw earlier. To keep PAC related changes separate, I added them in PAC specific area. I can extract the RET check into a separate function for better readability.

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.

when PAC is disabled, it allowed hijacking the current frame when interrupted on the RET instruction

Where is hijacking on RET rejected in main before these changes? We are not able to hijack on RET instruction - we can only hijack when the return address is spilled on stack.

returned 1 on RET instruction (cannot return 0/-1 as it means not in in epilog/unknown).

TrailingEpilogueInstructionsCount never returns 1 on Arm64, even for instructions that are clearly epilog. It always returns -1 for those. I do not see a problem with returning -1 for RET to follow the pattern.

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.

Apologies, my wording was wrong. I meant that on main we do not detect the RET instruction in TrailingEpilogueInstructionsCount(), so it returns 0. That lets GetReturnAddressHijackInfo() continue into the existing unwind path, which unwinds the current frame and may then identify the caller return-address slot for hijacking.
Returning -1 for RET would stop that path, even when PAC is disabled. I initially kept the RET check PAC-specific to avoid changing non-PAC behaviour.

I agreed your point that Arm64 already uses -1 for unsafe epilog states, and RET fits that pattern since FP/LR/SP have already been restored. Updated to return -1 for RET from TrailingEpilogueInstructionsCount().

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 lets GetReturnAddressHijackInfo() continue into the existing unwind path, which unwinds the current frame and may then identify the caller return-address slot for hijacking.

There is no return-address slot to hijacking when we are stopped at the RET instruction. The return address is in LR register at that point. I am still missing where this gets rejected in main today. I think it must be getting rejected somehow.

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 think it's line 1524 that rejects hijacks on RET.

if (pRegisterSet->GetReturnAddressRegisterLocation() == oldLocation)
{
    // This is the case when we are either:
    //
    // 1) In a leaf method that does not push return address register on stack, OR
    // 2) In the prolog/epilog of a non-leaf method that has not yet pushed return address register on stack
    //    or has return address register already popped off.
    return false;
}

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.

If this is rejecting RET, it should be rejecting it both with and without PAC changes. So why do you need to reject the RET explicitly in your change?

Comment thread src/coreclr/nativeaot/Runtime/unix/UnixNativeCodeManager.cpp Outdated
@SwapnilGaikwad

Copy link
Copy Markdown
Contributor Author

Failure seems related to the tagged issues, not with the PR.

Comment thread src/coreclr/nativeaot/Runtime/arm64/GcProbe.asm Outdated
Comment thread src/coreclr/nativeaot/Runtime/arm64/GcProbe.asm Outdated
Comment thread src/coreclr/nativeaot/Runtime/unix/UnixNativeCodeManager.cpp Outdated
Comment thread src/coreclr/nativeaot/Runtime/windows/CoffNativeCodeManager.cpp
Comment thread src/coreclr/nativeaot/Runtime/StackFrameIterator.cpp Outdated
@SwapnilGaikwad

Copy link
Copy Markdown
Contributor Author

Failures seem unrelated to the PR.

#endif

#if defined(TARGET_AMD64)
*pSpForArm64PacSign = 0;

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.

Clear it as the first thing in the method - same as for Unix

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

Labels

area-NativeAOT-coreclr 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.

2 participants