Repository navigation
Description
Summary
When sshcrypto.node is compiled with Node 24.19.0 (or newer 24.x) headers, the whole
process aborts as soon as the garbage collector reclaims one of the native cipher objects:
# node[7]: void node::RemoveEnvironmentCleanupHook(v8::Isolate*, CleanupHook, void*) at ../../src/api/hooks.cc:142
# Assertion failed: (env) != nullptr
1: node::Assert(node::AssertionInfo const&) [node]
2: node::RemoveEnvironmentCleanupHook(v8::Isolate*, void (*)(void*), void*) [node]
3: AESGCMDecipher::~AESGCMDecipher() [<app>/node_modules/ssh2/lib/protocol/crypto/build/Release/sshcrypto.node]
Seen in production on ssh2 1.17.0 with a long running service that opens an SFTP connection
every 30 seconds. No dependency changed; only the Node version used to build the addon moved
from 24.18.0 to 24.19.0.
Root cause is upstream, not in ssh2
Node 24.19.0 added cleanup hooks to node::ObjectWrap (nodejs/node#63642): its constructor
now calls AddEnvironmentCleanupHook and its destructor calls
RemoveEnvironmentCleanupHook. node_object_wrap.h is header only, so that code is compiled
into the addon. The destructor runs from the V8 weak callback, where no context is entered,
so Environment::GetCurrent() returns null and Node's CHECK_NOT_NULL(env) aborts.
On the 24.x line the matching runtime side fix was not backported, so every 24.x runtime is
affected. Details and a minimal reproducer without ssh2: nodejs/node#65262 and
nodejs/node#65446.
binding.cc picks up node::ObjectWrap through using namespace node; at the top, so all
of AESGCMCipher, AESGCMDecipher, ChaChaPoly* and Generic* inherit the affected
destructor.
Why the existing free() path does not save us
AESGCMDecipherBinding.free() in lib/protocol/crypto.js is an empty method, and the native
Free method only calls clear() (frees the EVP_CIPHER_CTX); it does not destroy the
wrapped object. So the ObjectWrap destructor always runs later, from GC, which is exactly
the path that aborts.
Suggestion
ssh2 already depends on NAN, and Nan::ObjectWrap (nan_object_wrap.h) has no cleanup hook
calls at all — its destructor only clears and resets the persistent handle. Inheriting from
Nan::ObjectWrap instead of node::ObjectWrap in binding.cc would make the addon
independent of this Node change.
Workarounds for users hitting this now
- Build the addon with Node 24.18.x headers (24.18.1 is the newest release on that line, so
no security fixes are given up). - Or skip the native binding and let ssh2 fall back to its JavaScript crypto, e.g.
rm -rf node_modules/ssh2/lib/protocol/crypto/buildafter install.
Environment
- ssh2 1.17.0
- Node 24.19.0 headers, 24.18.0 and 24.19.0 runtimes (both abort)
- Linux x86_64, Debian 13 container; the upstream reproducer also aborts on Darwin arm64
Activity
roy-trgscreen commentedon Aug 20, 2026
Thank you for the quick fix @mscdex
crypto: use nan's ObjectWrap
roy-trgscreen commentedon Sep 9, 2026
@mscdex would a new release be possible for this issue?
dsfcoll commentedon Sep 25, 2026
@mscdex Hello! When it will be shipped? It blocks using latest version of node js.