Skip to content

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

Closed

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.

Activity

  1. added
    bugConfirmed defect or regression
    parityCompatibility gap with Node.js, ECMAScript, or the supported ecosystem
    on Apr 18, 2026
  2. proggeramlug commented on Apr 18, 2026

    @proggeramlug
    ContributorAuthor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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