[PATCH] Cygwin: exceptions: Fix AArch64 non-incyg signal handling

Máte Dimand <[email protected]> Fri, 12 Jun 2026 10:02:26 +0200
Newsgroups gmane.os.cygwin.patches
Message-ID <[email protected]>
This patch fixes crashes that occur when a signal interrupts sigfe or
any non-cygwin function that does not preserve the LR register in its
prologue/epilogue. This crash was discovered through the "run-heredoc"
testcase in bash's testsuite, which caused bash to call "read"
frequently, leading to a high chance of a signal interrupting sigfe.

The "sigdelayed" function in gendef clobbers the LR register to return
to the instruction where the thread was interrupted. Picking any other
register for branching back would also clobber said register. Leaf
functions are not guaranteed to be compiled with LR being preserved on
the stack. The solution is to use RtlRestoreContext to restore all
registers without needing to sacrifice any.

The patch includes a C++ version of sigdelayed, which calls
RtlRestoreContext at the end. The non-incyg signal handling codepath
will change the thread's IP register to this new function instead of
the original sigdelayed function written in assembly. Cygwin functions
interrupted by signals still use the original function.

Signed-off-by: Máté Dimand <[email protected]>
---
  winsup/cygwin/exceptions.cc           | 87 ++++++++++++++++++++++++++-
  winsup/cygwin/local_includes/cygtls.h | 13 ++++
  2 files changed, 98 insertions(+), 2 deletions(-)

diff --git a/winsup/cygwin/exceptions.cc b/winsup/cygwin/exceptions.cc
index 1e129b319..d3fdb2a44 100644
--- a/winsup/cygwin/exceptions.cc
+++ b/winsup/cygwin/exceptions.cc
@@ -947,6 +947,56 @@ singlestep_handler (EXCEPTION_POINTERS *ep)
  }
  #endif

+#ifdef __aarch64__
+/* This function is a C version of sigdelayed, with the CPU state
+   restoration code being replaced with RtlRestoreContext to ensure that
+   LR does not get clobbered. Note that this function should not return,
+   and the stack contents created by this function are left un-popped.
+   This should not be a problem, since the context restoration also
+   restores SP. */
+void
+_cygtls::sigdelayed_non_incyg()
+{
+  int backup_errno = saved_errno;
+
+  call_signal_handler();
+
+  lock();
+
+  if(backup_errno)
+  {
+    *errno_addr = backup_errno;
+  }
+
+  /* In the asm version of sigdelayed the stack is popped to restore LR,
+     however we already have it in the stored context, so we don't need the
+     popped value itself. */
+  pop();
+
+  /* In order to stay accurate to the asm version of sigdelayed, we also
+     atomically clear the return address. */
+  InterlockedExchange64 ((LONG64*)stackptr, 0);
+
+  /* We copy the context to ensure nothing overwrites it
+     after unlocking and before restoring. */
+  CONTEXT cx = sigdelayed_context;
+
+  incyg = 0;
+  unlock();
+
+  RtlRestoreContext(&cx, NULL);
+
+  /* If we got here, something was wrong. */
+  api_fatal ("Failed to restore context in sigdelayed_non_incyg");
+}
+
+static void
+call_non_cygwin_sigdelayed(_cygtls* tls)
+{
+  tls->sigdelayed_non_incyg();
+}
+#endif
+
  bool
  _cygtls::interrupt_now (CONTEXT *cx, siginfo_t& si, void *handler,
              struct sigaction& siga)
@@ -998,10 +1048,31 @@ _cygtls::interrupt_now (CONTEXT *cx, siginfo_t& 
si, void *handler,
          return false; /* Not interrupted */
      }
  #endif
+
+#ifdef __aarch64__
+      /* Copy unmodified context to be restored by thread
+         after signals are handled. */
+      sigdelayed_context = *cx;
+#endif
+
        DWORD64 &ip = cx->_CX_instPtr;
        push (ip);
+
+#ifdef __aarch64__
+      _interrupt_setup (si, handler, siga);
+
+      /* Instead of setting IP to the asm sigdelayed pushed by 
interrupt_setup,
+         we redirect to an alternative version of it that restores the 
CPU state
+         using RtlRestoreContext. */
+      ip = reinterpret_cast<DWORD64>(&call_non_cygwin_sigdelayed);
+
+      /* X0 overwritten to pass this _cygtls in first argument to 
call_non_cygwin_sigdelayed. */
+      cx->X[0] = (DWORD64)this;
+#else
        interrupt_setup (si, handler, siga);
        ip = pop ();
+#endif
+
        SetThreadContext (*this, cx); /* Restart the thread in a new 
location */
        interrupted = true;
      }
@@ -1009,9 +1080,8 @@ _cygtls::interrupt_now (CONTEXT *cx, siginfo_t& 
si, void *handler,
  }

  void
-_cygtls::interrupt_setup (siginfo_t& si, void *handler, struct 
sigaction& siga)
+_cygtls::_interrupt_setup (siginfo_t& si, void *handler, struct 
sigaction& siga)
  {
-  push ((__tlsstack_t) sigdelayed);
    deltamask = siga.sa_mask & ~SIG_NONMASKABLE;
    sa_flags = siga.sa_flags;
    func = (void (*) (int, siginfo_t *, void *)) handler;
@@ -1037,6 +1107,13 @@ _cygtls::interrupt_setup (siginfo_t& si, void 
*handler, struct sigaction& siga)
            signal_arrived, si.si_signo);
  }

+void
+_cygtls::interrupt_setup (siginfo_t& si, void *handler, struct 
sigaction& siga)
+{
+  push ((__tlsstack_t) sigdelayed);
+  _interrupt_setup(si, handler, siga);
+}
+
  extern "C" void
  set_sig_errno (int e)
  {
@@ -1097,7 +1174,13 @@ sigpacket::setup_handler (void *handler, struct 
sigaction& siga, _cygtls *tls)
            ResumeThread (hth);
            goto out;
          }
+#ifdef __aarch64__
+        /* Since the non-incyg codepath uses a C++ version of 
sigdelayed, we have to preserve
+        the FPU registers within the thread context now. */
+        cx.ContextFlags = CONTEXT_FULL;
+#else
            cx.ContextFlags = CONTEXT_CONTROL | CONTEXT_INTEGER;
+#endif
            if (!GetThreadContext (hth, &cx))
          sigproc_printf ("couldn't get context of thread, %E");
            else
diff --git a/winsup/cygwin/local_includes/cygtls.h 
b/winsup/cygwin/local_includes/cygtls.h
index 0b5255495..1ad98c844 100644
--- a/winsup/cygwin/local_includes/cygtls.h
+++ b/winsup/cygwin/local_includes/cygtls.h
@@ -39,7 +39,13 @@ details. */
  #include "thread.h"
  #endif

+#ifdef __aarch64__
+/* This allows us to use the CONTEXT struct in _cygtls without
+   violating alignment rules. */
+#pragma pack(push,16)
+#else
  #pragma pack(push,8)
+#endif

  /* Defined here to support auto rebuild of tlsoffsets.h. */
  class tls_pathbuf
@@ -191,6 +197,9 @@ public: /* Do NOT remove this public: line, it's a 
marker for gentls_offsets. */
       aligned. The gentls_offsets script checks for that now and fails
       if the alignment is wrong. */
    ucontext_t context;
+#ifdef __aarch64__
+  CONTEXT sigdelayed_context;
+#endif
    DWORD thread_id;
    siginfo_t infodata;
    struct pthread *tid;
@@ -229,7 +238,11 @@ public: /* Do NOT remove this public: line, it's a 
marker for gentls_offsets. */
      return initialized == CYGTLS_INITIALIZED;
    }
    bool interrupt_now (CONTEXT *, siginfo_t&, void *, struct sigaction&);
+  void _interrupt_setup (siginfo_t&, void *, struct sigaction&);
    void interrupt_setup (siginfo_t&, void *, struct sigaction&);
+#ifdef __aarch64__
+  void sigdelayed_non_incyg();
+#endif

    bool inside_kernel (CONTEXT *, bool inside_cygwin = false);
    void signal_debugger (siginfo_t&);
-- 
2.51.0