From 5052749babf45ec20cd9cf6995c028fa894b160f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20Rodr=C3=ADguez?= Date: Tue, 11 Aug 2026 17:05:21 +0200 Subject: [PATCH] OpenJDK: Do not block signals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit JamVM blocks SIGQUIT, SIGINT and SIGPIPE in all threads. In the GNU Classpath port, a dedicated signal thread consumes SIGQUIT and SIGINT via sigwait(). SIGPIPE is blocked so that writes to broken pipes fail with EPIPE. The OpenJDK backend uses asynchronous signal handlers: JamVM itself handles SIGQUIT for thread dumps, while SIGHUP, SIGINT and SIGTERM are registered by the class library through sun.misc.Signal. Because SIGQUIT and SIGINT are blocked, their handlers are never invoked. As a result, Ctrl-C neither terminates the VM nor runs shutdown hooks, and SIGQUIT does not produce a thread dump. The signal mask also survives exec() and is inherited by processes launched through Runtime.exec() or ProcessBuilder. OpenJDK did not reset it in the posix_spawn() path until OpenJDK 20 (JDK-8234262). Make the signal-mask policy class-library specific: - GNU Classpath keeps blocking SIGQUIT, SIGINT and SIGPIPE as before. - OpenJDK instead ensures that SIGQUIT, SIGHUP, SIGINT, SIGTERM and SIGPIPE are unblocked, and installs a no-op handler for SIGPIPE so that writes to broken pipes remain nonfatal. Unlike a blocked or ignored signal, the SIGPIPE handler is reset to the default across exec() and therefore does not leak into spawned children. Signed-off-by: Guillermo Rodríguez --- src/classlib/gnuclasspath/classlib.h | 2 ++ src/classlib/gnuclasspath/thread.c | 14 +++++++++ src/classlib/openjdk/classlib.h | 1 + src/classlib/openjdk/thread.c | 45 ++++++++++++++++++++++++++-- src/thread.c | 30 +++++++------------ 5 files changed, 70 insertions(+), 22 deletions(-) diff --git a/src/classlib/gnuclasspath/classlib.h b/src/classlib/gnuclasspath/classlib.h index 1ecf9f9..e8ec4e2 100644 --- a/src/classlib/gnuclasspath/classlib.h +++ b/src/classlib/gnuclasspath/classlib.h @@ -51,6 +51,8 @@ extern Thread *classlibJThread2Thread(Object *jThread); extern void classlibThreadName2Buff(Object *jThread, char *buffer, int buff_len); +extern void classlibInitialiseSignalMask(); + #define classlibInitialiseSignals() \ /* NOTHING TO DO */ TRUE diff --git a/src/classlib/gnuclasspath/thread.c b/src/classlib/gnuclasspath/thread.c index 6136e6d..b6abc73 100644 --- a/src/classlib/gnuclasspath/thread.c +++ b/src/classlib/gnuclasspath/thread.c @@ -139,6 +139,20 @@ void classlibThreadName2Buff(Object *jThread, char *buffer, int buff_len) { String2Buff(name, buffer, buff_len); } +/* SIGQUIT and SIGINT are received synchronously via sigwait(), + so they must be blocked in all threads. SIGPIPE is blocked + so writes to broken pipes fail with EPIPE. Process-spawning + code must clear the inherited mask before exec. */ +void classlibInitialiseSignalMask() { + sigset_t mask; + + sigemptyset(&mask); + sigaddset(&mask, SIGQUIT); + sigaddset(&mask, SIGINT); + sigaddset(&mask, SIGPIPE); + sigprocmask(SIG_BLOCK, &mask, NULL); +} + void classlibSignalThread(Thread *self) { sigset_t mask; int sig; diff --git a/src/classlib/openjdk/classlib.h b/src/classlib/openjdk/classlib.h index 015dfb2..43a2e57 100644 --- a/src/classlib/openjdk/classlib.h +++ b/src/classlib/openjdk/classlib.h @@ -44,6 +44,7 @@ extern void classlibSetThreadState(Thread *thread, int state); extern void classlibThreadName2Buff(Object *jThread, char *buffer, int buff_len); +extern void classlibInitialiseSignalMask(); extern int classlibInitialiseSignals(); extern void classlibSignalThread(Thread *self); diff --git a/src/classlib/openjdk/thread.c b/src/classlib/openjdk/thread.c index 71fc8aa..21ea6a4 100644 --- a/src/classlib/openjdk/thread.c +++ b/src/classlib/openjdk/thread.c @@ -205,15 +205,43 @@ void classlibSignalThread(Thread *self) { } } +/* Signals are received via asynchronous handlers. Make sure all + signals the VM uses are unblocked, so they can be delivered to + the registered handlers. SIGQUIT and SIGPIPE are handled by + JamVM itself; SIGHUP, SIGINT and SIGTERM can be handled by the + class library via sun.misc.Signal, and are the only signals it + can register (see JVM_FindSignal) */ +void classlibInitialiseSignalMask() { + sigset_t mask; + + sigemptyset(&mask); + sigaddset(&mask, SIGQUIT); + sigaddset(&mask, SIGINT); + sigaddset(&mask, SIGHUP); + sigaddset(&mask, SIGTERM); + sigaddset(&mask, SIGPIPE); + pthread_sigmask(SIG_UNBLOCK, &mask, NULL); +} + +/* Writes to broken pipes must fail with EPIPE rather than kill + the VM. Unlike blocking or SIG_IGN, a no-op handler is reset + to default on exec, so it doesn't leak into spawned children. */ +static void pipeHandler(int sig) { +} + int classlibInitialiseSignals() { struct sigaction act; Class *signal_class; - act.sa_handler = signalHandler; + act.sa_handler = pipeHandler; sigemptyset(&act.sa_mask); act.sa_flags = SA_RESTART; - sigaction(SIGQUIT, &act, NULL); + sigaction(SIGPIPE, &act, NULL); + /* + * Initialise everything used by signalHandler before installing + * it, as these signals may already be unblocked. + */ sem_init(&signal_sem, 0, 0); signal_class = findSystemClass(SYMBOL(sun_misc_Signal)); @@ -222,6 +250,17 @@ int classlibInitialiseSignals() { signal_dispatch_mb = findMethod(signal_class, SYMBOL(dispatch), SYMBOL(_I__V)); + if(signal_dispatch_mb == NULL) + return FALSE; - return signal_dispatch_mb != NULL; + act.sa_handler = signalHandler; + sigemptyset(&act.sa_mask); + act.sa_flags = SA_RESTART; + + sigaction(SIGQUIT, &act, NULL); + sigaction(SIGHUP, &act, NULL); + sigaction(SIGINT, &act, NULL); + sigaction(SIGTERM, &act, NULL); + + return TRUE; } diff --git a/src/thread.c b/src/thread.c index e4fb7d1..8ca1fbc 100644 --- a/src/thread.c +++ b/src/thread.c @@ -761,8 +761,6 @@ void createJavaThread(Object *jThread, long long stack_size) { enableSuspend(self); } -static void initialiseSignalMask(); - Thread *attachJNIThread(char *name, char is_daemon, Object *group) { Thread *thread = sysMalloc(sizeof(Thread)); void *stack_base = nativeStackBase(); @@ -775,7 +773,7 @@ Thread *attachJNIThread(char *name, char is_daemon, Object *group) { memset(thread, 0, sizeof(Thread)); /* Externally created threads will not inherit signal state */ - initialiseSignalMask(); + classlibInitialiseSignalMask(); /* Initialise the thread and add it to the VM thread list */ return attachThread(name, is_daemon, stack_base, thread, group); @@ -1134,24 +1132,9 @@ void printThreadsDump(Thread *self) { resumeAllThreads(self); } -static void initialiseSignalMask() { - sigset_t mask; - - sigemptyset(&mask); - sigaddset(&mask, SIGQUIT); - sigaddset(&mask, SIGINT); - sigaddset(&mask, SIGPIPE); - sigprocmask(SIG_BLOCK, &mask, NULL); -} - static int initialiseSignals() { struct sigaction act; - /* Initialise signal mask. Signal masks are per-thread, - but as this is the main thread it will be inherited - by all threads created wtihin Java */ - initialiseSignalMask(); - /* Setup signal handler for thread suspension. Signal handlers are process-wide */ @@ -1161,7 +1144,16 @@ static int initialiseSignals() { sigaction(SIGUSR1, &act, NULL); /* Do classlib specific initialisation */ - return classlibInitialiseSignals(); + if(!classlibInitialiseSignals()) + return FALSE; + + /* Initialise signal mask, after the handlers have been + installed. Signal masks are per-thread, but as this is + the main thread it will be inherited by all threads + created wtihin Java */ + classlibInitialiseSignalMask(); + + return TRUE; } /* garbage collection support */