From 27f06f41dbf3c3d4042944f2c6fdd06ea12833d5 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 00:22:29 -0500 Subject: [PATCH 1/2] Carry Hash and Hmac values through unknown and any slots A Hash or Hmac that flows into an unknown or any slot, or into untyped JavaScript code, is now a native handle (Hash or Hmac) instead of a compile error. The handle keeps the value's identity, so a cast back to Hash or Hmac answers the same value. update(), digest() and Hash's copy() dispatch through it, called directly or read as values: update is one function for Hash and Hmac and digest is one per class, as in Node 24.22, and update() answers the receiver. The stream and event members throw a coded SC2020 error there, as do the methods called with another receiver; other names read as undefined. The to-dyn conversion boxes a Hash or Hmac through scr_crypto_hash_to_dyn, which installs the handle ops on first use, so programs that never box one link none of the handle code. A native handle read through `as any` now dispatches its method calls through the handle. Calling copy() on an Hmac throws Node's TypeError ("mac.copy is not a function") after the arguments are evaluated, instead of the method-call fence. Corpus 5768 and 5769 cover the TypeScript and JavaScript forms against Node 24.22, and errors.test.ts covers the SC2020 cases. corpus-costs.json records their plain and sanitized costs. --- .../limitations/builtin-apis/crypto.mdx | 2 +- .../generated/node-v24-backlog.json | 24 +++ .../generated/node-v24-internal.json | 32 +++ internal/compatibility/static-support.json | 2 +- packages/compiler/src/backend/llvm/dyn.ts | 22 ++- .../src/frontend/lowering/builtins/crypto.ts | 22 ++- .../src/frontend/lowering/lower-calls.ts | 6 + packages/compiler/src/ir/ir.ts | 2 + .../corpus/5768-crypto-hash-unknown.ts.json | 6 + .../corpus/5769-crypto-hash-unknown.cjs.json | 6 + packages/runtime/src/scr_crypto.h | 4 + packages/runtime/src/scr_dyn.h | 2 + packages/runtime/src/scr_lib.c | 187 ++++++++++++++++++ tests/corpus/5768-crypto-hash-unknown.ts | 68 +++++++ tests/corpus/5769-crypto-hash-unknown.cjs | 77 ++++++++ tests/harness/corpus-costs.json | 4 + tests/harness/errors.test.ts | 37 ++++ 17 files changed, 497 insertions(+), 6 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5768-crypto-hash-unknown.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5769-crypto-hash-unknown.cjs.json create mode 100644 tests/corpus/5768-crypto-hash-unknown.ts create mode 100644 tests/corpus/5769-crypto-hash-unknown.cjs diff --git a/docs/content/limitations/builtin-apis/crypto.mdx b/docs/content/limitations/builtin-apis/crypto.mdx index 586e0b1fd6..988228aff7 100644 --- a/docs/content/limitations/builtin-apis/crypto.mdx +++ b/docs/content/limitations/builtin-apis/crypto.mdx @@ -1 +1 @@ -- **Crypto:** Hashing, HMAC, PBKDF2, and HKDF support every digest `crypto.getHashes()` lists in Node 24, with OpenSSL's aliases and object identifiers. Hash and Hmac accept every Buffer encoding and any TypedArray or DataView, `createHash` and `hash.copy` take `outputLength`, and `crypto.hash` takes its options object; HMAC, PBKDF2, and HKDF reject the SHAKE and KMAC digests with Node's errors. Hash and Hmac values are also Transforms: they can be written, piped into, used as `pipeline()` stages, read, and listened to, and they report Node's errors after the stream flushes. `copy()` of a Hash whose stream flushed, and stream use of a value after its stream closed, throw a coded SC2020 error. `timingSafeEqual` compares any view or ArrayBuffer. Secret KeyObjects (`createSecretKey`, `generateKey` and `generateKeySync` for `hmac` and `aes`) support `type`, `symmetricKeySize`, `export()`, `equals()`, and `instanceof KeyObject`, and createHmac, hkdf, createCipheriv, and createDecipheriv accept them; asymmetric keys, `KeyObject.from()`, and `toCryptoKey()` are not available. `randomFill`, `randomFillSync` (every view kind and ArrayBuffer), `randomInt`, `randomUUID`, and `randomUUIDv7` follow Node 24.22, including callback timing; a `randomInt` callback whose error parameter is typed `Error | null` receives `null` where Node passes `undefined`. Scrypt supports its default parameters, numeric options, aliases, and memory limit. Both KDFs accept strings, Buffers, numeric typed arrays, DataViews, and fixed-length ArrayBuffers. `createCipheriv` and `createDecipheriv` support aes-128, aes-192, and aes-256 in CBC, CTR, and GCM modes and chacha20-poly1305, on native targets, through `update()`, `final()`, `setAAD()`, `setAutoPadding()`, `getAuthTag()`, and `setAuthTag()`; other ciphers, cipher streams, and wasm32-wasi builds are refused. Decrypting chacha20-poly1305 without an authentication tag fails in `final()`; Node 24.22 returns unverified plaintext. After `update()` fails, for example past the AES-GCM limit of 2^36 - 32 bytes per message, every later call on that cipher throws; Node still lets `final()` run. An output encoding that is a function or an instance of a class defining `toString` throws a coded SC2020 error. Public-key operations remain unsupported. Native derivations finish before their deferred callbacks, so computation blocks the caller. +- **Crypto:** Hashing, HMAC, PBKDF2, and HKDF support every digest `crypto.getHashes()` lists in Node 24, with OpenSSL's aliases and object identifiers. Hash and Hmac accept every Buffer encoding and any TypedArray or DataView, `createHash` and `hash.copy` take `outputLength`, and `crypto.hash` takes its options object; HMAC, PBKDF2, and HKDF reject the SHAKE and KMAC digests with Node's errors. Hash and Hmac values are also Transforms: they can be written, piped into, used as `pipeline()` stages, read, and listened to, and they report Node's errors after the stream flushes. `copy()` of a Hash whose stream flushed, and stream use of a value after its stream closed, throw a coded SC2020 error. Through `unknown` and `any` slots and untyped JavaScript, Hash and Hmac values keep their identity and support `update()`, `digest()`, and Hash's `copy()`, called or read as values; their stream and event members there throw a coded SC2020 error. `timingSafeEqual` compares any view or ArrayBuffer. Secret KeyObjects (`createSecretKey`, `generateKey` and `generateKeySync` for `hmac` and `aes`) support `type`, `symmetricKeySize`, `export()`, `equals()`, and `instanceof KeyObject`, and createHmac, hkdf, createCipheriv, and createDecipheriv accept them; asymmetric keys, `KeyObject.from()`, and `toCryptoKey()` are not available. `randomFill`, `randomFillSync` (every view kind and ArrayBuffer), `randomInt`, `randomUUID`, and `randomUUIDv7` follow Node 24.22, including callback timing; a `randomInt` callback whose error parameter is typed `Error | null` receives `null` where Node passes `undefined`. Scrypt supports its default parameters, numeric options, aliases, and memory limit. Both KDFs accept strings, Buffers, numeric typed arrays, DataViews, and fixed-length ArrayBuffers. `createCipheriv` and `createDecipheriv` support aes-128, aes-192, and aes-256 in CBC, CTR, and GCM modes and chacha20-poly1305, on native targets, through `update()`, `final()`, `setAAD()`, `setAutoPadding()`, `getAuthTag()`, and `setAuthTag()`; other ciphers, cipher streams, and wasm32-wasi builds are refused. Decrypting chacha20-poly1305 without an authentication tag fails in `final()`; Node 24.22 returns unverified plaintext. After `update()` fails, for example past the AES-GCM limit of 2^36 - 32 bytes per message, every later call on that cipher throws; Node still lets `final()` run. An output encoding that is a function or an instance of a class defining `toString` throws a coded SC2020 error. Public-key operations remain unsupported. Native derivations finish before their deferred callbacks, so computation blocks the caller. diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index 0046a5acc9..40f2f1ca29 100644 --- a/internal/compatibility/generated/node-v24-backlog.json +++ b/internal/compatibility/generated/node-v24-backlog.json @@ -6729,6 +6729,8 @@ "source": "compiler-dedicated:crypto.createHash", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -6767,6 +6769,8 @@ "source": "compiler-dedicated:crypto.createHmac", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -7389,6 +7393,8 @@ "source": "compiler-dedicated:crypto.hash", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -7512,6 +7518,8 @@ "source": "compiler-dedicated:crypto.pbkdf2", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -7550,6 +7558,8 @@ "source": "compiler-dedicated:crypto.pbkdf2Sync", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -9030,6 +9040,8 @@ "source": "compiler-dedicated:Hash", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -9068,6 +9080,8 @@ "source": "compiler-dedicated:hash.copy", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -9106,6 +9120,8 @@ "source": "compiler-dedicated:hash.digest", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -9144,6 +9160,8 @@ "source": "compiler-dedicated:hash.update", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -9182,6 +9200,8 @@ "source": "compiler-dedicated:Hmac", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -9220,6 +9240,8 @@ "source": "compiler-dedicated:hmac.digest", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -9258,6 +9280,8 @@ "source": "compiler-dedicated:hmac.update", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index 67d79376e4..d0313ae147 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -19849,6 +19849,8 @@ "tests/corpus/5760-crypto-hash-stream.ts", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/node24-crypto-kdf.ts", @@ -20390,6 +20392,8 @@ "evidence": "compiler-dedicated:crypto.createHash", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -20427,6 +20431,8 @@ "evidence": "compiler-dedicated:crypto.createHmac", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -21028,6 +21034,8 @@ "evidence": "compiler-dedicated:crypto.hash", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -21146,6 +21154,8 @@ "evidence": "compiler-dedicated:crypto.pbkdf2", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -21183,6 +21193,8 @@ "evidence": "compiler-dedicated:crypto.pbkdf2Sync", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -23083,6 +23095,8 @@ "evidence": "compiler-dedicated:Hash", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -23120,6 +23134,8 @@ "evidence": "compiler-dedicated:hash.copy", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -23157,6 +23173,8 @@ "evidence": "compiler-dedicated:hash.digest", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -23194,6 +23212,8 @@ "evidence": "compiler-dedicated:hash.update", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -23231,6 +23251,8 @@ "evidence": "compiler-dedicated:Hmac", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -23268,6 +23290,8 @@ "evidence": "compiler-dedicated:hmac.digest", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -23305,6 +23329,8 @@ "evidence": "compiler-dedicated:hmac.update", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -25019,6 +25045,8 @@ "evidence": "compiler-dedicated:crypto.pbkdf2", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -28102,6 +28130,8 @@ "evidence": "compiler-dedicated:Hash", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -28152,6 +28182,8 @@ "evidence": "compiler-dedicated:Hmac", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 252c19d719..9791b6594e 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -370,7 +370,7 @@ { "symbols": ["assert.deepEqual", "assert.notDeepEqual"], "status": "partial", "evidence": ["tests/corpus/5513-assert-deep-equal-loose.js", "tests/corpus/5514-assert-deep-equal-typed.ts", "tests/corpus/5516-assert-deep-equal-edges.js"] }, { "symbols": ["assert", "assert.ok", "assert.equal", "assert.notEqual", "assert.strictEqual", "assert.notStrictEqual", "assert.deepStrictEqual", "assert.notDeepStrictEqual", "assert.fail", "assert.match", "assert.doesNotMatch", "assert.throws", "assert.rejects", "assert.ifError"], "status": "partial", "evidence": ["tests/corpus/1600-assert-passing.ts", "tests/corpus/1604-assert-deep-structures.ts", "tests/corpus/1609-assert-async.ts", "tests/corpus/1721-assert-throws-regex-class.ts", "tests/corpus/2936-static-loose-equality.ts", "tests/corpus/5515-assert-deep-strict-typed-messages.ts"] }, { "chapter": "crypto", "symbols": ["Cipheriv", "cipher.update", "cipher.final", "cipher.setAAD", "cipher.getAuthTag", "cipher.setAutoPadding", "Decipheriv", "decipher.update", "decipher.final", "decipher.setAAD", "decipher.setAuthTag", "decipher.setAutoPadding"], "status": "partial", "evidence": ["tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/fixtures/node-types/cipher.ts"] }, - { "symbols": ["crypto.createHash", "Hash", "hash.update", "hash.digest", "hash.copy", "crypto.createHmac", "Hmac", "hmac.update", "hmac.digest", "crypto.hash", "crypto.pbkdf2", "crypto.pbkdf2Sync"], "status": "partial", "evidence": ["tests/corpus/5760-crypto-hash-stream.ts", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", "tests/corpus/node24-crypto-sha2.ts", "tests/corpus/5435-crypto-hash-algorithms.ts", "tests/corpus/5436-crypto-hash-names.js", "tests/corpus/5437-crypto-hmac-algorithms.ts", "tests/corpus/5438-crypto-kdf-digests.ts", "tests/corpus/5440-crypto-hash-encodings.js", "tests/corpus/5441-crypto-hash-output-length.ts", "tests/harness/npm-static.test.ts"] }, + { "symbols": ["crypto.createHash", "Hash", "hash.update", "hash.digest", "hash.copy", "crypto.createHmac", "Hmac", "hmac.update", "hmac.digest", "crypto.hash", "crypto.pbkdf2", "crypto.pbkdf2Sync"], "status": "partial", "evidence": ["tests/corpus/5760-crypto-hash-stream.ts", "tests/corpus/5768-crypto-hash-unknown.ts", "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", "tests/corpus/node24-crypto-sha2.ts", "tests/corpus/5435-crypto-hash-algorithms.ts", "tests/corpus/5436-crypto-hash-names.js", "tests/corpus/5437-crypto-hmac-algorithms.ts", "tests/corpus/5438-crypto-kdf-digests.ts", "tests/corpus/5440-crypto-hash-encodings.js", "tests/corpus/5441-crypto-hash-output-length.ts", "tests/harness/npm-static.test.ts"] }, { "symbols": ["crypto.randomFill", "crypto.randomFillSync", "crypto.randomUUID", "crypto.randomUUIDv7"], "status": "supported", "evidence": ["tests/corpus/5442-crypto-random-values.ts", "tests/corpus/5443-crypto-random-arguments.js", "tests/corpus/1358-crypto-random.ts"] }, { "symbols": ["crypto.createSecretKey", "crypto.generateKey", "crypto.generateKeySync", "KeyObject", "keyObject.export", "keyObject.equals", "keyObject.symmetricKeySize", "keyObject.type"], "status": "partial", "evidence": ["tests/corpus/5445-crypto-secret-key-objects.ts", "tests/corpus/5446-crypto-secret-key-errors.js"] }, { "symbols": ["crypto.timingSafeEqual"], "status": "supported", "evidence": ["tests/corpus/5444-crypto-timing-safe-equal.js", "tests/corpus/2890-crypto-static-utility.ts"] }, diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index 86a8a945e5..0ea8402549 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -84,6 +84,16 @@ const DYN_HANDLE_TAG_NUM: Record = { child: 16, fileHandle: 17, dir: 44, + cryptoHash: 49, + cryptoHmac: 50, +}; + +/** Handle kinds whose runtime unit boxes them through its own function, + * which installs the tag's ops on first use (scr_crypto.h). Programs that + * never box one of these values do not link its dynamic members. */ +const DYN_HANDLE_BOX_FN: Record = { + cryptoHash: "scr_crypto_hash_to_dyn", + cryptoHmac: "scr_crypto_hash_to_dyn", }; export const DYN_KIND = { @@ -2932,9 +2942,17 @@ export class LlDyn { // tag's installed ops. const h = DYN_HANDLE_KINDS.get(t.kind); if (h) { - host.declare(`declare ptr @scr_dyn_new_handle(ptr, i32)`); + const box = DYN_HANDLE_BOX_FN[t.kind]; const r = B.tmp(); - B.line(`${r} = call ptr @scr_dyn_new_handle(ptr %v, i32 ${DYN_HANDLE_TAG_NUM[t.kind]})`); + if (box) { + host.declare(`declare ptr @${box}(ptr)`); + B.line(`${r} = call ptr @${box}(ptr %v)`); + } else { + host.declare(`declare ptr @scr_dyn_new_handle(ptr, i32)`); + B.line( + `${r} = call ptr @scr_dyn_new_handle(ptr %v, i32 ${DYN_HANDLE_TAG_NUM[t.kind]})`, + ); + } B.terminate(`ret ptr ${r}`); break; } diff --git a/packages/compiler/src/frontend/lowering/builtins/crypto.ts b/packages/compiler/src/frontend/lowering/builtins/crypto.ts index 2202088fae..ae6ddc9516 100644 --- a/packages/compiler/src/frontend/lowering/builtins/crypto.ts +++ b/packages/compiler/src/frontend/lowering/builtins/crypto.ts @@ -1,6 +1,6 @@ import { lowerBuiltinByteInput, errorFirstBytesCallback, errorFirstCallback } from "./arguments.js"; import { isKeyObjectValue } from "./key-object.js"; -import { boolLit, dynUndefinedExpr, varRef } from "../../../ir/build.js"; +import { boolLit, dynUndefinedExpr, nodeThrowExpr, varRef } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer, own } from "../lowerer.js"; import { locOf } from "../../program.js"; @@ -940,9 +940,27 @@ export function lowerCryptoHashMethodCall( if (call.questionDotToken || access.questionDotToken) return null; const receiverType = lowerer.mapTypeOf(lowerer.typeOf(access.expression)); if (receiverType?.kind !== "cryptoHash" && receiverType?.kind !== "cryptoHmac") return null; - if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const loc = locOf(call); + // Hmac has no copy(): Node reads the member as undefined and throws a + // TypeError once the arguments are evaluated. + if (receiverType.kind === "cryptoHmac" && name === "copy") { + if (call.arguments.some(ts.isSpreadElement)) + lowerer.noLowering("Hmac.copy with spread arguments", call); + const evaluated = [access.expression, ...call.arguments].map((node): IrStmt => ({ + kind: "exprStmt", + expr: lowerer.lowerExpr(node), + loc, + })); + return { + kind: "seqExpr", + stmts: evaluated, + result: nodeThrowExpr(1, "", `${access.getText()} is not a function`, DYN, loc), + type: DYN, + loc, + }; + } + if (!lowerer.isStdlibMember(access)) return null; const receiver = (): IrExpr => lowerer.lowerExprExpecting(access.expression, receiverType); const hash = receiverType.kind === "cryptoHash"; const label = hash ? "Hash" : "Hmac"; diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 2ba83a3d58..27abe2115a 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -33,6 +33,7 @@ import { BOOL, CAUGHT, DYN, + DYN_HANDLE_KINDS, F64, type IrExpr, type IrFunction, @@ -3252,6 +3253,11 @@ function lowerDynReceiverMethodCall( lowerStringMethodCall(lowerer, call, access, () => recv) ); } + // A native handle read through `as any` (`(hash as any).update(x)`) + // dispatches through the handle, as the same value held in an + // 'unknown' slot does. + if (checkerUntyped && DYN_HANDLE_KINDS.has(recv.type.kind)) + recv = lowerer.coerceToExpected(recv, DYN); if (recv.type.kind !== "dyn") return null; if ( access.name.text === "normalize" && diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index eaee503fa5..d99c77da16 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -3879,6 +3879,8 @@ export const DYN_HANDLE_KINDS: ReadonlyMap ["http2Stream", { tag: "SCR_DYNH_H2_STREAM", cls: "Http2Stream" }], ["httpClientReq", { tag: "SCR_DYNH_HTTP_CLIENT", cls: "ClientRequest" }], ["dir", { tag: "SCR_DYNH_DIR", cls: "Dir" }], + ["cryptoHash", { tag: "SCR_DYNH_HASH", cls: "Hash" }], + ["cryptoHmac", { tag: "SCR_DYNH_HMAC", cls: "Hmac" }], ]); /** A class value that crosses an `unknown` slot as a compiler-owned typed diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5768-crypto-hash-unknown.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5768-crypto-hash-unknown.ts.json new file mode 100644 index 0000000000..ba6370e68a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5768-crypto-hash-unknown.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5768-crypto-hash-unknown.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5769-crypto-hash-unknown.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5769-crypto-hash-unknown.cjs.json new file mode 100644 index 0000000000..60e1632e90 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5769-crypto-hash-unknown.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5769-crypto-hash-unknown.cjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_crypto.h b/packages/runtime/src/scr_crypto.h index 6b70a7fab6..e106a5ab4b 100644 --- a/packages/runtime/src/scr_crypto.h +++ b/packages/runtime/src/scr_crypto.h @@ -25,6 +25,10 @@ ScrCryptoHash *scr_crypto_hash_retain(ScrCryptoHash *h); void scr_crypto_hash_release(ScrCryptoHash *h); void *scr_crypto_hash_retain_v(void *p); void scr_crypto_hash_release_v(void *p); +/* A Hash or Hmac boxed as a checked-dynamic handle (+1; retains h). The + * emitted to-dyn conversion calls it, and it installs the handle ops on + * first use, so programs that never box a Hash do not link them. */ +ScrDyn *scr_crypto_hash_to_dyn(ScrCryptoHash *h); ScrCryptoHash *scr_crypto_hash_update_str(ScrCryptoHash *h, ScrStr *data); ScrCryptoHash *scr_crypto_hash_update_bytes(ScrCryptoHash *h, ScrBytes *data); ScrCryptoHash *scr_crypto_hash_copy(ScrCryptoHash *h); diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 375ced5737..f0b0538bff 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -168,6 +168,8 @@ typedef enum { SCR_DYNH_DECIPHER, /* node:crypto Decipheriv (scr_cipher.c) */ SCR_DYNH_NET_SOCKET_ADDRESS, /* net.SocketAddress (scr_net.c) */ SCR_DYNH_NET_BLOCK_LIST, /* net.BlockList (scr_net.c) */ + SCR_DYNH_HASH, /* node:crypto Hash (scr_lib.c) */ + SCR_DYNH_HMAC, /* node:crypto Hmac (scr_lib.c) */ SCR_DYNH_COUNT, } ScrDynHandleTag; diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index 5f595f2632..21534d36a3 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -8031,6 +8031,193 @@ void scr_crypto_hash_release(ScrCryptoHash *hash) { void *scr_crypto_hash_retain_v(void *p) { return scr_crypto_hash_retain(p); } void scr_crypto_hash_release_v(void *p) { scr_crypto_hash_release(p); } +/* Hash and Hmac values seen as 'unknown' are native handles. update(), + * digest() and Hash's copy() dispatch through the handle, called or read + * as values; the other members Node defines on a Hash or Hmac (the + * Transform and emitter surface) throw a coded SC2020 error, and other + * names read as undefined, as they do in Node. The member names are one + * NUL-separated char array, which the linker drops from programs that + * never box a Hash (an array of string pointers keeps the strings in the + * unit's shared string section). */ +static const char scr_crypto_hash_dyn_members[] = + "_destroy\0_events\0_eventsCount\0_final\0_flush\0_maxListeners\0_options\0_read\0" + "_readableState\0_transform\0_undestroy\0_writableState\0_write\0_writev\0addListener\0" + "allowHalfOpen\0closed\0compose\0constructor\0cork\0destroy\0destroyed\0digest\0drop\0" + "emit\0end\0errored\0eventNames\0every\0filter\0find\0flatMap\0forEach\0getMaxListeners\0" + "isPaused\0iterator\0listenerCount\0listeners\0map\0off\0on\0once\0pause\0pipe\0" + "prependListener\0prependOnceListener\0push\0rawListeners\0read\0readable\0" + "readableAborted\0readableBuffer\0readableDidRead\0readableEncoding\0readableEnded\0" + "readableFlowing\0readableHighWaterMark\0readableLength\0readableObjectMode\0reduce\0" + "removeAllListeners\0removeListener\0resume\0setDefaultEncoding\0setEncoding\0" + "setMaxListeners\0some\0take\0toArray\0uncork\0unpipe\0unshift\0update\0wrap\0writable\0" + "writableBuffer\0writableCorked\0writableEnded\0writableFinished\0writableHighWaterMark\0" + "writableLength\0writableNeedDrain\0writableObjectMode\0write\0"; + +static bool scr_crypto_hash_dyn_member(const ScrCryptoHash *hash, const char *key, size_t len) { + if (len == 4 && memcmp(key, "copy", 4) == 0) return !hash->hmac; + for (const char *name = scr_crypto_hash_dyn_members; *name; name += strlen(name) + 1) + if (strlen(name) == len && memcmp(name, key, len) == 0) return true; + return false; +} + +static void scr_crypto_hash_dyn_unsupported(const ScrCryptoHash *hash) { + static const char hash_msg[] = + "Hash members other than update(), digest() and copy() have no lowering on an 'unknown' " + "value; cast the value to Hash first"; + static const char hmac_msg[] = + "Hmac members other than update() and digest() have no lowering on an 'unknown' value; " + "cast the value to Hmac first"; + if (hash->hmac) scr_throw_error_msg_code(SCR_ERR_ERROR, hmac_msg, sizeof hmac_msg - 1, "SC2020"); + else scr_throw_error_msg_code(SCR_ERR_ERROR, hash_msg, sizeof hash_msg - 1, "SC2020"); +} + +/* The method values: update() is one function for Hash and Hmac, as in + * Node; digest() is one per class, and copy() is Hash's. */ +enum { SCR_HASH_DYN_UPDATE, SCR_HASH_DYN_HASH_DIGEST, SCR_HASH_DYN_HMAC_DIGEST, SCR_HASH_DYN_COPY }; +static const char *const scr_crypto_hash_dyn_method_names[] = {"update", "digest", "digest", "copy"}; +static SCR_TL ScrDyn *scr_crypto_hash_dyn_methods[4]; +static SCR_TL bool scr_crypto_hash_dyn_methods_registered; + +static void scr_crypto_hash_dyn_methods_cleanup(void) { + for (size_t i = 0; i < 4; i++) { + ScrDyn *method = scr_crypto_hash_dyn_methods[i]; + scr_crypto_hash_dyn_methods[i] = NULL; + scr_dyn_release(method); + } +} + +static int scr_crypto_hash_dyn_method_index(const ScrCryptoHash *hash, const char *key, size_t len) { + if (len == 6 && memcmp(key, "update", 6) == 0) return SCR_HASH_DYN_UPDATE; + if (len == 6 && memcmp(key, "digest", 6) == 0) + return hash->hmac ? SCR_HASH_DYN_HMAC_DIGEST : SCR_HASH_DYN_HASH_DIGEST; + if (len == 4 && memcmp(key, "copy", 4) == 0 && !hash->hmac) return SCR_HASH_DYN_COPY; + return -1; +} + +/* Runs method `index` on `hash`; `self` is the receiving box, which + * update() answers. */ +static ScrDyn *scr_crypto_hash_dyn_run(ScrCryptoHash *hash, ScrDyn *self, int index, + ScrDyn *const *args, size_t argc) { + ScrDyn *undefined = scr_dyn_undefined(); + ScrDyn *first = argc > 0 ? args[0] : undefined; + ScrDyn *second = argc > 1 ? args[1] : undefined; + ScrDyn *result = NULL; + if (index == SCR_HASH_DYN_UPDATE) { + ScrCryptoHash *updated = scr_crypto_hash_update_dyn(hash, first, second); + if (updated) { + scr_crypto_hash_release(updated); + result = scr_dyn_retain(self); + } + } else if (index == SCR_HASH_DYN_COPY) { + ScrCryptoHash *copy = scr_crypto_hash_copy_opts(hash, first); + if (copy) { + result = scr_dyn_new_handle(copy, SCR_DYNH_HASH); + scr_crypto_hash_release(copy); + } + } else { + result = scr_crypto_hash_digest_dyn(hash, first); + } + scr_dyn_release(undefined); + return result; +} + +static ScrDyn *scr_crypto_hash_dyn_method_call(ScrClosure *clo, ScrDyn *const *args, size_t argc) { + int index = (int)scr_box_get_f64(clo->caps[0]); + ScrDyn *self = scr_dyn_this_get(); + bool hash_this = self->kind == SCR_DYN_HANDLE && self->v.handle.tag == SCR_DYNH_HASH; + bool hmac_this = self->kind == SCR_DYN_HANDLE && self->v.handle.tag == SCR_DYNH_HMAC; + bool valid = index == SCR_HASH_DYN_UPDATE ? hash_this || hmac_this + : index == SCR_HASH_DYN_HMAC_DIGEST ? hmac_this + : hash_this; + ScrDyn *result = NULL; + if (valid) { + result = scr_crypto_hash_dyn_run(self->v.handle.ptr, self, index, args, argc); + } else { + static const char msg[] = + "Hash and Hmac methods called with a receiver other than their own Hash or Hmac have no " + "lowering"; + scr_throw_error_msg_code(SCR_ERR_ERROR, msg, sizeof msg - 1, "SC2020"); + } + scr_dyn_release(self); + return result; +} + +static ScrDyn *scr_crypto_hash_dyn_method_value(int index) { + if (!scr_crypto_hash_dyn_methods_registered) { + scr_crypto_hash_dyn_methods_registered = true; + scr_atexit(scr_crypto_hash_dyn_methods_cleanup); + } + if (!scr_crypto_hash_dyn_methods[index]) { + ScrClosure *clo = scr_closure_new(NULL, 1); + clo->caps[0] = scr_box_new(SCR_BOX_F64); + scr_box_set_f64(clo->caps[0], (double)index); + uint32_t arity = index == SCR_HASH_DYN_UPDATE ? 2 : 1; + scr_crypto_hash_dyn_methods[index] = scr_dyn_new_func( + clo, scr_crypto_hash_dyn_method_call, arity, "", scr_crypto_hash_dyn_method_names[index]); + } + return scr_dyn_retain(scr_crypto_hash_dyn_methods[index]); +} + +static ScrDyn *scr_crypto_hash_dyn_invoke(void *h, ScrDyn *self, const char *method, + ScrDyn *const *args, size_t argc, const char *what) { + ScrCryptoHash *hash = h; + size_t len = strlen(method); + int index = scr_crypto_hash_dyn_method_index(hash, method, len); + if (index >= 0) return scr_crypto_hash_dyn_run(hash, self, index, args, argc); + if (scr_crypto_hash_dyn_member(hash, method, len)) { + scr_crypto_hash_dyn_unsupported(hash); + return NULL; + } + char message[160]; + int n = snprintf(message, sizeof message, "%s is not a function", what); + scr_throw_error_msg(SCR_ERR_TYPE, message, (size_t)n); + return NULL; +} + +static ScrDyn *scr_crypto_hash_dyn_get(void *h, const char *key, size_t len) { + int index = scr_crypto_hash_dyn_method_index(h, key, len); + if (index >= 0) return scr_crypto_hash_dyn_method_value(index); + if (scr_crypto_hash_dyn_member(h, key, len)) scr_crypto_hash_dyn_unsupported(h); + return NULL; +} + +static bool scr_crypto_hash_dyn_set(void *h, const char *key, size_t len, const ScrDyn *value) { + (void)h; + (void)key; + (void)len; + (void)value; + return false; +} + +static void scr_crypto_hash_dyn_install(void) { + static SCR_TL bool installed; + if (installed) return; + installed = true; + static const ScrDynHandleOps hash_ops = { + .cls = "Hash", + .retain = scr_crypto_hash_retain_v, + .release = scr_crypto_hash_release_v, + .invoke = scr_crypto_hash_dyn_invoke, + .get = scr_crypto_hash_dyn_get, + .set = scr_crypto_hash_dyn_set, + }; + static const ScrDynHandleOps hmac_ops = { + .cls = "Hmac", + .retain = scr_crypto_hash_retain_v, + .release = scr_crypto_hash_release_v, + .invoke = scr_crypto_hash_dyn_invoke, + .get = scr_crypto_hash_dyn_get, + .set = scr_crypto_hash_dyn_set, + }; + scr_dyn_handle_install(SCR_DYNH_HASH, &hash_ops); + scr_dyn_handle_install(SCR_DYNH_HMAC, &hmac_ops); +} + +ScrDyn *scr_crypto_hash_to_dyn(ScrCryptoHash *hash) { + scr_crypto_hash_dyn_install(); + return scr_dyn_new_handle(hash, hash->hmac ? SCR_DYNH_HMAC : SCR_DYNH_HASH); +} + static ScrCryptoHash *scr_crypto_hash_update_raw(ScrCryptoHash *hash, const unsigned char *data, size_t len) { if (!scr_crypto_hash_updatable(hash)) return NULL; diff --git a/tests/corpus/5768-crypto-hash-unknown.ts b/tests/corpus/5768-crypto-hash-unknown.ts new file mode 100644 index 0000000000..7948dc758d --- /dev/null +++ b/tests/corpus/5768-crypto-hash-unknown.ts @@ -0,0 +1,68 @@ +// Hash and Hmac values held in 'unknown' and 'any' slots: update(), +// digest() and Hash's copy() dispatch through the value, update() answers +// the same value, a cast back to Hash or Hmac keeps its identity, and the +// errors after digest() are Node's. An Hmac has no copy(). +import { createHash, createHmac, type Hash, type Hmac } from "node:crypto"; + +function code(run: () => unknown): string { + try { + const value = run(); + return typeof value === "string" ? `ok ${value.slice(0, 16)}` : `ok ${typeof value}`; + } catch (error) { + const e = error as Error & { code?: string }; + return `${e.name} ${e.code ?? ""} ${e.message}`; + } +} + +function hexOf(value: any): string { + return value.update("abc").digest("hex"); +} + +console.log("any param", hexOf(createHash("sha256"))); +console.log("any param hmac", hexOf(createHmac("sha256", "key"))); + +const hash = createHash("sha1"); +const held: unknown = hash; +console.log("typeof", typeof held, String(held), Object.prototype.toString.call(held)); +console.log("cast keeps identity", (held as Hash) === hash); +const chained: any = held; +console.log("update answers the value", chained.update("a") === chained); +console.log("typed digest after untyped update", hash.digest("hex")); +console.log("digest again", code(() => chained.digest("hex"))); +console.log("update after digest", code(() => chained.update("x"))); + +const values: unknown[] = [createHash("md5"), createHmac("sha512", "k"), createHash("sha256")]; +for (const value of values) { + const v = value as any; + v.update("one"); + v.update(Buffer.from("two")); + v.update("74687265", "hex"); + console.log("array", v.digest("base64")); +} + +const record: { label: string; hash: unknown } = { label: "rec", hash: createHash("sha384") }; +const fromRecord = record.hash as Hash; +fromRecord.update("record"); +console.log(record.label, fromRecord.digest("hex").slice(0, 32)); + +const original: any = createHash("sha256").update("prefix"); +const copy = original.copy(); +copy.update("-copy"); +original.update("-original"); +console.log("copy", copy.digest("hex").slice(0, 16), original.digest("hex").slice(0, 16)); +console.log("copy is a new value", copy === original); +const shake: any = createHash("shake256"); +shake.update("x"); +console.log("copy outputLength", shake.copy({ outputLength: 8 }).digest("hex")); +console.log("copy bad outputLength", code(() => shake.copy({ outputLength: -1 }))); + +const mac: unknown = createHmac("sha256", "secret"); +const macAny: any = mac; +console.log("hmac copy", typeof macAny.copy, macAny.copy === undefined); +console.log("hmac copy call", code(() => macAny.copy())); +console.log("hmac digest", (mac as Hmac).update("data").digest("hex").slice(0, 16)); +console.log("hmac digest again", JSON.stringify(macAny.digest("hex"))); + +console.log("missing member", macAny.nope, typeof chained.missing); +console.log("bad data", code(() => (createHash("sha256") as any).update(42))); +console.log("default digest", (createHash("md5") as any).update("z").digest().length); diff --git a/tests/corpus/5769-crypto-hash-unknown.cjs b/tests/corpus/5769-crypto-hash-unknown.cjs new file mode 100644 index 0000000000..90782757cb --- /dev/null +++ b/tests/corpus/5769-crypto-hash-unknown.cjs @@ -0,0 +1,77 @@ +// Hash and Hmac values passed through untyped JavaScript code: function +// parameters, object fields, arrays and Map values. update() answers the +// same value, digest() and copy() follow Node, and Hmac has no copy(). +const crypto = require("node:crypto"); + +function code(run) { + try { + const value = run(); + return typeof value === "string" ? `ok ${value.slice(0, 16)}` : `ok ${typeof value}`; + } catch (error) { + return `${error.name} ${error.code || ""} ${error.message}`; + } +} + +function digestAll(items, encoding) { + return items.map((item) => item.update("payload").digest(encoding)); +} + +console.log(digestAll([crypto.createHash("sha256"), crypto.createHmac("sha1", "k")], "hex")); + +const box = { hash: crypto.createHash("md5"), mac: crypto.createHmac("sha256", Buffer.from("key")) }; +box.hash.update("a").update("b"); +console.log("field", box.hash.digest("hex"), box.mac.update("c").digest("base64url")); + +const byName = new Map(); +byName.set("sha512", crypto.createHash("sha512")); +byName.set("hmac", crypto.createHmac("md5", "x")); +for (const [name, value] of byName) { + const same = value.update(name); + console.log("map", name, same === value, value.digest("hex").slice(0, 20)); +} + +function copyTwice(h) { + const a = h.copy(); + const b = h.copy(); + a.update("1"); + b.update("2"); + return [a.digest("hex").slice(0, 12), b.digest("hex").slice(0, 12), h.digest("hex").slice(0, 12)]; +} +console.log("copies", copyTwice(crypto.createHash("sha256").update("base"))); + +function describe(value) { + return [typeof value, String(value), Object.prototype.toString.call(value), value.notAMember]; +} +console.log("hash", describe(crypto.createHash("sha256"))); +console.log("hmac", describe(crypto.createHmac("sha256", "k"))); +const plainMac = [crypto.createHmac("sha256", "k")][0]; +console.log("hmac copy read", typeof plainMac.copy, plainMac.copy === undefined); + +const mac = crypto.createHmac("sha256", "k"); +const viaObject = { mac }; +console.log("hmac copy", code(() => viaObject.mac.copy())); +console.log("digest", viaObject.mac.digest("hex").slice(0, 16), JSON.stringify(viaObject.mac.digest("hex"))); +console.log("update after digest", code(() => viaObject.mac.update("x"))); + +const used = [crypto.createHash("sha1")]; +used[0].digest(); +console.log("digest twice", code(() => used[0].digest("hex"))); +console.log("copy after digest", code(() => used[0].copy())); +console.log("update bad", code(() => [crypto.createHash("sha1")][0].update(null))); + +function methods(h, m) { + return [h.update === m.update, h.digest === m.digest, typeof h.copy, typeof m.copy, + h.update.name, h.update.length, h.digest.name, h.digest.length, h.copy.length]; +} +console.log("methods", methods(crypto.createHash("sha256"), crypto.createHmac("sha1", "k"))); +function viaCall(h) { + const update = h.update; + const digest = h.digest; + return [update.call(h, "abc") === h, digest.call(h, "hex").slice(0, 16)]; +} +console.log("call", viaCall(crypto.createHash("sha256")), viaCall(crypto.createHmac("sha256", "k"))); +function sharedUpdate(h, m) { + h.update.call(m, "shared"); + return m.digest("hex").slice(0, 16); +} +console.log("shared update", sharedUpdate(crypto.createHash("md5"), crypto.createHmac("md5", "k"))); diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index 8ffc03b9ad..2825d99ac9 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -1841,6 +1841,8 @@ "5648-dns-lookup-forms.ts": 2992, "5649-dns-service-servers.ts": 2712, "5650-dns-lookup-errors-untyped.cjs": 2822, + "5768-crypto-hash-unknown.ts": 3470, + "5769-crypto-hash-unknown.cjs": 3490, "5833-url-search-params-iterators.ts": 2678, "5834-url-search-params-iterators-dynamic.mjs": 2696, "5836-url-search-params-tuple-pairs.ts": 2118, @@ -4482,6 +4484,8 @@ "5648-dns-lookup-forms.ts": 3416, "5649-dns-service-servers.ts": 4916, "5650-dns-lookup-errors-untyped.cjs": 3889, + "5768-crypto-hash-unknown.ts": 3960, + "5769-crypto-hash-unknown.cjs": 4090, "5833-url-search-params-iterators.ts": 5339, "5834-url-search-params-iterators-dynamic.mjs": 3797, "5836-url-search-params-tuple-pairs.ts": 3193, diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts index 28daa88d34..53f09192dd 100644 --- a/tests/harness/errors.test.ts +++ b/tests/harness/errors.test.ts @@ -928,6 +928,43 @@ hash.end("abc"); }); }); +// A Hash or Hmac held in an 'unknown' slot answers update(), digest() and +// Hash's copy(). Its other members throw a coded SC2020 error, and so do +// those methods called with another receiver. +describe("Hash and Hmac members through 'unknown'", () => { + test("throw SC2020 other than update, digest and copy", async () => { + const r = await compileAndRun( + "hash-unknown-members", + `const crypto = require("node:crypto"); +function attempt(value, run) { + try { + run(value); + } catch (error) { + console.log(error.name, error.code, error.message); + } +} +const hash = [crypto.createHash("sha256")][0]; +attempt(hash, (h) => h.on("data", () => {})); +attempt(hash, (h) => h.readable); +attempt([crypto.createHmac("sha256", "k")][0], (m) => m.write("x")); +attempt(hash, (h) => h.digest.call({})); +console.log(hash.update("abc").digest("hex").slice(0, 8)); +`, + "cjs", + ); + expect(r.exitCode).toBe(0); + expect(r.stdout).toBe( + "Error SC2020 Hash members other than update(), digest() and copy() have no lowering on an 'unknown' value; cast the value to Hash first\n".repeat( + 2, + ) + + "Error SC2020 Hmac members other than update() and digest() have no lowering on an 'unknown' value; cast the value to Hmac first\n" + + "Error SC2020 Hash and Hmac methods called with a receiver other than their own Hash or Hmac have no lowering\n" + + "ba7816bf\n", + ); + expect(r.stderr).toBe(""); + }); +}); + // Node 24.22 skips the chacha20-poly1305 tag check when neither // setAuthTag() nor authTagLength was used, and returns unverified // plaintext. scriptc keeps Node 24.15's behavior: final() fails. From 14372c827483a30b94968fbe8e4303bbfc26486c Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 18:09:08 -0500 Subject: [PATCH 2/2] Build "is not a function" errors for Hash, Hmac and KeyObject handles at any length The Hash/Hmac and KeyObject handle dispatchers formatted " is not a function" into a 160-byte stack buffer and passed snprintf's return value as the message length. A callee text longer than about 140 bytes made that length exceed the buffer, so the error copied bytes past it: the message ended in stack contents, and sanitized builds aborted. Both dispatchers now build the message in a growable buffer, as the shared dynamic-call path does, so the TypeError carries the whole callee text, as in Node. Corpus 5774 calls a missing method on a Hash, an Hmac and a KeyObject through callee texts longer than 160 bytes, and through a short one, and corpus-costs.json records its plain and sanitized costs. --- .../5774-crypto-handle-long-callee.cjs.json | 6 +++ packages/runtime/src/scr_lib.c | 18 +++++--- .../corpus/5774-crypto-handle-long-callee.cjs | 46 +++++++++++++++++++ tests/harness/corpus-costs.json | 2 + 4 files changed, 66 insertions(+), 6 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5774-crypto-handle-long-callee.cjs.json create mode 100644 tests/corpus/5774-crypto-handle-long-callee.cjs diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5774-crypto-handle-long-callee.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5774-crypto-handle-long-callee.cjs.json new file mode 100644 index 0000000000..d2f21dbe28 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5774-crypto-handle-long-callee.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5774-crypto-handle-long-callee.cjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index b0a269a929..90601a0db6 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -8076,6 +8076,16 @@ void scr_crypto_hash_release(ScrCryptoHash *hash) { void *scr_crypto_hash_retain_v(void *p) { return scr_crypto_hash_retain(p); } void scr_crypto_hash_release_v(void *p) { scr_crypto_hash_release(p); } +/* Node's TypeError for calling a missing member: " is not a + * function", where `what` is the callee's source text, of any length. */ +static void scr_lib_throw_not_a_function(const char *what) { + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, what); + scr_jb_puts(&b, " is not a function"); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b)); +} + /* Hash and Hmac values seen as 'unknown' are native handles. update(), * digest() and Hash's copy() dispatch through the handle, called or read * as values; the other members Node defines on a Hash or Hmac (the @@ -8213,9 +8223,7 @@ static ScrDyn *scr_crypto_hash_dyn_invoke(void *h, ScrDyn *self, const char *met scr_crypto_hash_dyn_unsupported(hash); return NULL; } - char message[160]; - int n = snprintf(message, sizeof message, "%s is not a function", what); - scr_throw_error_msg(SCR_ERR_TYPE, message, (size_t)n); + scr_lib_throw_not_a_function(what); return NULL; } @@ -9501,9 +9509,7 @@ static ScrDyn *scr_key_object_invoke(void *h, ScrDyn *self, const char *method, scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); return NULL; } - char message[160]; - int n = snprintf(message, sizeof message, "%s is not a function", what); - scr_throw_error_msg(SCR_ERR_TYPE, message, (size_t)n); + scr_lib_throw_not_a_function(what); return NULL; } diff --git a/tests/corpus/5774-crypto-handle-long-callee.cjs b/tests/corpus/5774-crypto-handle-long-callee.cjs new file mode 100644 index 0000000000..89a5fbbbf9 --- /dev/null +++ b/tests/corpus/5774-crypto-handle-long-callee.cjs @@ -0,0 +1,46 @@ +// A missing method called on a Hash, Hmac or KeyObject held in an untyped +// value throws Node's TypeError with the whole callee text, also when the +// text is longer than a fixed-size message buffer. +const { createHash, createHmac, createSecretKey } = require("node:crypto"); + +const holder = { + aHashValueStoredUnderAPropertyNameLongEnoughThatTheCalleeTextOfTheMissingMethodRunsPastOneHundredAndSixtyBytes: + createHash("sha256"), + anHmacValueStoredUnderAPropertyNameLongEnoughThatTheCalleeTextOfTheMissingMethodRunsPastOneHundredAndSixtyBytes: + createHmac("sha256", "key"), + aSecretKeyObjectStoredUnderAPropertyNameLongEnoughThatTheCalleeTextOfTheMissingMethodRunsPastOneHundredAndSixty: + createSecretKey(Buffer.alloc(16, 1)), +}; + +function report(label, run) { + try { + run(); + console.log(label, "did not throw"); + } catch (e) { + console.log(label, e instanceof TypeError, e.name, e.message.length); + console.log(e.message); + } +} + +report("hash", () => + holder.aHashValueStoredUnderAPropertyNameLongEnoughThatTheCalleeTextOfTheMissingMethodRunsPastOneHundredAndSixtyBytes.missingMethodOnTheHashValue(), +); +report("hmac", () => + holder.anHmacValueStoredUnderAPropertyNameLongEnoughThatTheCalleeTextOfTheMissingMethodRunsPastOneHundredAndSixtyBytes.missingMethodOnTheHmacValue(), +); +report("key", () => + holder.aSecretKeyObjectStoredUnderAPropertyNameLongEnoughThatTheCalleeTextOfTheMissingMethodRunsPastOneHundredAndSixty.missingMethodOnTheKeyObject(), +); + +// A short callee text through an untyped parameter, for comparison. +function callMissing(value) { + return value.nope(); +} +report("short", () => + callMissing(holder.aHashValueStoredUnderAPropertyNameLongEnoughThatTheCalleeTextOfTheMissingMethodRunsPastOneHundredAndSixtyBytes), +); +console.log( + holder.aHashValueStoredUnderAPropertyNameLongEnoughThatTheCalleeTextOfTheMissingMethodRunsPastOneHundredAndSixtyBytes + .update("abc") + .digest("hex"), +); diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index a6bb392cb6..39dd56b8bc 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -2041,6 +2041,7 @@ "5771-guarded-downcasts.ts": 3555, "5772-guarded-catch-narrows.ts": 2601, "5773-stale-narrowing-inherited.ts": 2486, + "5774-crypto-handle-long-callee.cjs": 1820, "5776-power-of-two-remainder-integer-path.ts": 3263, "5780-readline-history-options.ts": 2909, "5781-readline-history-options.cjs": 3288, @@ -5066,6 +5067,7 @@ "5771-guarded-downcasts.ts": 3443, "5772-guarded-catch-narrows.ts": 3695, "5773-stale-narrowing-inherited.ts": 3758, + "5774-crypto-handle-long-callee.cjs": 2420, "5776-power-of-two-remainder-integer-path.ts": 3584, "5780-readline-history-options.ts": 3008, "5781-readline-history-options.cjs": 5419,