Repository navigation
Silent process termination after long-running child_process.spawn() on Windows (v24.13.0, works on v22.22.1) #62125
Description
Node.js v24.13.0: Silent process crash after long-running child_process operations on Windows
Summary
Node.js v24.13.0 on Windows 11 silently terminates (no error, no exit handler, no diagnostic report) after ~25 minutes of child process execution. The crash occurs consistently at a specific point in the program flow. Downgrading to Node.js v22.22.1 LTS completely resolves the issue with no code changes.
Environment
- Node.js: v24.13.0 (crashes) / v22.22.1 (works)
- OS: Windows 11 Home 10.0.22621 (x64)
- CPU: AMD Ryzen 5 5600G (AMD64 Family 25 Model 33)
- RAM: 32 GB
- Shell: Git Bash (MINGW64)
- Node version manager: nvm-windows
Reproduction scenario
The application is a CLI tool that distributes tasks to remote cloud servers via SSH:
- Phase 1: Connects to a remote server via ssh2 (npm) and uploads files via SFTP
- Phase 2: Disconnects ssh2, then spawns a system
sshchild process (child_process.spawn) to run a long-running remote command (~25 minutes) - Phase 3: Spawns another
ssh+tarchild process pair (piped) to download results - Phase 4: Cleans up remote files via another spawned
sshprocess
The crash occurs at the transition from Phase 2 to Phase 3 — after the long-running child process completes and before (or during) the next spawn() call.
Crash characteristics
The crash is not a JavaScript-level error. All of the following were in place, and none of them fired:
// All registered — none triggered on crash
process.on('uncaughtException', handler); // ✗ not called
process.on('unhandledRejection', handler); // ✗ not called
process.on('exit', handler); // ✗ not called
process.on('SIGTERM', handler); // ✗ not called
process.on('SIGHUP', handler); // ✗ not called
process.on('SIGBREAK', handler); // ✗ not called
// Diagnostic reports enabled — no report generated
process.report.reportOnFatalError = true; // ✗ no report file
process.report.reportOnSignal = true; // ✗ no report fileAdditionally:
- No Windows Event Log entries related to the crash
- No crash dump files generated
- No stderr output at the moment of crash
- The process simply vanishes — the terminal returns to the prompt with no message
Debugging performed
1. Synchronous breadcrumb logging
Added fs.appendFileSync() calls before and after every operation to trace the exact crash point. This confirmed the crash happens after Phase 2 completes and before Phase 3's spawn() returns.
Example breadcrumb log (last entries before crash on v24):
9:06:32 [worker-1] _sshExec: completed with code 0 ← Phase 2 done
9:06:32 [worker-1] GC complete before Phase 3
9:06:32 [worker-1] Phase 3 START (heapUsed=24MB, rss=99MB)
9:06:32 [worker-1] Phase 3a: before rmSync ← CRASH HERE
2. Memory analysis (ruled out OOM)
At the moment of crash:
heapUsed: 24 MBrss: 99 MB- System free memory: ~9 GB (of 32 GB total)
OOM is definitively ruled out.
3. ssh2 library ruled out
Initially suspected the ssh2 npm library's native crypto bindings. However:
- ssh2 was disconnected and set to
null29 minutes before the crash - The native crypto build directory was removed via postinstall script (JS fallback only)
global.gc()was explicitly called twice after disconnecting ssh2- The crash still occurred even with ssh2 completely out of the picture
4. Systematic elimination
| Hypothesis | Test | Result |
|---|---|---|
| ssh2 native crypto | Removed native build, used JS fallback | Still crashes |
| ssh2 connection state | Disconnected ssh2 29 min before crash | Still crashes |
| OOM | Measured: 24MB heap, 99MB rss | Not OOM |
| System memory | 9GB free of 32GB | Not system OOM |
| SFTP download | Replaced with system ssh+tar pipe | Still crashes |
rmSync with Unicode paths |
Removed rmSync before crash point | Still crashes |
| GC timing | Explicit global.gc() before Phase 3 |
Still crashes |
| Event loop starvation | Added 50ms yield before Phase 3 | Still crashes |
| Node.js v24.13.0 | Switched to v22.22.1 | Fixed |
5. Successful run on v22.22.1
With identical code (no changes between test runs other than the Node.js version):
=== Summary ===
Completed: 3/3
Elapsed: 57.4 min
Process exiting with code 0
All three tasks completed, including two that involve ~25 minutes of child process execution followed by download. The crash never occurred.
Minimal reproduction pattern
While I haven't created a minimal reproducer, the pattern is:
- Run on Windows 11 with Node.js v24.13.0
- Use
child_process.spawn()to run a long-lived process (~25 min) - After the spawned process exits, immediately spawn new processes
- The process silently terminates at or around the second
spawn()call
The application uses:
child_process.spawn()for SSH and tar processesstream.pipe()between spawned processes (ssh stdout → tar stdin)setInterval(1s dashboard timer running throughout)--expose-gcflag with explicitglobal.gc()calls- ESM modules (
"type": "module")
Possibly related
- CVE-2025-59466: async_hooks stack overflow causing unrecoverable process termination. Fixed in v24.13.0, but the symptoms (process death bypassing all error handlers) are identical. This application does not explicitly use async_hooks, but dependencies or Node.js internals may.
- The crash only manifests after long-running operations (~25 min), suggesting a time-dependent or GC-cycle-dependent trigger.
Dependencies
{
"dotenv": "^16.5.0",
"js-yaml": "^4.1.0",
"ssh2": "^1.17.0",
"yargs": "^17.7.2"
}Note: ssh2's native crypto build is removed via postinstall (rm -rf node_modules/ssh2/lib/protocol/crypto/build), forcing the pure JS fallback.
Expected behavior
The process should either:
- Continue running normally, or
- If an error occurs, trigger
uncaughtException/unhandledRejection/exithandlers and produce a diagnostic report
Actual behavior
The process silently vanishes with no output, no error handlers triggered, and no diagnostic report generated.
Workaround
Use Node.js v22 LTS instead of v24.
Activity
tsunosekai commentedon Mar 6, 2026
Additional clue:--expose-gc+ explicitglobal.gc()may be the triggerUpdate: This hypothesis was disproven — see the next comment.
--expose-gcis NOT the trigger. The crash also occurs without--expose-gcand without anyglobal.gc()calls.Original comment (kept for investigation history)
We have a nearly identical application that performs the same workload (SSH into remote server, run ~25 min task, download results) but does not crash on v24.13.0.
Key differences between the crashing app and the non-crashing app:
--expose-gcflagglobal.gc()callsprocess.report, signal handlers)await cli.parse())The crashing app calls
global.gc()twice right after disconnecting the ssh2 library (which uses Node.js built-incryptomodule internally), then idles for ~25 minutes while a child process runs, then crashes when it tries tospawn()again.The non-crashing app keeps the ssh2 connection alive throughout and never calls
global.gc().tsunosekai commentedon Mar 6, 2026
Update:
--expose-gcis NOT the triggerRan the same application on v24.13.0 without
--expose-gcand without anyglobal.gc()calls. The crash still occurs.Furthermore, the crash also occurs when all remote tasks complete in seconds (output already existed, so all steps were skipped). This rules out the long-idle-period hypothesis.
New observation: the crash consistently happens after processing a task whose directory name contains non-ASCII (Japanese) characters. A task with an ASCII-only name always succeeds.
The crash sequence:
テスト_001) — upload, exec, download complete → crash before starting task 3The download uses
child_process.spawn('ssh', ...)piped tospawn('tar', ['xf', '-', '-C', localDir])wherelocalDircontains Unicode characters. Thetarprocess completes successfully (log shows "ssh+tar download complete"), but the process dies shortly after.This may be a Unicode path handling bug in child_process.spawn() or fs operations on Windows in v24.13.0.
Test conditions:
--expose-gc, noglobal.gc()callsRenegade334 commentedon Mar 8, 2026
Could you provide an example code reproduction?
DrDexter6000 commentedon Apr 6, 2026
Additional Independent Verification: Minimal Reproduction with Detached Process (~4-6 seconds)
We independently encountered and diagnosed this exact issue while debugging a Chrome Native Messaging host chain for a browser automation tool. Our findings strongly corroborate the original report and provide a significantly tighter minimal reproduction window.
Environment
child_process.spawnfrom within a Chrome Native Messaging host (native-host.cjs)Key Finding: Crash in 4-6 Seconds, Not 25 Minutes
Unlike the original report where the crash occurred after ~25 minutes of complex I/O (SSH, SFTP, tar), we isolated the issue to occur within 4-6 seconds using a completely bare script with zero I/O, zero external modules, and zero business logic.
Minimal Reproduction
worker.js— bare event loop, nothing else:host.js— spawns worker as a detached process:Result on v24.13.1:
worker.jssilently terminates viaTerminateProcess()after ~4-6 seconds.Result on v22.22.2:
worker.jssurvives indefinitely (tested 137+ seconds, manually stopped).Observations
No Exit Handlers Fire: We attached all available handlers — none triggered before the process vanished:
Wrapper Independence: Wrapping the spawn in
cmd.exe /c startorpowershell.exedid not prevent termination. Renamingnode.exeto a custom binary name also had no effect.Digital Signature Verified:
node.exehas a valid digital signature (OpenJS Foundation, Microsoft-verified), ruling out AV/trust policy as the cause.Python Baseline: An equivalent Python
time.sleep()loop spawned as a detached process survives indefinitely under identical conditions, confirming this is Node.js-specific.Version-Specific:
Impact in Our Use Case
This single bug explained all observed symptoms across 29 prior diagnostic tasks:
EPIPEerrors when the plugin wrote to a dead broker pipeprocess.on('exit')traces (handlers never fired becauseTerminateProcess()bypasses JS cleanup)What appeared to be a complex multi-layer failure in the Chrome extension ↔ native-host ↔ broker ↔ named-pipe chain was entirely caused by this single runtime regression.
Workaround (Verified)
Downloading
node.exefrom the v22.22.2 LTS release and using it specifically to spawn the broker process resolved the issue completely:After this change, the broker process (running on v22.22.2) survived 137+ seconds with no crashes, and all browser automation tools (
browser_get_tabs,browser_navigate,browser_snapshot) worked correctly.Hope this additional data point — especially the minimal reproduction with a bare
setInterval— helps narrow down the root cause in the runtime layer.DrDexter6000 commentedon Apr 6, 2026
Update: Windows Security Behavioral Detection Confirmed as Contributing Factor
After further investigation, we now have direct evidence that Windows Security (Defender) behavioral detection is killing detached child processes on Windows. This may be a contributing factor alongside any Node.js v24 runtime behavior.
Evidence
On 2026-04-06 at 08:23 local time, while running minimal detached spawn reproduction tests with Node.js v24.13.1 on Windows 11, Windows Security flagged and terminated the test script:
Trojan:Win32/SuspExec.SE(Suspicious Execution)The Windows Security Protection History shows the exact command line that was flagged. The test script was a minimal reproduction: spawn a detached
node -e 'setInterval(()=>{},1000)'child, then poll it withprocess.kill(pid, 0).Why This Pattern Triggers Detection
Windows Security behavioral analysis flags this pattern because it resembles C2 beacon/persistence behavior:
This is indistinguishable from malware persistence techniques at the behavioral level.
Key Observation: Other Tools Work Fine
On the same machine, other browser automation tools work without issues:
This suggests the issue is not purely a Node.js v24 runtime bug. If it were, all Node.js-based tools would be equally affected.
Revised Hypothesis
The root cause is likely a combination of factors:
detached: true + stdio: 'ignore' + child.unref()+ polling is exactly how many persistence implants work.Why Node v22 LTS Works
Node v22 LTS has been around longer. Windows Security has likely built behavioral whitelisting for common Node.js v22 spawn patterns. Node v24 is newer and its behavioral profile may not be fully recognized yet.
Updated Workaround
In addition to using Node v22 LTS, also consider:
nvm install 22 && nvm use 22This finding also explains the "invisible hand" effect we observed: processes dying after ~4-6 seconds with zero diagnostic output, zero JS exit handlers firing, and zero Event Log entries. Windows Security
TerminateProcess()produces exactly this symptom profile.DrDexter6000 commentedon Apr 6, 2026
TL;DR: Our original diagnosis was wrong. The actual root cause was Windows Defender behavior detection (
Trojan:Win32/SuspExec.SE), not Node.js v24 silently terminating detached processes.What Changed
After adding Windows Defender exclusions for
OpenCode.exe,opencode-cli.exe,node.exe, and their install directories:The same Node v24.13.1 binary, same code, same machine — the only change was Defender exclusions.
Corrected Diagnosis
TerminateProcess()Trojan:Win32/SuspExec.SEand kills themWhy the Misdiagnosis
TerminateProcess()was called — but by Windows Defender, not Node.js internalsTerminateProcess()is a forced kill, identical behavior to what we attributed to NodeEvidence Log
Trojan:Win32/SuspExec.SEdetected during debugging sessionsetIntervaldetached spawn on v24.13.1Apologies for the initial misattribution. The #62125 issue may still be valid for other reporters — we can only correct our own reproduction case.
Root cause analysis documented at: [Gist link TBD]
github-actions commentedon Jul 20, 2026
This issue has been marked as stale due to 90 days of inactivity.
It will be automatically closed in 30 days if no further activity occurs. If this is still relevant, please leave a comment or update it to keep it open.
chore(deps): major updates without code migrations
fix(spawn): windowsHide:true + Node v22 wrapper — kills 100-window sp…
fix(watchdog): use process.execPath + windowsHide on restart spawn