AArch64-Win64: Fix SEH local unwind for Exit/Break/Continue in try/finally

Fixes #40203

Summary

This patch fixes Structured Exception Handling (SEH) for local unwind operations on Windows ARM64. When Exit/Break/Continue is used inside a try/finally block, the finally block now executes correctly before control transfers to the target.

Root Cause

On AArch64-Win64, RtlUnwindEx validates that TargetFrame >= EstablisherFrame. The EstablisherFrame is computed by Windows as SP-at-function-entry. Due to the standard ARM64 prolog stp fp,lr,[sp,#-16]!, we have:

  • FP = SP_at_entry - 16
  • Therefore SP_at_entry = FP + 16

Passing just FP as TargetFrame fails validation with STATUS_INVALID_UNWIND_TARGET (0xC0000028).

Additionally, ARM64 uses SP-relative addressing for temps (positive offsets from SP), unlike x64 which uses FP-relative (negative offsets from RBP). The $fin$ handler has its own SP but receives the parent's FP, so it cannot access parent's temps without address conversion.

Changes

compiler/aarch64/cgcpu.pas

  1. Implement g_local_unwind: Generates call to _fpc_local_unwind(FP+16, target_label) for Exit/Break/Continue in try/finally blocks. Passes FP+16 to satisfy RtlUnwindEx's TargetFrame validation.

  2. Store total_stackframe_size: Saves the offset from FP to SP after prolog completes, needed for temp address conversion in fin handlers.

  3. Skip mov sp,fp in exceptfilter epilog: The prolog already correctly skips mov fp, sp for exceptfilters (since the exceptfilter receives the parent's FP). However, commit 959804798c added mov sp, fp to the epilog for Windows without a corresponding exceptfilter check. Since exceptfilter's FP points to the parent's frame (not its own local frame), executing mov sp, fp corrupts the stack. This causes memory corruption when -O2 optimization is used, as callee-saved registers (x19-x28) trigger the regsstored code path which includes this instruction. Fix: Add potype_exceptfilter checks at all three locations in g_proc_exit where mov sp, fp is generated.

compiler/ncgbas.pas

Convert SP-relative temps to FP-relative for exceptfilter: The $fin$ handler receives the parent's FP but has its own SP. Temps allocated as [SP + X] in the parent are converted to [FP + (X - total_stackframe_size)] so the handler can access them correctly.

compiler/procinfo.pas

Add total_stackframe_size field: Stores the offset from FP to SP after prolog, used by ncgbas for temp address conversion.

compiler/ogcoff.pas

Fix ADRP relocation calculation: The addend must be added to the target address BEFORE computing the page difference, not after. This fixes incorrect address calculations for page-relative relocations.

compiler/aarch64/agcpugas.pas

Document xdata header format and set epilog start index: Adds comments clarifying the ARM64 xdata header bit layout and sets epilog start index to 1.

rtl/win/sysosh.inc

Use SYSTEM_USE_WIN_SEH: Changes from FPC_USE_WIN64_SEH to SYSTEM_USE_WIN_SEH for the _fpc_local_unwind declaration, making it available on ARM64.

rtl/win/systhrd.inc

8-byte alignment for threadvars: Ensures thread variable data is allocated at 8-byte boundaries on 64-bit platforms. ARM64 atomic operations (ldxr/stxr) require natural alignment.

Testing

Extensively tested on Windows 11 ARM64 with 44+ test cases covering:

test_seh_suite.pas (19 tests)

  • Basic exception handling with try/except/finally
  • Exit inside try/finally
  • Break inside try/finally in loop
  • Continue inside try/finally in loop
  • Nested try/finally with exit
  • Nested exceptions
  • Re-raise exception
  • Exit with result value
  • Multiple exits in try/finally
  • Deep nesting (5+ levels) with exit
  • Exit with many local variables (integers, doubles, pointers, strings, arrays)
  • Break/Continue with many locals and globals
  • Multiple exits with padding arrays
  • String manipulation in finally blocks
  • Global variable modification in finally
  • Exit inside finally block itself
  • Nested exit in finally blocks

forintest_extended.pas (25 tests)

  • For-in with Exit inside try/finally
  • For-in with Exit inside try/except
  • For-in with exception raised and caught
  • For-in with exception in try/finally (propagation)
  • Nested for-in with Exit
  • For-in with Break inside try/finally
  • For-in with Continue inside try/finally
  • For-in with multiple Exit points
  • For-in with re-raised exception
  • For-in with exception replaced
  • For-in over array with Exit
  • For-in with string characters and Exit
  • For-in with deeply nested try blocks (3 levels)
  • For-in with local variables preservation
  • For-in with Exit inside nested procedure
  • For-in with Exit and var parameters
  • For-in with multiple lists and Exit
  • For-in with complex control flow

test_minimal.pas

  • Minimal reproduction case from issue #40203
  • Verifies finally block executes on Exit

All 44+ tests pass on Windows ARM64.

Test Files

test_minimal.pas test_seh_suite.pas forintest_extended.pas

References

Edited by foxpas

Merge request reports

Loading