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
AOT: crypto.createHash('sha256').update(buf).digest() returns Buffer with undefined length for FIRST call inside socket-callback context #88
Inside the socket 'data' callback path of a real MySQL connection, the firstcrypto.createHash('sha256').update(buf).digest() call returns a Buffer whose .length reads as undefined. Subsequent calls to the same function return correct 32-byte buffers.
At top level (no async context) or inside a plain setTimeout callback, all calls return correct 32-byte buffers. So this isn't a pure parity bug — it's context-sensitive.
Trace from inside @perry/mysql's sha256Scramble during caching_sha2_password auth:
[sha256] input.len=7 digest.len=undefined ← FIRST call, buggy
[scramble] sha256Pw.len=0 ← .length reads as 0 in the caller
[sha256] input.len=0 digest.len=32 ← SECOND call, fine
[scramble] sha256Sha256Pw.len=32
[sha256] input.len=52 digest.len=32 ← THIRD call, fine
[scramble] inner.len=32
out.len=0 loop-bound=0 ← loop never runs (bound=0)
sha256Scramble returned len=0 ← driver sends empty auth_response
The first non-empty-buffer hash returns a digest whose .length is undefined at the callsite, which is silently coerced to 0 in the outer scramble routine. The subsequent hashes (including hashing the broken zero-length digest) work fine.
Discovered workaround
Priming the sha256 machinery with a throwaway empty call before the real first one restores correctness:
sha256(Buffer.alloc(0));// throwaway — its digest.len is still 'undefined'constreal=sha256(pwBuf);// now returns a 32-byte Buffer correctly
src/auth/caching-sha2.ts:
function sha256(b: Buffer): Buffer {
const h = crypto.createHash('sha256');
h.update(b);
return h.digest(); // ← returns Buffer w/ undef length on first call in async ctx
}
export function sha256Scramble(password: string, challenge: Buffer): Buffer {
const sha256Pw = sha256(Buffer.from(password, 'utf8')); // ← len=0 here
...
}
Before applying the prime workaround the driver sent a zero-byte auth_response in HandshakeResponse41 and the server rejected with errno 1045 ("Access denied ... using password: NO"). After priming, the driver authenticates cleanly.
The priming workaround feels like papering over a real codegen/runtime issue. Would be great to fix properly.
#86 (closed / fixed in 0.5.95) covered the top-level case where digest returned typeof === 'number'. That part is fixed — typeof digest === 'object' now at top level AND in setTimeout. This new issue is specifically about:
The first call inside a socket 'data' event handler
The .length is undefined (not a number-return, the digest IS a Buffer-like object but its length metadata is missing)
Expected
digest.length is 32 on every call, regardless of async context.
Fixed in v0.5.98. Root cause: net.createConnection uses its own NEXT_NET_ID counter (starts at 1) while crypto.createHash uses the common HANDLES registry (also starts at 1). js_handle_method_dispatch checked is_net_socket_handle(handle) BEFORE the HashHandle downcast, so when the first HashHandle shared id=1 with the live socket, h.update(buf)/h.digest() routed to dispatch_net_socket which returned a stale sentinel — digest().length === 0. 2nd+ call worked because the hash id had advanced past the socket. Fix: reorder dispatch so with_handle::<HashHandle> runs first (it safely downcasts against the typed common registry). SHA-256 now returns 32 bytes on every call inside a socket 'data' callback.
Activity
proggeramlug commentedon Apr 18, 2026
Fixed in v0.5.98. Root cause:
net.createConnectionuses its ownNEXT_NET_IDcounter (starts at 1) whilecrypto.createHashuses the commonHANDLESregistry (also starts at 1).js_handle_method_dispatchcheckedis_net_socket_handle(handle)BEFORE theHashHandledowncast, so when the first HashHandle shared id=1 with the live socket,h.update(buf)/h.digest()routed todispatch_net_socketwhich returned a stale sentinel —digest().length === 0. 2nd+ call worked because the hash id had advanced past the socket. Fix: reorder dispatch sowith_handle::<HashHandle>runs first (it safely downcasts against the typed common registry). SHA-256 now returns 32 bytes on every call inside a socket 'data' callback.fix: gate handle dispatchers by method vocabulary (v0.5.99, #91)
fix: backfill v0.5.97/v0.5.98 source that v0.5.99 dispatch builds on