Skip to content

Fix ARM32 NativeAOT SIGSEGV: restore VRS helpers and __unw_*_reg outside _LIBUNWIND_NATIVEAOT guard#128830

Draft
Copilot wants to merge 3 commits into
mainfrom
copilot/fix-sigsegv-crash-arm32
Draft

Fix ARM32 NativeAOT SIGSEGV: restore VRS helpers and __unw_*_reg outside _LIBUNWIND_NATIVEAOT guard#128830
Copilot wants to merge 3 commits into
mainfrom
copilot/fix-sigsegv-crash-arm32

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Jun 1, 2026

PR #128667's _LIBUNWIND_NATIVEAOT guards accidentally excluded _Unwind_VRS_Get/Set/Pop (and their __unw_get/set_reg/__unw_save_vfp_as_X dependencies) from the NativeAOT build while leaving their caller _Unwind_VRS_Interpret enabled. The linker resolved the missing exported VRS symbols from libgcc_s.so.1, whose implementations treat NativeAOT's ArmUnwindCursor* shim as a real libgcc context → memory corruption → SIGSEGV on ARM32. x64/arm64 are unaffected (they use DWARF templates directly on Registers_REGDISPLAY, never calling _Unwind_VRS_*).

Changes

  • Unwind-EHABI.cpp: Split single _LIBUNWIND_NATIVEAOT guard into two blocks. ValueAsBitPattern, _Unwind_VRS_Set, _Unwind_VRS_Get_Internal, _Unwind_VRS_Get, _Unwind_VRS_Pop now sit between the two guards (always compiled). Exception-dispatch functions that depend on __unw_step (__aeabi_unwind_cpp_pr*, unwind_phase1/2, _Unwind_RaiseException, etc.) remain guarded.

  • libunwind.cpp: Split the single large _LIBUNWIND_NATIVEAOT guard into three inner guards, making __unw_get_reg, __unw_set_reg, __unw_get_fpreg, __unw_set_fpreg, and __unw_save_vfp_as_X unconditional. The singleton, __unw_init_local, __unw_step, __unw_resume, DWARF/FDE functions, and Apple dynamic sections remain guarded.

  • llvm-libunwind-version.txt: Recorded fix commit per repo convention for vendored source patches.

Original prompt

Summary

PR #128667 ("Guard unused llvm-libunwind symbols to avoid duplicates on Android") introduced a regression that causes SIGSEGV crashes during stack unwinding on ARM32 (32-bit ARM / EHABI) NativeAOT targets only. x64 and arm64 are unaffected. This reproduces on Linux arm32 (glibc), not just Android.

Root cause

On ARM32, NativeAOT unwinds managed frames through the EHABI virtual-register-set interpreter _Unwind_VRS_Interpret in src/native/external/llvm-libunwind/src/Unwind-EHABI.cpp. That function was deliberately kept enabled by PR #128667 (it sits above the new #if !defined(_LIBUNWIND_NATIVEAOT) guard, which starts at line 450).

However, _Unwind_VRS_Interpret calls the helper functions _Unwind_VRS_Get, _Unwind_VRS_Set, and _Unwind_VRS_Pop (and the ValueAsBitPattern helper they reference). Those three helpers are defined below the new guard (around lines 915, 1045, 1060), inside the #if !defined(_LIBUNWIND_NATIVEAOT) block that spans lines 450–1219. So when _LIBUNWIND_NATIVEAOT is defined, the PR accidentally removed _Unwind_VRS_Get/Set/Pop from the NativeAOT build while leaving their caller _Unwind_VRS_Interpret in place.

As a result, the linker resolves the now-missing _Unwind_VRS_Get/Set/Pop calls against the platform libgcc_s.so.1 (_Unwind_VRS_*@GCC_3.5). This was confirmed via nm -D on a final ARM32 executable, which shows:

U _Unwind_VRS_Get@GCC_3.5
U _Unwind_VRS_Pop@GCC_3.5
U _Unwind_VRS_Set@GCC_3.5
U __aeabi_unwind_cpp_pr0@GCC_3.5
U __aeabi_unwind_cpp_pr1@GCC_3.5
U __aeabi_unwind_cpp_pr2@GCC_3.5

NativeAOT's _Unwind_VRS_Interpret passes its own ArmUnwindCursor shim cast to _Unwind_Context* (see src/coreclr/nativeaot/Runtime/unix/UnwindHelpers.cpp, UnwindHelpers::StepFrame, the _LIBUNWIND_ARM_EHABI branch). The in-tree _Unwind_VRS_Get/Set/Pop understand this shim (cast to unw_cursor_tAbstractUnwindCursor* → virtual dispatch into Registers_REGDISPLAY). The libgcc versions expect a real libgcc _Unwind_Context and dereference it at different offsets → garbage reads/writes → SIGSEGV during unwinding.

x64/arm64 never call _Unwind_VRS_* (they use the DWARF/compact unwind templates directly on Registers_REGDISPLAY), which is exactly why only ARM32 regressed.

Required fix

Narrow the _LIBUNWIND_NATIVEAOT guard in src/native/external/llvm-libunwind/src/Unwind-EHABI.cpp so the helpers that _Unwind_VRS_Interpret depends on remain compiled into the NativeAOT build:

  • _Unwind_VRS_Get (and its internal helper _Unwind_VRS_Get_Internal)
  • _Unwind_VRS_Set
  • _Unwind_VRS_Pop
  • the ValueAsBitPattern helper they reference

These should be outside (above/before) the #if !defined(_LIBUNWIND_NATIVEAOT) guard, alongside _Unwind_VRS_Interpret which already is.

The genuinely-unused C++ exception-dispatch entry points must REMAIN behind the guard (they depend on __unw_step and other public API from libunwind.cpp that is excluded for NativeAOT):

  • __aeabi_unwind_cpp_pr0/pr1/pr2
  • unwind_phase1, unwind_phase2, unwind_phase2_forced
  • _Unwind_RaiseException, _Unwind_Resume, _Unwind_Complete
  • _Unwind_GetLanguageSpecificData, _Unwind_GetRegionStart, _Unwind_ForcedUnwind, _Unwind_DeleteException
  • __gnu_unwind_frame

In practice this means: move the #if !defined(_LIBUNWIND_NATIVEAOT) block boundaries so that _Unwind_VRS_Set, _Unwind_VRS_Get_Internal, _Unwind_VRS_Get, _Unwind_VRS_Pop, and ValueAsBitPattern are NOT guarded out, while everything currently guarded that depends on __unw_step stays guarded. Be careful with ValueAsBitPattern (it is [[gnu::unused]] and only referenced by _LIBUNWIND_TRACE_API in the VRS helpers) — keep it available wherever the VRS helpers are so there is no unused/missing-symbol issue.

This is a patch to a vendored external source. Per repo convention, also record the patch commit in src/native/external/llvm-libunwind-version.txt (append an Apply https://github.com/dotnet/runtime/commit/<sha> line as the previous patches do), consistent with how PR #128667 recorded its change.

Validation

After the change, the same nm -D on a built ARM32 executable should no longer list _Unwind_VRS_Get/Set/Pop as undefined @GCC_3.5 imports — they should be resolved locally. Ensure the build still compiles for all NativeAOT unix targets (x64, arm64, arm32) and that the non-NativeAOT (regular llvm-libunwind) build is unaffected by the guard changes.

References

The following is the prior conversation context from the user's chat exploration (may be truncated):

User: after this PR merged, I'm seeing SIGSEGV crash...

This pull request was created from Copilot chat.

@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.

Split the single #if !defined(_LIBUNWIND_NATIVEAOT) guard into two blocks
so that _Unwind_VRS_Get, _Unwind_VRS_Get_Internal, _Unwind_VRS_Set,
_Unwind_VRS_Pop, and ValueAsBitPattern are compiled into the NativeAOT
build. These helpers are called by _Unwind_VRS_Interpret which is the
ARM32 EHABI managed-frame unwinding path in NativeAOT.

PR #128667 accidentally included them inside the guard, causing the
linker to resolve them against libgcc_s.so.1 instead of the in-tree
implementations that understand the NativeAOT ArmUnwindCursor shim,
resulting in SIGSEGV during ARM32 stack unwinding.

The genuinely-unused C++ exception dispatch functions (__aeabi_unwind_cpp_pr*,
unwind_phase1/2, _Unwind_RaiseException, _Unwind_ForcedUnwind, etc.) that
depend on __unw_step and other libunwind.cpp public API remain guarded.

Co-authored-by: MichalStrehovsky <13110571+MichalStrehovsky@users.noreply.github.com>
Copilot AI requested review from Copilot and removed request for Copilot June 1, 2026 04:46
… _LIBUNWIND_NATIVEAOT guard

PR #128667 accidentally excluded _Unwind_VRS_Get/Set/Pop (and their
__unw_get/set_reg/__unw_save_vfp_as_X dependencies) from the NativeAOT
build while leaving _Unwind_VRS_Interpret (their caller) in place.
The linker resolved the exported VRS symbols from libgcc_s.so.1 which
treats ArmUnwindCursor* as a real libgcc context → SIGSEGV on ARM32.

Unwind-EHABI.cpp: Split single NATIVEAOT guard into two blocks so
ValueAsBitPattern and _Unwind_VRS_Set/Get_Internal/Get/Pop sit between
the two guards (always compiled).

libunwind.cpp: Split single NATIVEAOT guard into smaller inner guards
keeping __unw_get_reg, __unw_set_reg, __unw_get_fpreg, __unw_set_fpreg,
__unw_save_vfp_as_X unconditional so they are compiled for NativeAOT.
The singleton, __unw_init_local, __unw_step, __unw_resume, DWARF/FDE
functions, and Apple dynamic sections remain behind the guard.

Co-authored-by: MichalStrehovsky <13110571+MichalStrehovsky@users.noreply.github.com>
Copilot AI requested review from Copilot and removed request for Copilot June 1, 2026 05:06
Copilot AI changed the title [WIP] Fix SIGSEGV crashes during stack unwinding on ARM32 Fix ARM32 NativeAOT SIGSEGV: restore VRS helpers and __unw_*_reg outside _LIBUNWIND_NATIVEAOT guard Jun 1, 2026
Copilot AI requested a review from MichalStrehovsky June 1, 2026 05:07
@MichalStrehovsky
Copy link
Copy Markdown
Member

/azp run runtime-nativeaot-outerloop

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 1 pipeline(s).

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants