Skip to content

JNI_OnLoad overrides JVM SIGUSR1/SIGUSR2 signal handlers process-wide, causing indefinite GC hang with JavaFX WebKit #631

@rfcom

Description

@rfcom

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

  1. Add com.fazecast:jSerialComm as a dependency to a JavaFX application that uses
    the WebView component
  2. Open a serial port briefly
  3. Use the WebView long enough for JavaScript GC to trigger
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions