Environment
- perry 0.5.97
- macOS arm64 (darwin 25.4.0)
Problem
Inside the socket 'data' callback path of a real MySQL connection, the first crypto.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'
const real = sha256(pwBuf); // now returns a 32-byte Buffer correctly
Hit in real code
@perry/mysql's caching_sha2_password auth path. Source layout:
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.
Separation from #86
#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.
Environment
Problem
Inside the socket 'data' callback path of a real MySQL connection, the first
crypto.createHash('sha256').update(buf).digest()call returns a Buffer whose.lengthreads asundefined. Subsequent calls to the same function return correct 32-byte buffers.At top level (no async context) or inside a plain
setTimeoutcallback, 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
sha256Scrambleduringcaching_sha2_passwordauth:The first non-empty-buffer hash returns a digest whose
.lengthis 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:
Hit in real code
@perry/mysql'scaching_sha2_passwordauth path. Source layout:Before applying the prime workaround the driver sent a zero-byte
auth_responsein 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.
Separation from #86
#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:.lengthis undefined (not a number-return, the digest IS a Buffer-like object but its length metadata is missing)Expected
digest.lengthis 32 on every call, regardless of async context.