Skip to content

AOT: crypto.createHash('sha1').digest() returns a number, not a Buffer #86

Closed

Description

@proggeramlug

Environment

  • perry 0.5.95
  • macOS arm64 (darwin 25.4.0)

Problem

crypto.createHash(...).update(buf).digest() under the AOT binary returns a value with typeof === 'number' instead of a 20-byte Buffer. instanceof Uint8Array is false, .length is undefined.

Other Buffer-producing paths (Buffer.alloc, Buffer.from) work correctly from the same cross-module context, so this is specific to the crypto Hash wrapper.

Reproduction

// repro.ts
import * as crypto from 'node:crypto';

const h = crypto.createHash('sha1');
h.update(Buffer.from('hello', 'utf8'));
const d = h.digest();

console.log('typeof=' + typeof d);                           // 'number' (bug); expect 'object'
console.log('length=' + (d as { length?: number }).length);  // undefined; expect 20
console.log('instanceof Uint8Array=' + (d instanceof Uint8Array)); // false; expect true
perry compile repro.ts -o /tmp/hashrepro && /tmp/hashrepro
# Observed:
#   typeof=number
#   length=undefined
#   instanceof Uint8Array=false
#
# Expected (Node):
#   typeof=object
#   length=20
#   instanceof Uint8Array=true

Scope / blast radius

Any Node crypto API that returns a Buffer via the Hash object is affected. This breaks:

  • mysql_native_password: SHA1(pw) XOR SHA1(challenge ++ SHA1(SHA1(pw)))
  • caching_sha2_password: SHA256(pw) XOR SHA256(SHA256(SHA256(pw)) ++ challenge)
  • Any protocol that needs SHA-1 / SHA-256 / SHA-512 digests in-process.

Workarounds are painful — you'd have to reimplement the hash by hand, which defeats the point of having crypto in the stdlib.

Hit in real code

@perry/mysql's auth plugins (src/auth/native-password.ts, src/auth/caching-sha2.ts) compute the auth-response via h.digest(). Under the AOT binary the digest buffer has length === 0 by the time it reaches the outer nativeScramble, so the driver ends up sending an empty auth_response field in HandshakeResponse41 and the server rejects with errno 1045 "Access denied (using password: NO)".

Same source, run under Bun or Node, authenticates correctly against MySQL 8.

Expected

hash.digest() returns a Buffer of length matching the algorithm (20 for SHA-1, 32 for SHA-256, 64 for SHA-512), with Buffer.isBuffer(d) === true and d instanceof Uint8Array === true.

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.97. New HashHandle in perry-stdlib/src/crypto.rs wraps sha1/sha256/sha512/md5 state behind the handle registry; js_crypto_create_hash returns a small-int handle NaN-boxed with POINTER_TAG, and common/dispatch.rs routes subsequent .update/.digest through dispatch_hash. Also wired js_stdlib_init_dispatch() into main's prologue so HANDLE_METHOD_DISPATCH is registered eagerly (was previously lazy via ensure_pump_registered, which never fired for sync-only programs). SHA-1('hello') now returns aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d.

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