You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
0.5.7: SIGABRT from node-pty exit-waiter ThreadSafeFunction teardown (quit-time throw and uv_cond_wait abort) #806
Munder Difflin 0.5.7 (macOS, Electron 32.3.3, bundled node-pty 1.1.0) aborts with SIGABRT in two different ways. Both involve node-pty's per-pty exit-waiter threads (SetupExitCallback, one std::thread per pty, each holding a Napi::ThreadSafeFunction into the JS onExit callback). We saw 4 crash reports in one day (macOS 26.6.2, Apple silicon) with 6 to 7 live pty sessions in each.
Type A: exception thrown from a ThreadSafeFunction callback while quitting (1 of 4)
Faulting thread is the main thread, during app.quit(). A C++ exception escapes node-pty's native callback and hits std::terminate:
abort
__abort_message / std::terminate / __cxa_throw
pty.node: Napi::Error::ThrowAsJavaScriptException()
pty.node: Napi::ThreadSafeFunction::CallJS(...)
Electron Framework: uv_async_send / uv_run
Electron Framework: node::FreeEnvironment
This is the same stack shape as microsoft/node-pty#904, which is open and unfixed. The app quit and the abort landed in the same second, and the app was relaunched 5 s later. I compared SetupExitCallback in node-pty v1.1.0 and v1.2.0-beta.17: the thread/TSFN section is unchanged, so upgrading does not help. Related: nodejs/node#49171 (TSFN finalization during environment teardown), closed as not planned.
Type B: worker thread aborts inside uv_cond_wait (3 of 4)
Faulting thread is a non-main thread started straight from _pthread_start:
abort
Electron Framework: uv_cond_wait+28
Electron Framework: uv_cancel+680
libsystem_pthread: _pthread_start+136
Frame names inside the stripped Electron Framework are nearest-exported-symbol guesses, so the real function may differ. libuv calls abort() when pthread_cond_wait returns an error, which points at a condition variable or mutex that was destroyed or invalid under a waiting thread. node-pty does not use uv_cond_t itself. The only candidate is the cond/mutex that backs a TSFN inside Node/Electron, which makes this plausibly the same teardown-lifetime defect as Type A seen from the other thread. That is a hypothesis; we have no symbols for Electron Framework 32.3.3 to confirm it.
Observations:
Uptime at crash: 10, 14 and 29 minutes.
In 1 of 3, several agent sessions had been closed (pty teardown) within the previous 3 minutes; in the other 2 nothing was spawned or closed in the 7 minutes before. So teardown looks like an accelerator at most, not a proven trigger.
Every dump shows 6 to 7 SetupExitCallback threads.
kill -9 restarts leave no crash report, so there may be more.
Version
Munder Difflin 0.5.7, Electron 32.3.3, node-pty 1.1.0, macOS 26.6.2 arm64.
Suggested mitigations
Before app.quit(), close every pty and wait for its exit event, so no exit-waiter thread is still holding a TSFN when the environment is freed.
Wrap the JS onExit / onData handlers passed to node-pty so they can never throw, which removes the Type A path.
Ship a build with Electron symbols (or a symbolicated .ips workflow) so Type B can be confirmed or ruled out.
Write app stderr to a rotated log rather than overwriting it on each start, so the last lines before a crash survive.
Update: the Type B abort is libuv#4165, not a node-pty teardown defect
Correction to the "Type B" section above, which guessed at a condition variable destroyed under a waiter. Further analysis found the actual cause.
Cause: libuv/libuv#4165 ("darwin: uv_cond_wait abort due to apple bug", closed). On macOS, pthread_cond_wait can return EINVAL with errno == EBUSY spuriously. libuv 1.46.0 treats any non-zero return as fatal and calls abort(). libuv 1.47.0 and later ignore exactly that case in uv_cond_wait.
Why this build is affected: Electron 32.3.3 bundles Node 20.18.1 and libuv 1.46.0 (version string 1.46.0 in the shipped Electron Framework). Disassembling uv_cond_wait in the shipped binary shows a bare pthread_cond_wait / cbnz / bl _abort, with no EBUSY handling. Node 20.x never received the workaround, and neither did Electron 33 or 34 (Node 20.18.x). Node 22 does (Electron 35 = Node 22.14.0, libuv 1.49.2).
Evidence (two of the four crash dumps symbolicated with the official Electron 32.3.3 dSYM; the dSYM, the installed binary and the dumps share the same arm64 UUID; return addresses taken at address - 4):
Main thread in both: __psynch_mutexwait <- uv_mutex_lock <- uv__work_submit (threadpool.c:274) <- uv_fs_write (fs.c:2187) <- node::fs::WriteBuffer (node_file.cc:2275) <- JS. This is frame-for-frame the pair of stacks in libuv#4165 (uv_mutex_lock <- uv__work_submit <- uv_fs_write on the main thread, uv_cond_wait abort on a pool thread).
Correction to the earlier symbolication: a label of uv_cond_timedwait (thread.c:798) is an off-by-one on arm64 return addresses; the real site is uv_cond_wait.
Not established: a kernel log line ("same sequence already present") at the crash times. The log store retains no kernel lines for those windows, so this is neither confirmed nor ruled out. The dumps are on macOS 26.6.2, while the libuv workaround comment names OS X up to 13.6, so the same kernel behaviour on 26.x is inferred from the matching stacks.
What the main thread was doing: in the dumps it was queueing an async fs.write onto the threadpool from node-pty's write queue. node-pty 1.1.0 (lib/unixTerminal.js, around lines 314 to 319) retries fs.write on EAGAIN via setImmediate(() => _processWriteQueue()) with no delay, counter or reader check. If a pty's slave side is not being read (the tty input queue fills, so the write returns EAGAIN), this becomes a tight loop of threadpool submissions. That does not cause the kernel bug, but every submission is another wake-up of the pool's condition variable, so it multiplies the chances of hitting it. We could not show that it changes the crash rate: the same spin ran for many hours on the same binary without an abort, and a repro (hundreds of millions of EAGAIN writes) did not abort either. Treat it as a risk amplifier, not the trigger.
Requests:
Move to Electron 35 or later (Node 22, libuv 1.49.2), which carries the libuv fix. No Electron 32 to 34 build has it. node-pty must be rebuilt for the new ABI.
Bound the node-pty EAGAIN retry: replace the immediate retry with a growing setTimeout (for example 1 ms doubling up to 1 s), clear it in dispose(), and give up (drop the write queue) after a time limit. This removes the hot loop even before the Electron upgrade.
Avoid writing into a pty whose reader is not consuming input (stop after N seconds of backlog).
Type A (the quit-time exception from the ThreadSafeFunction callback, matching microsoft/node-pty#904) is a separate defect and still stands as reported above.
Activity
sirsid2001 commentedon Oct 30, 2026
Update: the Type B abort is libuv#4165, not a node-pty teardown defect
Correction to the "Type B" section above, which guessed at a condition variable destroyed under a waiter. Further analysis found the actual cause.
Cause: libuv/libuv#4165 ("darwin: uv_cond_wait abort due to apple bug", closed). On macOS,
pthread_cond_waitcan returnEINVALwitherrno == EBUSYspuriously. libuv 1.46.0 treats any non-zero return as fatal and callsabort(). libuv 1.47.0 and later ignore exactly that case inuv_cond_wait.Why this build is affected: Electron 32.3.3 bundles Node 20.18.1 and libuv 1.46.0 (version string
1.46.0in the shipped Electron Framework). Disassemblinguv_cond_waitin the shipped binary shows a barepthread_cond_wait/cbnz/bl _abort, with noEBUSYhandling. Node 20.x never received the workaround, and neither did Electron 33 or 34 (Node 20.18.x). Node 22 does (Electron 35 = Node 22.14.0, libuv 1.49.2).Evidence (two of the four crash dumps symbolicated with the official Electron 32.3.3 dSYM; the dSYM, the installed binary and the dumps share the same arm64 UUID; return addresses taken at address - 4):
abort<-uv_cond_wait(thread.c:794) <- threadpoolworker(threadpool.c:76).__psynch_mutexwait<-uv_mutex_lock<-uv__work_submit(threadpool.c:274) <-uv_fs_write(fs.c:2187) <-node::fs::WriteBuffer(node_file.cc:2275) <- JS. This is frame-for-frame the pair of stacks in libuv#4165 (uv_mutex_lock<-uv__work_submit<-uv_fs_writeon the main thread,uv_cond_waitabort on a pool thread).uv_cond_timedwait(thread.c:798) is an off-by-one on arm64 return addresses; the real site isuv_cond_wait.What the main thread was doing: in the dumps it was queueing an async
fs.writeonto the threadpool from node-pty's write queue. node-pty 1.1.0 (lib/unixTerminal.js, around lines 314 to 319) retriesfs.writeonEAGAINviasetImmediate(() => _processWriteQueue())with no delay, counter or reader check. If a pty's slave side is not being read (the tty input queue fills, so the write returnsEAGAIN), this becomes a tight loop of threadpool submissions. That does not cause the kernel bug, but every submission is another wake-up of the pool's condition variable, so it multiplies the chances of hitting it. We could not show that it changes the crash rate: the same spin ran for many hours on the same binary without an abort, and a repro (hundreds of millions ofEAGAINwrites) did not abort either. Treat it as a risk amplifier, not the trigger.Requests:
EAGAINretry: replace the immediate retry with a growingsetTimeout(for example 1 ms doubling up to 1 s), clear it indispose(), and give up (drop the write queue) after a time limit. This removes the hot loop even before the Electron upgrade.Type A (the quit-time exception from the
ThreadSafeFunctioncallback, matching microsoft/node-pty#904) is a separate defect and still stands as reported above.