Hi Jon,
Thanks for the review.
This is a follow-up patch to: 
https://cygwin.com/pipermail/cygwin-patches/2026q3/015206.html
Responses inline below; v3 addresses all of the review points.
For testing, we built a standalone ucontext/altstack test suite and ran it 
against a freshly built cygwin1.dll on Windows ARM64. The tests cover 
getcontext/setcontext/swapcontext/makecontext round-trips, odd argument counts 
(9–13), NULL uc_link exit behaviour, a 5000-iteration swapcontext loop, and an 
SA_ONSTACK signal handler.
We also ran a four-way isolation matrix where each branch reverted exactly one 
of the three fixes to verify that each change is independently required. 
Reverting the setcontext hand-restore causes every makecontext/swapcontext test 
to crash with STATUS_STACK_BUFFER_OVERRUN, matching the pre-port baseline. 
Reverting the trampoline (including the sp,x19 fix and mov w0,#0xff) causes the 
odd-argc tests to fail with STATUS_DATATYPE_MISALIGNMENT, and the NULL uc_link 
test exits with 0 instead of 0xff.
I agree there's nothing in the upstream testsuite covering this yet. I'm happy 
to contribute these tests as a follow-up if that would be useful.
Regarding the clobber list in call_signal_handler, the previous comment was 
misleading. The clobber list is serving two distinct purposes, and I've 
rewritten the comment to explain that. x0-x7, x9, x10, and x29 are explicitly 
used by the assembly as scratch/argument registers and are listed as clobbers 
to prevent GCC from allocating "r" input operands into registers that the 
assembly overwrites before reading. The x86_64 version doesn't need this 
because its inputs are "o" memory operands. x8, x11-x17, x30 (clobbered by 
blr), and v0-v7/v16-v31 are the normal caller-saved registers clobbered by the 
C call to altstack_wrapper. x18 (TEB) and v8-v15 (callee-saved on Windows) are 
preserved and therefore omitted.
 So the registers you pointed out aren't redundant with the explicit 
save/restore sequence. Removing them would allow GCC to place an input operand 
into a register that the assembly overwrites before reading it. That said, I'm 
also happy to convert this path to "o" memory operands like the x86_64 
implementation if you think that's the cleaner approach. I kept "r" operands 
simply to minimise churn in the already-tested assembly.
 For RtlRestoreContext, unfortunately I haven't found any Microsoft 
documentation describing this behaviour. The STATUS_ILLEGAL_INSTRUCTION failure 
on a synthetic context was discovered empirically. I also tried setting 
ContextFlags = CONTEXT_FULL on the makecontext-generated context in case 
RtlRestoreContext required a particular flag mask, but it made no difference. 
The hand-restore is genuinely required. I've added a comment noting this.
 For the 272 constant, I've added a comment explaining that it's the byte 
offset of the V-register array within struct __mcontext, whose layout mirrors 
the Windows ARM64 CONTEXT structure. The comment now also documents all offsets 
used by the routine.
 Regarding x17 and CPSR not being restored in setcontext, this is the same 
asymmetry as in the earlier incyg fix, but you're right that it should be 
documented explicitly rather than left implicit. The omission is intentional. 
x16 (IP0) and x17 (IP1) are the ABI-defined intra-procedure-call scratch 
registers. This implementation uses x16 as the base register and x17 as the 
branch target, and the ABI already permits linker-inserted veneers to clobber 
both registers, so a ucontext consumer cannot rely on either surviving a 
setcontext() regardless. Their saved values are therefore intentionally not 
restored. CPSR is likewise not restored because there is no EL0 instruction 
that restores the entire register, and the only architecturally visible state 
that user code could reasonably care about (NZCV) is caller-clobbered across 
the makecontext()/swapcontext() boundary that this code serves. glibc and musl 
take the same approach on AArch64. I've added comments explaining this.
 Finally, I've fixed both the "return address on the stack" comment and the 
missing makecontext #error cases. The header comment now states that the 
continuation trampoline is reached via a stack return slot on x86_64 but via lr 
on AArch64. I also added the missing #else / #error unimplemented for this 
target to each of the four #if defined(x86_64) / #elif defined(aarch64) blocks 
in makecontext.
Inline patch
---
 winsup/cygwin/exceptions.cc | 345 +++++++++++++++++++++++++++++++-----
 1 file changed, 304 insertions(+), 41 deletions(-)

diff --git a/winsup/cygwin/exceptions.cc b/winsup/cygwin/exceptions.cc
index 1e129b319..4224cdfcf 100644
--- a/winsup/cygwin/exceptions.cc
+++ b/winsup/cygwin/exceptions.cc
@@ -1892,7 +1892,7 @@ _cygtls::call_signal_handler ()

     /* In assembler: Save regs on new stack, move to alternate stack,
        call thisfunc, revert stack regs. */
-#ifdef __x86_64__
+#if defined(__x86_64__)
     /* Clobbered regs: rcx, rdx, r8, r9, r10, r11, rbp, rsp */
     __asm__ ("\n\
         movq  %[NEW_SP], %%rax  # Load alt stack into rax  \n\
@@ -1930,6 +1930,66 @@ _cygtls::call_signal_handler ()
             [FUNC]  "o" (thisfunc),
             [WRAPPER] "o" (altstack_wrapper)
         : "memory");
+#elif defined(__aarch64__)
+    __asm__ ("\n\
+        mov   x9, %[NEW_SP]     // Load alt stack into x9  \n\
+        sub   x9, x9, #0x60     // Make room on alt stack  \n\
+                 // for clobbered regs      \n\
+        str   x0, [x9, #0x00]      // Save clobbered regs     \n\
+        str   x1, [x9, #0x08]                  \n\
+        str   x2, [x9, #0x10]                  \n\
+        str   x3, [x9, #0x18]                  \n\
+        str   x4, [x9, #0x20]                  \n\
+        str   x5, [x9, #0x28]                  \n\
+        str   x6, [x9, #0x30]                  \n\
+        str   x7, [x9, #0x38]                  \n\
+        str   fp, [x9, #0x40]                  \n\
+        mov   x10, sp        // Copy sp into x10 for saving   \n\
+        str   x10, [x9, #0x48]              \n\
+        str   x30, [x9, #0x50]  // Save link register      \n\
+        mov   x0, %[SIG]     // thissig to 1st arg reg  \n\
+        mov   x1, %[SI]      // &thissi to 2nd arg reg  \n\
+        mov   x2, %[CTX]     // thiscontext to 3rd arg reg \n\
+        mov   x3, %[FUNC]    // thisfunc to x3    \n\
+        mov   x4, %[WRAPPER]    // wrapper address to x4   \n\
+        mov   sp, x9         // Move alt stack into sp  \n\
+        blr   x4       // Call wrapper         \n\
+        mov   x9, sp         // Restore clobbered regs  \n\
+        ldr   x30, [x9, #0x50]  // Restore link register   \n\
+        ldr   x10, [x9, #0x48]              \n\
+        ldr   fp,  [x9, #0x40]              \n\
+        ldr   x7,  [x9, #0x38]              \n\
+        ldr   x6,  [x9, #0x30]              \n\
+        ldr   x5,  [x9, #0x28]              \n\
+        ldr   x4,  [x9, #0x20]              \n\
+        ldr   x3,  [x9, #0x18]              \n\
+        ldr   x2,  [x9, #0x10]              \n\
+        ldr   x1,  [x9, #0x08]              \n\
+        ldr   x0,  [x9, #0x00]              \n\
+        mov   sp,  x10    // Restore stack pointer   \n"
+        : : [NEW_SP]    "r" (new_sp),
+            [SIG]    "r" (thissig),
+            [SI]  "r" (&thissi),
+            [CTX]    "r" (thiscontext),
+            [FUNC]   "r" (thisfunc),
+            [WRAPPER] "r" (altstack_wrapper)
+        /* The clobber list serves two roles here.  x0-x7, x9, x10
+           and x29 are hardcoded by this asm as scratch/argument
+           registers, so they are listed to stop gcc allocating the
+           "r" inputs into them.  (The x86_64 path above needs no
+           such guard because its inputs are "o" memory operands.)
+           The remaining registers - x8, x11-x17, x30 (trampled by
+           the blr) and v0-v7/v16-v31 - are caller-saved and clobbered
+           by the ordinary C call to altstack_wrapper.  x18 (the
+           Windows TEB pointer) and v8-v15 (callee-saved on Windows)
+           survive the call and are omitted.  */
+        : "memory", "cc",
+          "x0", "x1", "x2", "x3", "x4", "x5", "x6", "x7",
+          "x8", "x9", "x10", "x11", "x12", "x13", "x14", "x15",
+          "x16", "x17", "x29", "x30",
+          "v0", "v1", "v2", "v3", "v4", "v5", "v6", "v7",
+          "v16", "v17", "v18", "v19", "v20", "v21", "v22", "v23",
+          "v24", "v25", "v26", "v27", "v28", "v29", "v30", "v31");
 #else
 #error unimplemented for this target
 #endif
@@ -2009,10 +2069,89 @@ setcontext (const ucontext_t *ucp)
 {
   PCONTEXT ctx = (PCONTEXT) &ucp->uc_mcontext;
   set_signal_mask (_my_tls.sigmask, ucp->uc_sigmask);
+#if defined(__aarch64__)
+  /* On ARM64, RtlRestoreContext raises STATUS_ILLEGAL_INSTRUCTION when asked
+     to restore a synthetic context built by makecontext rather than one
+     captured by RtlCaptureContext/GetThreadContext: it rejects an arbitrary
+     PC and a stack outside the thread's registered range.  Restore the
+     registers by hand and branch to the saved PC instead, as glibc/musl do
+     for aarch64.
+
+     The numeric offsets below are byte offsets into struct __mcontext, whose
+     layout mirrors the Windows ARM64 CONTEXT: x0/x1 @8, x2-x15 @24-120,
+     x18-x28/fp @152-232, lr @248, sp @256, pc @264, v[0..31] @272 (16 bytes
+     each), fpcr @784, fpsr @788.
+
+     x16 (IP0) and x17 (IP1) are the ABI intra-procedure-call scratch
+     registers: their values in the context are deliberately not restored.
+     We reuse x16 as the base pointer and x17 as the branch target, and the
+     ABI permits the linker to clobber x16/x17 in any call/branch veneer, so a
+     ucontext consumer may not rely on them surviving a setcontext.  CPSR is
+     likewise not restored: it cannot be written from EL0 with a plain msr,
+     the only flags of interest (NZCV) are caller-clobbered across the
+     makecontext/swapcontext boundary this path serves, and glibc/musl take
+     the same approach for aarch64.  */
+  register PCONTEXT base __asm__ ("x16") = ctx;
+  __asm__ __volatile__ ("\n\
+  add   x17, x16, #272             \n\
+  ldp   q0, q1, [x17, #0]          \n\
+  ldp   q2, q3, [x17, #32]            \n\
+  ldp   q4, q5, [x17, #64]            \n\
+  ldp   q6, q7, [x17, #96]            \n\
+  ldp   q8, q9, [x17, #128]           \n\
+  ldp   q10, q11, [x17, #160]            \n\
+  ldp   q12, q13, [x17, #192]            \n\
+  ldp   q14, q15, [x17, #224]            \n\
+  ldp   q16, q17, [x17, #256]            \n\
+  ldp   q18, q19, [x17, #288]            \n\
+  ldp   q20, q21, [x17, #320]            \n\
+  ldp   q22, q23, [x17, #352]            \n\
+  ldp   q24, q25, [x17, #384]            \n\
+  ldp   q26, q27, [x17, #416]            \n\
+  ldp   q28, q29, [x17, #448]            \n\
+  ldp   q30, q31, [x17, #480]            \n\
+  /* Restore FPCR and FPSR */            \n\
+  ldr   w17, [x16, #784]           \n\
+  msr   fpcr, x17               \n\
+  ldr   w17, [x16, #788]           \n\
+  msr   fpsr, x17               \n\
+  /* Load PC into x17 (branch target, offset 264) */ \n\
+  ldr   x17, [x16, #264]           \n\
+  /* Restore callee-saved GPRs x18..x28, fp, lr */   \n\
+  ldp   x18, x19, [x16, #152]            \n\
+  ldp   x20, x21, [x16, #168]            \n\
+  ldp   x22, x23, [x16, #184]            \n\
+  ldp   x24, x25, [x16, #200]            \n\
+  ldp   x26, x27, [x16, #216]            \n\
+  ldp   x28, x29, [x16, #232]            \n\
+  ldr   x30, [x16, #248]           \n\
+  /* Restore caller-saved GPRs x2..x15 */         \n\
+  ldp   x2, x3, [x16, #24]            \n\
+  ldp   x4, x5, [x16, #40]            \n\
+  ldp   x6, x7, [x16, #56]            \n\
+  ldp   x8, x9, [x16, #72]            \n\
+  ldp   x10, x11, [x16, #88]          \n\
+  ldp   x12, x13, [x16, #104]            \n\
+  ldp   x14, x15, [x16, #120]            \n\
+  /* Restore x0, x1 */             \n\
+  ldp   x0, x1, [x16, #8]          \n\
+  /* Set SP from context (last use of x16 as base) */   \n\
+  ldr   x16, [x16, #256]           \n\
+  mov   sp, x16                 \n\
+  /* Branch to saved PC */            \n\
+  br x17                  \n\
+"
+  : /* no outputs (noreturn) */
+  : "r" (base)
+  : "memory"
+  );
+  __builtin_unreachable ();
+#else
   RtlRestoreContext (ctx, NULL);
   /* If we got here, something was wrong. */
   set_errno (EINVAL);
   return -1;
+#endif
 }

 extern "C" int
@@ -2049,7 +2188,7 @@ swapcontext (ucontext_t *oucp, const ucontext_t *ucp)
 /* Trampoline function to set the context to uc_link.  The pointer to the
    address of uc_link is stored in a callee-saved register, referenced by
    _MC_uclinkReg from the C code.  If uc_link is NULL, call exit. */
-#ifdef __x86_64__
+#if defined(__x86_64__)
 /* _MC_uclinkReg == %rbx */
 __asm__ ("          \n\
   .global  __cont_link_context  \n\
@@ -2070,19 +2209,50 @@ __cont_link_context:       \n\
   nop            \n\
   .seh_endproc         \n\
   ");
-
+#elif defined(__aarch64__)
+/*   _MC_uclinkReg == x19.  x19 holds the address of the uc_link slot but is
+  only 8-byte aligned, so read through it and mask into SP in one step
+  rather than moving the unaligned value into SP first.  setcontext and
+  cygwin_exit are noreturn, so tail-call them with 'b': this leaves x30
+  untouched and keeps the frame leaf, matching the empty SEH prologue. */
+__asm__ ("             \n\
+  .global  __cont_link_context     \n\
+  .seh_proc __cont_link_context    \n\
+__cont_link_context:            \n\
+  .seh_endprologue        \n\
+  ldr   x0, [x19]         \n\
+  and   sp, x19, #~0xf       \n\
+  cbnz  x0, 1f            \n\
+  mov   w0, #0xff         \n\
+  b  cygwin_exit       \n\
+1:                  \n\
+  b  setcontext        \n\
+  .seh_endproc            \n"
+  );
 #else
 #error unimplemented for this target
 #endif

 /* makecontext is modelled after GLibc's makecontext.  The stack from uc_stack
    is prepared so that it starts with a pointer to the linked context uc_link,
-   followed by the arguments to func, and finally at the bottom the "return"
-   address set to __cont_link_context.  In the ucp context, rbx/ebx is set to
-   point to the stack address where the pointer to uc_link is stored.  The
-   requirement to make this work is that rbx/ebx are callee-saved registers
-   per the ABI.  If any function is called which doesn't follow the ABI
-   conventions, e.g. assembler code, this method will break.  But that's ok. */
+   followed by the arguments to func.
+
+   The trampoline __cont_link_context is reached differently per target: on
+   x86_64 its address is written at the bottom of the stack as the "return"
+   address, whereas on aarch64 it is placed in lr (see below), since the
+   AArch64 ABI returns through the link register rather than the stack.
+
+   x86_64: In the ucp context, rbx is set to point to the stack address where
+   the pointer to uc_link is stored.  The requirement to make this work is that
+   rbx is a callee-saved register per the ABI.
+
+   ARM64: In the ucp context, x19 is set to point to the stack address where
+   the pointer to uc_link is stored.  The requirement is that x19 is a
+   callee-saved register per the ARM64 ABI.
+
+   If any function is called which doesn't follow the ABI conventions, e.g.
+   assembler code, this method will break.  But that's ok. */
+
 extern "C" void
 makecontext (ucontext_t *ucp, void (*func) (void), int argc, ...)
 {
@@ -2090,65 +2260,158 @@ makecontext (ucontext_t *ucp, void (*func) (void), int 
argc, ...)
   uintptr_t *sp;
   va_list ap;

+#if defined(__x86_64__)
+  /* x86_64: Arguments beyond the first 4 go on the stack.
+     However, we allocate shadow space for all args including register args. */
+  int stack_args = argc;
+
+#elif defined(__aarch64__)
+  /* ARM64: Arguments beyond the first 8 go on the stack.
+     We only allocate stack space for args beyond registers. */
+  int stack_args = (argc > 8) ? (argc - 8) : 0;
+
+#else
+#error unimplemented for this target
+#endif
+
   /* Initialize sp to the top of the stack. */
   sp = (uintptr_t *) ((uintptr_t) ucp->uc_stack.ss_sp + ucp->uc_stack.ss_size);
-  /* Subtract slots required for arguments and the pointer to uc_link. */
-  sp -= (argc + 1);
-  /* Align. */
-  sp = (uintptr_t *) ((uintptr_t) sp & ~0xf);
-  /* Subtract one slot for setting the return address. */
+
+#if defined(__x86_64__)
+  /* x86_64: Subtract slots for all arguments + uc_link pointer
+     and return address.  */
+  sp -= (stack_args + 1);  /* argc + 1 for uc_link */
+  /* Align to 16 bytes. */
+  sp = (uintptr_t *) ((uintptr_t) sp & ~0xfUL);
+  /* Subtract one more slot for the return address. */
   --sp;
-  /* Set return address to the trampolin function __cont_link_context. */
+  /* Set return address to the trampoline function __cont_link_context. */
   sp[0] = (uintptr_t) __cont_link_context;
-  /* Fetch arguments and store them on the stack.

-     x86_64:
+#elif defined(__aarch64__)
+  /* ARM64: Subtract slots for stack arguments + uc_link pointer. */
+  sp -= (stack_args + 1);  /* stack_args + 1 for uc_link */
+  /* ARM64 requires 16-byte alignment at public interfaces. */
+  sp = (uintptr_t *) ((uintptr_t) sp & ~0xfUL);

-     - Store first four args in the AMD64 ABI arg registers.
+#else
+#error unimplemented for this target
+#endif

+  /* Fetch arguments and store them.
+     x86_64:
+     - Store first four args in the AMD64 ABI arg registers (rcx, rdx, r8, r9).
      - Note that the stack is not short by these four register args.  The
        reason is the shadow space for these regs required by the AMD64 ABI.
-
      - The definition of makecontext only allows for "int" sized arguments to
        func, 32 bit, likely for historical reasons.  However, the argument
        slots on x86_64 are 64 bit anyway, so we can fetch and store the args
        as 64 bit values, and func can request 64 bit args without violating
        the definition.  This potentially allows porting 32 bit applications
-       providing pointer values to func without additional porting effort. */
+       providing pointer values to func without additional porting effort.
+
+     ARM64:
+     - Store first eight args in ARM64 ABI arg registers (x0-x7).
+     - Arguments beyond 8 go on the stack.
+     - Similar to x86_64, we store as uintptr_t for pointer compatibility. */
+
   va_start (ap, argc);
   for (int i = 0; i < argc; ++i)
-#ifdef __x86_64__
-    switch (i)
-      {
-      case 0:
-  ucp->uc_mcontext.rcx = va_arg (ap, uintptr_t);
-  break;
-      case 1:
-  ucp->uc_mcontext.rdx = va_arg (ap, uintptr_t);
-  break;
-      case 2:
-  ucp->uc_mcontext.r8 = va_arg (ap, uintptr_t);
-  break;
-      case 3:
-  ucp->uc_mcontext.r9 = va_arg (ap, uintptr_t);
-  break;
-      default:
-  sp[i + 1] = va_arg (ap, uintptr_t);
-  break;
-      }
+    {
+#if defined(__x86_64__)
+      switch (i)
+        {
+        case 0:
+          ucp->uc_mcontext.rcx = va_arg (ap, uintptr_t);
+          break;
+        case 1:
+          ucp->uc_mcontext.rdx = va_arg (ap, uintptr_t);
+          break;
+        case 2:
+          ucp->uc_mcontext.r8 = va_arg (ap, uintptr_t);
+          break;
+        case 3:
+          ucp->uc_mcontext.r9 = va_arg (ap, uintptr_t);
+          break;
+        default:
+          /* Stack arguments start at sp[i + 1] because sp[0] is return 
address */
+          sp[i + 1] = va_arg (ap, uintptr_t);
+          break;
+        }
+
+#elif defined(__aarch64__)
+      switch (i)
+        {
+        case 0:
+          ucp->uc_mcontext.x0 = va_arg (ap, uintptr_t);
+          break;
+        case 1:
+          ucp->uc_mcontext.x1 = va_arg (ap, uintptr_t);
+          break;
+        case 2:
+          ucp->uc_mcontext.x2 = va_arg (ap, uintptr_t);
+          break;
+        case 3:
+          ucp->uc_mcontext.x3 = va_arg (ap, uintptr_t);
+          break;
+        case 4:
+          ucp->uc_mcontext.x4 = va_arg (ap, uintptr_t);
+          break;
+        case 5:
+          ucp->uc_mcontext.x5 = va_arg (ap, uintptr_t);
+          break;
+        case 6:
+          ucp->uc_mcontext.x6 = va_arg (ap, uintptr_t);
+          break;
+        case 7:
+          ucp->uc_mcontext.x7 = va_arg (ap, uintptr_t);
+          break;
+        default:
+          /* Stack arguments beyond the first 8 registers. */
+          sp[i - 8] = va_arg (ap, uintptr_t);
+          break;
+        }
 #else
 #error unimplemented for this target
 #endif
+    }
   va_end (ap);
-  /* Store pointer to uc_link at the top of the stack. */
+
+#if defined(__x86_64__)
+  /* Store pointer to uc_link at sp[argc + 1], after return address
+     and args.  */
   sp[argc + 1] = (uintptr_t) ucp->uc_link;
+
+#elif defined(__aarch64__)
+  /* Store pointer to uc_link at the top of our allocated area. */
+  sp[stack_args] = (uintptr_t) ucp->uc_link;
+
+#else
+#error unimplemented for this target
+#endif
+
   /* Last but not least set the register in the context at ucp so that a
      subsequent setcontext or swapcontext picks up the right values:
      - Set instruction pointer to the target function.
      - Set stack pointer to the just computed stack pointer value.
      - Set Cygwin-specific uclink register to the address of the pointer
-       to uc_link. */
+       to uc_link.
+
+     x86_64: uclink register is rbx (callee-saved)
+     ARM64:  uclink register is x19 (callee-saved) */
+
   ucp->uc_mcontext._MC_instPtr = (uint64_t) func;
   ucp->uc_mcontext._MC_stackPtr = (uint64_t) sp;
+
+#if defined(__x86_64__)
   ucp->uc_mcontext._MC_uclinkReg = (uint64_t) (sp + argc + 1);
+
+#elif defined(__aarch64__)
+  /* Set LR to __cont_link_context for ARM64 (used as return address). */
+  ucp->uc_mcontext.lr = (uint64_t) __cont_link_context;
+  ucp->uc_mcontext._MC_uclinkReg = (uint64_t) (sp + stack_args);
+
+#else
+#error unimplemented for this target
+#endif
 }
--
2.50.1.windows.1





Attachment: Cygwin-Add-AArch64-support-for-signal-handling-and-u.patch
Description: Cygwin-Add-AArch64-support-for-signal-handling-and-u.patch

Reply via email to