Re: [PATCH] Cygwin: exceptions: Fix AArch64 non-incyg signal handling
Máte Dimand <[email protected]> Thu, 18 Jun 2026 17:05:06 +0300
| Newsgroups | gmane.os.cygwin.patches |
|---|---|
| Message-ID | <[email protected]> |
Thank you for the response! On 6/17/2026 1:07 PM, Jon Turney wrote: > It seems like it should be possible to construct a relatively simple > test to demonstrate this. Could you suggest what that test might look > like? I have attached the source code for a reproducer. It reproduces the issue by forking the process and having the child process signal the parent process while the parent process is in a leaf function (which doesn't preserve the link register). The expected behavior is both begin and end getting printed successfully and the process ending gracefully. However, without the patch, only begin gets printed, the parent process crashes inside the test function, and the child process will fail to send SIGUSR1 to the dead parent process. Unfortunately, to my knowledge, the current upstream version of AArch64 Cygwin GCC is not yet suitable for compiling it. > Hmmm... it seems like this is functionally incremental to a patch > which hasn't been applied yet, but I can't work out which one. Maybe you were thinking of this patch: https://cygwin.com/pipermail/cygwin-patches/2026q1/014641.html but as far as I can tell it has already been applied, for example this is where the X30/LR register gets set in sigdelayed: https://cygwin.com/cgit/newlib-cygwin/tree/winsup/cygwin/scripts/gendef#n558. > I spent about an hour trying to salvage this, without success. Could I > possibly trouble you to resend it as an attachment? I'm sorry for giving you trouble with the formatting of my patch yet again, I have attached it in this message.
reproducer.c
(text/plain, 1.4 KB)
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <errno.h>
static void usr1_handler(int sig) {}
void test() {
for(volatile int i = 0; i < 100000000; i++) {}
}
int main(void) {
struct sigaction sa = {0};
sa.sa_handler = usr1_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
if (sigaction(SIGUSR1, &sa, NULL) == -1) {
perror("sigaction");
return 1;
}
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return 1;
}
if (pid == 0) {
pid_t ppid = getppid();
for (size_t i = 0; i < 10000; i++) {
if (kill(ppid, SIGUSR1) == -1) {
perror("kill");
_exit(1);
}
if (usleep(10) == -1) {
perror("usleep");
_exit(1);
}
}
_exit(0);
}
printf("begin\n");
test();
printf("end\n");
int status;
pid_t r;
do {
r = waitpid(pid, &status, 0);
} while (r == -1 && errno == EINTR);
if (r == -1) {
perror("waitpid");
return 1;
}
if (WIFEXITED(status)) {
printf("child exited with status %d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("child killed by signal %d\n", WTERMSIG(status));
}
return 0;
}
0001-exceptions-Fix-AArch64-non-incyg-signal-handling.patch
(text/plain, 7 KB)
From 9824c28ccc9588fc34df936aa84b295d014e3012 Mon Sep 17 00:00:00 2001 From: matdim01 <[email protected]> Date: Wed, 27 May 2026 16:37:08 +0200 Subject: [PATCH] exceptions: Fix AArch64 non-incyg signal handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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