Skip to content

AOT: crypto.createHash('sha256').update(buf).digest() returns Buffer with undefined length for FIRST call inside socket-callback context #88

Description

@proggeramlug

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:

  1. The first call inside a socket 'data' event handler
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugConfirmed defect or regressionparityCompatibility gap with Node.js, ECMAScript, or the supported ecosystem

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions