Summary
On Linux, JNI_OnLoad in SerialPort_Posix.c calls sigaction() to set SIGUSR1
and SIGUSR2 to SIG_IGN, overwriting the JVM's registered signal handlers for
these signals without preserving the previous handler (third argument is NULL
instead of &oldAction).
This breaks the JVM's internal signal chain. JavaScriptCore (WebKit) uses SIGUSR1
to suspend threads during garbage collection: the GC thread sends SIGUSR1 via
pthread_kill to all threads, and each thread's signal handler calls sem_post to
acknowledge. With SIG_IGN, the acknowledgement never comes — the GC thread waits
indefinitely, causing the native JS GC function to never return.
Reproduction
- Add
com.fazecast:jSerialComm as a dependency to a JavaFX application that uses
the WebView component
- Open a serial port briefly
- Use the WebView long enough for JavaScript GC to trigger
- The application hangs indefinitely; a thread dump shows the JavaFX WebKit GC
thread blocked on a semaphore
Removing jSerialComm from the classpath immediately resolves the hang.
Root Cause
In SerialPort_Posix.c, JNI_OnLoad contains:
struct sigaction ignoreAction;
memset(&ignoreAction, 0, sizeof(struct sigaction));
ignoreAction.sa_handler = SIG_IGN;
sigaction(SIGHUP, &ignoreAction, NULL);
sigaction(SIGIO, &ignoreAction, NULL);
sigaction(SIGTTOU, &ignoreAction, NULL);
sigaction(SIGTTIN, &ignoreAction, NULL);
sigaction(SIGUSR1, &ignoreAction, NULL); // ← breaks JSC GC thread suspension
sigaction(SIGUSR2, &ignoreAction, NULL); // ← breaks JSC GC thread suspension
sigaction() is process-wide. Calling it from a JNI library without saving and re-chaining the previous handler (NULL as third argument) permanently discards the JVM's handlers. The JVM does not expose a safe API for native libraries to override these signals.
Note: most of the other signals (SIGHUP, SIGTTOU, SIGTTIN) are also unnecessary because serial ports opened with `O_NOCTTY`` cannot become the controlling terminal, so these signals are never sent for those file descriptors.
Fix
Remove all sigaction calls from JNI_OnLoad. Instead, use pthread_sigmask() at the start of each worker thread to block signals that should not interrupt serial port I/O. This is thread-local and does not touch the JVM's signal chain:
// At the start of eventReadingThread1 and eventReadingThread2:
sigset_t signalMask;
sigemptyset(&signalMask);
sigaddset(&signalMask, SIGHUP);
sigaddset(&signalMask, SIGIO);
sigaddset(&signalMask, SIGTTOU);
sigaddset(&signalMask, SIGTTIN);
pthread_sigmask(SIG_BLOCK, &signalMask, NULL);
Important: SIGUSR1 and SIGUSR2 are intentionally NOT blocked. These threads must remain suspendable by the JVM GC — blocking them would prevent GC from working on those threads.
References
Summary
On Linux,
JNI_OnLoadinSerialPort_Posix.ccallssigaction()to setSIGUSR1and
SIGUSR2toSIG_IGN, overwriting the JVM's registered signal handlers forthese signals without preserving the previous handler (third argument is
NULLinstead of
&oldAction).This breaks the JVM's internal signal chain. JavaScriptCore (WebKit) uses
SIGUSR1to suspend threads during garbage collection: the GC thread sends
SIGUSR1viapthread_killto all threads, and each thread's signal handler callssem_posttoacknowledge. With
SIG_IGN, the acknowledgement never comes — the GC thread waitsindefinitely, causing the native JS GC function to never return.
Reproduction
com.fazecast:jSerialCommas a dependency to a JavaFX application that usesthe
WebViewcomponentthread blocked on a semaphore
Removing jSerialComm from the classpath immediately resolves the hang.
Root Cause
In
SerialPort_Posix.c,JNI_OnLoadcontains:sigaction()is process-wide. Calling it from a JNI library without saving and re-chaining the previous handler (NULLas third argument) permanently discards the JVM's handlers. The JVM does not expose a safe API for native libraries to override these signals.Note: most of the other signals (
SIGHUP,SIGTTOU,SIGTTIN) are also unnecessary because serial ports opened with `O_NOCTTY`` cannot become the controlling terminal, so these signals are never sent for those file descriptors.Fix
Remove all
sigactioncalls fromJNI_OnLoad. Instead, usepthread_sigmask()at the start of each worker thread to block signals that should not interrupt serial port I/O. This is thread-local and does not touch the JVM's signal chain:// At the start of
eventReadingThread1andeventReadingThread2:Important:
SIGUSR1andSIGUSR2are intentionally NOT blocked. These threads must remain suspendable by the JVM GC — blocking them would prevent GC from working on those threads.References