Skip to content

webcrypto: ML-DSA + ML-KEM, ChaCha20-Poly1305, raw-secret/raw-public, toCryptoKey, v26 SubtleCrypto surface (+10 tests, webcrypto 58%→76%) - #34838

Merged
dylan-conway merged 49 commits into
mainfrom
claude/webcrypto-chacha20-raw-secret
Jul 24, 2026
Merged

dylan-conway merged 49 commits into
mainfrom
claude/webcrypto-chacha20-raw-secret

Conversation

@cirospaciari

@cirospaciari cirospaciari commented Jul 20, 2026 •

Copy link
Copy Markdown
Member

Implements node v26's WebCrypto surface additions, verified case-by-case against the node v26.3.0 binary. Adds 10 vendored upstream tests. Based on main. Stacked on #34431.

What changed

  • ML-DSA (44/65/87) and ML-KEM (512/768/1024) over BoringSSL, including encapsulateBits/encapsulateKey/decapsulateBits/decapsulateKey (per-path result enumeration order matches node, which itself differs between the two), spki/pkcs8/jwk/raw-* import/export, and structuredClone of AKP keys guarded with DataCloneError instead of a release assert.
  • ChaCha20-Poly1305 over EVP_aead_chacha20_poly1305, with node's 12-byte nonce validation and raw-secret-only raw import (node rejects user-facing raw for ChaCha; deriveKey's inner import uses raw-secret accordingly).
  • raw-secret/raw-public key formats, accepted as aliases where node aliases them (all algorithms pre-dispatch — restricting to EC/OKP would break HKDF parity); shared helpers at all four import/export/wrap/unwrap sites.
  • KeyObject.prototype.toCryptoKey, SubtleCrypto.supports(), and getPublicKey coverage across RSA/EC/OKP/AKP.
  • Error-contract parity swept against node: exact DataError/TypeError classes and messages for JWK validation (use/key_ops/ext check order probed on doubly-invalid input, duplicate key_ops rejected via one shared helper), ERR_INVALID_ARG_TYPE for non-buffer keyData with zero getter invocations (format-first converter dispatch, no exception clearing), wrapKey's extractable check ordered before format aliasing, and the KEM-usage rejections for RSA variants.
  • JsonWebKey.kty is no longer a required IDL member (WebCrypto does not mark it required; node rejects a non-JWK object with DataError where Bun threw TypeError).

Verification

web-crypto suite 87+ pass / 0 fail with per-claim probes captured from live node; the four vendored ML tests plus test-webcrypto-export-import* pass; regression tests proven failing on the unfixed build for each behavior fix (getter counts, clone guard, coercion counts).

Compat impact (upstream node v26.3.0 test files vendored, in-tree = passing)

  • webcrypto: 58% → 76% (29 → 38 of 50)

cirospaciari and others added 22 commits July 16, 2026 17:56
Bun's WebCrypto reported the right failures with the wrong names, codes and
ordering, so code that works on Bun can behave differently on Node. Each
behavior below was checked against a real Node v26.3.0 build before changing
it, and the vendored Node webcrypto tests are synced to that version.

CryptoKey.usages / JWK key_ops ordering. usages() walked the usage bitmap
alphabetically. The KeyUsage enum in the WebCrypto spec orders them encrypt,
decrypt, sign, verify, deriveKey, deriveBits, wrapKey, unwrapKey, which is what
Node emits. Most visible on ECDH: Node reports ["deriveKey","deriveBits"] and
Bun reported the reverse. key_ops is built from usages(), so JWK export was
wrong too.

SubtleCrypto error messages. Aligned the operation guards with Node's wording:
"CryptoKey doesn't match AlgorithmIdentifier" -> "Key algorithm mismatch";
"CryptoKey doesn't support encryption" -> "Unable to use this key to encrypt"
(likewise decrypt/sign/verify/wrapKey/unwrapKey); the two derive cases ->
"baseKey does not have deriveBits/deriveKey usage"; "The CryptoKey is
nonextractable" -> "key is not extractable". deriveBits/deriveKey also checked
the algorithm match before the usage; Node checks usage first, and on
doubly-invalid input the order decides which error wins.

Missing vs ill-typed dictionary members. throwRequiredMemberTypeError only
fires when a required member is absent, which Node reports as
ERR_MISSING_OPTION rather than ERR_INVALID_ARG_TYPE; that holds for every
required member, including a params dictionary with no name at all. A member
that was present but ill-typed threw a bare "Type error" with no code, so
deriveBits({name:'ECDH', public:{}}) was indistinguishable from an internal
failure; it now reports ERR_INVALID_ARG_TYPE like Node. Both helpers are used
only by the webcrypto bindings.

crypto.getRandomValues. Float32Array, Float64Array, DataView, ArrayBuffer and
SharedArrayBuffer were all filled with random bytes. The spec allows only
integer-typed views; everything else must raise TypeMismatchError, which Node
does. The message already existed, only the check was missing.

HKDF / PBKDF2 / ECDH derived-length errors. A null length and a length that is
not a multiple of 8 both produced an OperationError with an empty message,
surfacing as the generic "The operation failed for an operation-specific
reason". Node distinguishes "length cannot be null" from "length must be a
multiple of 8", and ECDH/X25519 say "derived bit length is too small".

Illegal constructor. Interfaces that cannot be constructed at all routed both
call and construct through the "called without new" path, so new CryptoKey()
answered "Use `new CryptoKey(...)` instead of `CryptoKey(...)`" — advice it had
already followed. Both paths now report "Illegal constructor", matching Node,
browsers, and the wording the Rust helper's own documentation already claimed
to produce. MessagePort opts into ERR_CONSTRUCT_CALL_INVALID and keeps Node's
message for that code, "Constructor cannot be called".

Tests: the vendored Node webcrypto tests are byte-identical to upstream, and
only tests that pass are added — tests needing algorithms Bun does not
implement are left alone rather than quarantined. test-webcrypto-random.js had
been edited to assert Bun's old behavior ("These types are allowed in Bun");
those assertions are restored, leaving only the 65536-byte quota commented out,
since that needs a QuotaExceededError global Bun does not have yet.
…ard hash aliases

Second pass on Node v26.3.0 webcrypto compatibility, taking the vendored
upstream suite from 22 to 30 passing files. As before, every behavior was
checked against a real Node v26.3.0 build first.

Non-standard hash aliases removed. The registry registered SHA1/SHA224/SHA256/
SHA384/SHA512 as alternative names beside the hyphenated spec names. It already
matches case-insensitively as the spec requires, so the alias table was an extra
deviation: `hash: 'SHA256'` was accepted where Node and browsers throw
NotSupportedError, meaning code written against Bun could fail elsewhere. Nothing
in the repo relied on them (`Bun.CryptoHasher("SHA256")` and node:crypto's
`createHash` are separate surfaces). Dropping them also lets the registry
re-assert that each algorithm registers exactly once.

Unrecognized algorithm names. Only `digest` said "Unrecognized algorithm name",
via a hardcoded string that overrode every message. The other operations reported
the DOMException default, "The operation is not supported.". The message now comes
from the layer that owns the invariant — every failing lookup in
normalizeCryptoAlgorithmParameters — so all operations agree with Node, including
a recognized name used for the wrong operation (`digest('AES-GCM')`).

Import errors. Every import rejection carried the right error name but a generic
message: JWK "use"/"alg"/"crv" mismatches, unsupported key usages, and empty
usages on a private or secret key all collapsed into "Data provided to an
operation does not meet requirements". They now carry Node's text.

"Invalid key type" vs "Invalid keyData". Importing an EC key as RSA reported the
same generic DataError as random bytes. RSA/EC/OKP conflated "the parser found a
well-formed key of another type" with "this is not a key", because both were one
condition. They are split, and the distinction reaches the caller through an
optional out-param. OKP hand-parses its DER, and a byte compare cannot tell those
two cases apart, so it confirms with the real parser before claiming a type
mismatch — otherwise garbage would be reported as a wrong-typed key.

Ed25519 JWK "alg". Export omitted it and import never checked it, so a JWK with
`alg: 'foo'` was accepted. RFC 8037 gives the Edwards curves an "alg" of the curve
name or "EdDSA"; the montgomery curves have none, so X25519 still omits it.

ECDH derive errors, HKDF info. ECDH reported nothing for a curve or algorithm
mismatch. HKDF accepted an `info` longer than the 1024 bytes Node allows.

util.inspect(CryptoKey) printed `CryptoKey {}`. The attributes are prototype
accessors, so the key has no own enumerable properties. It now prints the four
slots like Node, reading the internal state directly rather than through the
getters, whose cached objects user code can mutate.

Tests: 17 more upstream files (30 of Node's 53), all byte-identical to upstream.
Three still test nothing under BoringSSL (`common.skip` on `requires OpenSSL >= 3`).
The rest need algorithms or APIs Bun lacks — ML-KEM, ML-DSA, TurboSHAKE,
ChaCha20-Poly1305, AES-OCB, Ed448/X448, subtle.getPublicKey, SubtleCrypto.supports,
KeyObject.prototype.toCryptoKey, QuotaExceededError — or Node's internal/* module
registry, which is not portable.
body.test.ts built its fixtures with `crypto.getRandomValues(new bufferType(n))`
over a list that includes ArrayBuffer, SharedArrayBuffer and the Float views.
getRandomValues takes integer-typed views only — Node throws TypeMismatchError
for all five — so the fixtures were relying on Bun's older, laxer check. Fill
through a Uint8Array view over the same bytes instead, which keeps every
bufferType in the matrix.
…memory

`random_get` called `crypto.getRandomValues(this.memory.buffer, bufPtr, bufLen)`.
getRandomValues takes a single integer-typed view and ignores any further
arguments, so bufPtr and bufLen were dropped on the floor and the whole wasm
linear memory was overwritten with random bytes on every call — 65241 of 65536
bytes outside the requested 16-byte window, in the added test's terms. Passing a
Uint8Array view over exactly the requested range fixes that, and also keeps the
syscall working now that getRandomValues rejects a plain ArrayBuffer: every wasm
guest that reaches random_get (Rust getrandom, Go crypto/rand, WASI-libc
getentropy) would otherwise trap.
WASI preview1 defines random_get as returning errno (0 on success). The
handler returned bufLen, so a guest asking for 16 bytes would see errno 16
back. Pre-existing on main, but since this PR already touches the function to
fix the windowing bug it makes sense to complete the fix here. The test now
asserts WASI_ESUCCESS and the tautological expect(WASI_ESUCCESS).toBe(0) is
dropped.
…ters

normalizeCryptoAlgorithmParameters declares a JSC ThrowScope, so every
caller must perform an exception check before the next exception-scope
use. digest was the only SubtleCrypto method that did; generateKey,
deriveKey, deriveBits, and importKey had RETURN_IF_EXCEPTION after the
early return that rejects the promise, and encrypt, sign, verify,
wrapKey, and unwrapKey had no scope at all. With
BUN_JSC_validateExceptionChecks=1, 25 of 36 crypto.subtle paths
aborted, including the decrypt, wrapKey, and unwrapKey success paths.

Give every method the digest shape: RETURN_IF_EXCEPTION right after the
normalize call, and RELEASE_AND_RETURN on the trailing algorithm call,
whose callbacks can reach DeferredPromise synchronously. The two
wrapped-key callback lambdas get the same treatment for their
throw-scoped toJS<IDLDictionary<JsonWebKey>>, JSONStringify, and
JSONParse calls.

The reorder in wrapKey also fixes a user-reachable debug assertion: a
throwing `name` getter on the wrap algorithm makes the first normalize
return ExistingExceptionError, which tripped the ASSERT at
SubtleCrypto.cpp:1144 and then ran the second normalize with a pending
JS exception.

No user-visible change on release builds: the fixture's 39-case
transcript is byte-identical before and after.
…lback

m_pendingPromises is a HashMap<DeferredPromise*, Ref<DeferredPromise>> and
get() peeks the value as a raw pointer, so removing the map entry before
rejecting frees the DeferredPromise the reject is about to use. The
unwrapKey callback already holds a RefPtr across this pattern; match it.

Exercise the path with a fixture case that makes wrapKey("jwk")'s internal
JSON.stringify throw via an inherited Object.prototype.toJSON. Without the
exception check this PR adds after JSONStringify, that input also leaves
the promise unsettled forever on release builds: the pending exception
survives into a later DeferredPromise::reject, which reports it as
uncaught and returns without settling.
The keyTypeMismatch out-param was only set when the 4-byte OID prefix
mismatched, but Ed25519/X25519/Ed448/X448 all share that prefix and differ
only in the fifth byte. Importing an Ed25519 SPKI as X25519 fell through the
switch arm without setting the flag and reported the generic 'Invalid keyData'
instead of Node's 'Invalid key type'. Cover both switch arms in importSpki and
importPkcs8 via a shared lambda, still gated on the real DER parser so garbage
with a matching prefix is not misreported.

Also: add Float16Array to the getRandomValues rejection matrix (the old test
asserted it was filled; nothing covered the new rejection), and trim the
Bun-authored divergence comment in test-webcrypto-random.js to 3 lines.
… OKP-as-EC as key-type mismatch

Same-class follow-ups to the error-message work:

- importKey's 'Usages cannot be empty when importing a private/secret key.'
  now also appears in deriveKey's and unwrapKey's inner import callbacks, which
  share the identical predicate. generateKey carries Node's 'Usages cannot be
  empty when creating a key.' for both the single-key and key-pair arms.

- CryptoKeyEC::platformImportSpki required a two-element AlgorithmIdentifier
  before checking the OID, so an OKP SPKI (one-element AlgId per RFC 8410)
  bailed without setting keyTypeMismatch and reported 'Invalid keyData'
  instead of 'Invalid key type'. Set the flag at that guard too, gated on
  d2i_PUBKEY succeeding so garbage is not misreported. importPkcs8 already
  checks EVP_PKEY_base_id and was unaffected.
ECDH's deriveBits got Node's 'key algorithm mismatch' and 'Named curve
mismatch', but the byte-identical checks in X25519's parallel were left as the
empty-message InvalidAccessError. Line 86 is reachable today
(deriveBits({name:'X25519', public: ecdhKey}, x25519Private)), and the vendored
cfrg tests assert this exact message behind a keys.X448 guard that is skipped
under BoringSSL, so it would fail the moment X448 lands.

New test covers both directions against the ECDH sibling.
…JWK alg straggler

The four RSA importKey implementations have the same usage-guard shape that
ECDSA/ECDH/Ed25519/X25519 got Node's 'Unsupported key usage for a <name> key'
for; all four RSA files were already touched here to thread keyTypeMismatch and
add the JWK 'use'/'alg' messages, so the usage guards in the same functions get
the same treatment. Also: RSAES-PKCS1-v1_5's JWK 'alg' mismatch was the only one
of the four still at the empty message; its three siblings got the message in
this PR.

generateKey's usage checks are left alone: the EC/OKP files left those at the
empty message too, and AES/HMAC importKey is in files this PR does not touch.
Node's ec.js and cfrg.js build the message from a single template that uses
'an'; the RSA siblings added in 9cb0754 matched that, but the EC/OKP
parallels said 'a ECDSA' etc. The vendored tests only assert on the
/Unsupported key usage/ regex so both spellings pass; this aligns the exact
text with Node and the RSA siblings.
Implements the ChaCha20-Poly1305 WebCrypto algorithm over BoringSSL's
EVP_aead_chacha20_poly1305, with an AeadParams dictionary derived from the
identical AesGcmParams shape, and adds node's `raw-secret` import/export format.
`raw-secret` is also accepted as a `raw` alias for AES-* and HMAC, matching
node. CryptoKeyRaw gains extractability so it can back a name-only key
algorithm, as node's does.

Other digests and ciphers in this area were ruled out first by grepping the
vendored BoringSSL: TurboSHAKE, KangarooTwelve, AES-OCB and Argon2 have no
symbols there, so those tests are blocked on the primitive rather than on
binding work.

JsonWebKey.kty is no longer a required IDL member. WebCrypto's dictionary does
not mark it required, and node rejects a non-JWK object with DataError where bun
threw a TypeError; both now throw DataError. One bun-owned assertion in
test/js/web/crypto/web-crypto.test.ts is updated to the node-parity error while
keeping its settle and leak guard intact. No vendored node test was edited.

Adds test-webcrypto-encrypt-decrypt-chacha20-poly1305 and
test-webcrypto-aead-decrypt-detached-buffer from Node v26.3.0, verbatim, with
their fixture.
@robobun

robobun commented Jul 20, 2026 •

Copy link
Copy Markdown
Collaborator
Updated 5:10 PM PT - Jul 23rd, 2026

✅ @cirospaciari, your commit d8947814ee5207498eea353d11cb99abaaa25003 passed in Build #78963! 🎉


🧪   To try this PR locally:

bunx bun-pr 34838

That installs a local version of the PR into your bun-34838 executable, so you can run:

bun-34838 --bun

@github-actions

Copy link
Copy Markdown
Contributor

Found 2 issues this PR may fix:

  1. Support ChaCha20-Poly1305 in node:crypto #8072 - Requests ChaCha20-Poly1305 cipher support in node:crypto; this PR implements it via WebCrypto/BoringSSL
  2. Proposal: Implement "Modern Algorithms in the Web Cryptography API" (WICG specification) #29218 - Proposes implementing WICG modern WebCrypto algorithms; ChaCha20-Poly1305 is listed as priority Copy source lines when generating error messages #3 and is directly addressed by this PR

If this is helpful, copy the block below into the PR description to auto-close these issues on merge.

Fixes #8072
Fixes #29218

🤖 Generated with Claude Code

….3.0

Passes on this branch as-is; copied verbatim from upstream. It forges all four
CryptoKey prototype getters and checks that util.inspect, exportKey('jwk'),
KeyObject.from, createHmac and crypto.sign/verify all read the native slots
rather than the forged ones, and that Object.prototype pollution of hash /
publicExponent does not leak into a generated AES-GCM key.

Verified 3/3 and tamper-checked, with the same tamper run against the node
v26.3.0 binary as a control.

No-Verification-Needed: test-only diff, no runtime surface to drive
@cirospaciari cirospaciari changed the title webcrypto: ChaCha20-Poly1305 and the raw-secret key format (+2 tests) webcrypto: ChaCha20-Poly1305, raw-secret key format, hidden-slots test (+3 tests) Jul 20, 2026
@cirospaciari
cirospaciari marked this pull request as ready for review July 20, 2026 21:34
Comment thread src/jsc/bindings/webcore/SerializedScriptValue.cpp
Comment thread src/jsc/bindings/webcrypto/CryptoAlgorithmChaCha20Poly1305.cpp
Comment thread src/jsc/bindings/webcrypto/CryptoKeyFormat.h Outdated
Comment thread src/jsc/bindings/webcrypto/CryptoAlgorithmChaCha20Poly1305.cpp
cirospaciari and others added 2 commits July 23, 2026 09:07
Node routes every algorithm through the same validateKeyOps, so the
duplicate scan the AKP arm already had applies to the oct algorithms
too. Extract it as hasDuplicateJwkKeyOps next to the JsonWebKey struct
and call it from all three JWK pre-validation blocks.

[allow size]
Comment thread src/jsc/bindings/webcrypto/JSSubtleCrypto.cpp Outdated
Comment thread src/jsc/bindings/webcrypto/JSJsonWebKey.cpp
Comment thread src/jsc/bindings/webcrypto/CryptoAlgorithmRSAES_PKCS1_v1_5.cpp
Buffer formats no longer run the JWK dictionary conversion, so a
poisoned member getter cannot fire and no pending exception is
cleared before reporting node's type error; jwk conversion errors
still propagate. Mirror the optional kty in convertDictionaryToJS,
and collapse the RSAES KEM-usage matrix to the single deprecation
assertion its gate allows.

[allow size]

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No new findings on this pass — all prior rounds' items are addressed and resolved. That said, this is a ~460k-char diff across 85 files adding six new crypto algorithms (ML-DSA-44/65/87, ML-KEM-768/1024, ChaCha20-Poly1305), a new CryptoKeyAKP key class, four new KeyUsage values, three new key formats, and the v26 SubtleCrypto surface (supports/getPublicKey/encap/decap) plus KeyObject.prototype.toCryptoKey, so it warrants a human look.

What was reviewed across passes: the reachable RELEASE_ASSERT on structuredClone of ChaCha/AKP keys (now DataCloneError); deriveKey/encap/decap → inner-importKey format routing for ChaCha/HKDF/PBKDF2; the raw-secret/raw-public/raw-seed aliasing sweep across every algorithm's import/export switch and its ordering vs. the extractable check in wrapKey; the kty-optional IDL change and both mirrored converters; importKey's per-format keyData converter (no tryClearException, no getter reads on buffer formats); the shared hasDuplicateJwkKeyOps for AKP/ChaCha/HMAC; and the KEM-usage bitmap sweep across every usagesAreInvalid predicate.

Extended reasoning...

Overview

This PR implements post-quantum ML-DSA (44/65/87) and ML-KEM (768/1024) plus ChaCha20-Poly1305 as WebCrypto algorithms over BoringSSL's EVP layer, backed by a new CryptoKeyAKP key class with JWK kty:"AKP", seed-only PKCS#8, and raw-public/raw-seed formats. It also adds Node's v26 SubtleCrypto surface (SubtleCrypto.supports() static, getPublicKey(), encapsulateBits/Key, decapsulateBits/Key), KeyObject.prototype.toCryptoKey, four new KEM CryptoKeyUsage values, and the raw-secret key format. Cross-cutting: JsonWebKey.kty becomes optional in the IDL, Crypto.prototype.subtle moves to the prototype with an ERR_INVALID_THIS brand check, and structured clone of the new key types throws DataCloneError (Node clones them; documented as a follow-up). Nearly every existing CryptoAlgorithm*.cpp gets touched for the new formats and KEM-usage exclusions.

Security risks

This is core crypto: new signature (ML-DSA) and KEM (ML-KEM) primitives wired to BoringSSL, new AEAD cipher (ChaCha20-Poly1305), new key import/export formats, and a bridge from node:crypto KeyObject into WebCrypto CryptoKey. The BoringSSL calls look correctly bounded (two-call length-then-fill patterns, ERR_clear_error on failure paths, RAII EvpPKeyPtr/EvpPKeyCtxPtr), JWK pub is verified against the seed-derived public key on private-JWK import, and all length/tag/nonce validation runs before dispatch — but a human should confirm the ML-DSA context-string handling, the seed-only PKCS#8 length table, and that SubtleCrypto.supports()'s tryClearException inside the normalize lambda is scoped to converter noise only.

Level of scrutiny

High. New user-facing crypto API surface, new key class, security-sensitive C++ over BoringSSL, and architectural additions (encapsulate/decapsulate as new CryptoAlgorithm virtuals, CryptoKeyClass::AKP). The structured-clone DataCloneError compromise and the subtle prototype-getter relocation in ZigGlobalObject are design decisions worth a maintainer's eye.

Other factors

The PR has been through many automated review rounds; every raised finding (including the original process-aborting structuredClone crash, the deriveKey format mismatch, the tryClearException swallowing user throws, and several sibling-site sweeps) was addressed with live-Node verification and regression tests. Eleven vendored Node tests are added byte-identical to upstream, and the description reports clean regression runs including BUN_JSC_validateExceptionChecks=1. The remaining scale and security sensitivity are what put this beyond auto-approval.

@cirospaciari cirospaciari changed the title webcrypto: ML-DSA + ML-KEM algorithms, v26 SubtleCrypto surface, toCryptoKey, ChaCha20-Poly1305, raw-secret (+11 tests) webcrypto: ML-DSA + ML-KEM, ChaCha20-Poly1305, raw-secret/raw-public, toCryptoKey, v26 SubtleCrypto surface (+10 tests) Jul 23, 2026
Node clones these key types; Bun rejected them with a deliberate
DataCloneError guard. Key serialization format version 2 adds:

- a CryptoKeyClassSubtag::AKP arm — public keys serialize as raw public
  bytes, private keys as PKCS#8 (which embeds the FIPS 203/204 seed, so
  raw-seed exportability survives the round trip like in Node)
- the four KEM usage tags, replacing the countUsages desync assert
- identifier tags for ChaCha20-Poly1305 and the five ML algorithms
  (ChaCha rides the existing Raw key-class arm)
- readRawKey now honors the stored extractable flag; HKDF/PBKDF2 keys
  always stored false, so v1 payloads deserialize unchanged

Also from the same review batch:

- undersized AEAD decrypt inputs reject with the default OperationError
  message like Node (ChaCha20-Poly1305 and the pre-existing AES-GCM
  site; nothing asserted the old text)
- BoringSSL error-queue hygiene on every new failure path (ChaCha
  seal/open, ML-KEM ctx/init, ML-DSA sign/verify init, AKP generatePair
  and exports): clear the queue unless the caller attaches it as a
  cause, per the importAkpKey convention
- exportRawSeed shrinks its buffer to the reported length, matching
  rawPublicKeyBytes

Tests: clone round-trips (material/algorithm/usages/extractable) for
ChaCha and both ML halves, extractable=false preservation, a Worker
postMessage delivery test, and the decrypt message; all fail on the
unfixed build. Vendored webcrypto ML/ChaCha/clone-transfer suites pass.

[allow size]
Comment thread src/jsc/bindings/webcore/SerializedScriptValue.cpp
Comment thread test/js/web/crypto/web-crypto.test.ts Outdated
… key

readAKPKey's importRawPublic/importPkcs8 leave the parse error in the
queue on failure and nothing attaches it as a cause, matching the
convention the rest of the clone change follows. Also await the two
rejection assertions added with the clone tests so they can actually
fail, and pin the non-extractable export message.

[allow size]
@cirospaciari cirospaciari changed the title webcrypto: ML-DSA + ML-KEM, ChaCha20-Poly1305, raw-secret/raw-public, toCryptoKey, v26 SubtleCrypto surface (+10 tests) webcrypto: ML-DSA + ML-KEM, ChaCha20-Poly1305, raw-secret/raw-public, toCryptoKey, v26 SubtleCrypto surface (+10 tests, webcrypto 58%→76%) Jul 23, 2026
Comment thread src/jsc/bindings/webcrypto/CryptoAlgorithmAES_GCM.cpp
The AES-GCM arm of the message change had no test; extend the
ChaCha20-Poly1305 case to a matrix (verified generic in node v26.3.0).

[allow size]

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No new findings on d894781. This is a large, security-sensitive crypto PR (85 files: ML-DSA/ML-KEM over BoringSSL, ChaCha20-Poly1305, new SubtleCrypto surface, structured-clone format bump, Crypto.prototype.subtle brand check), so it warrants a human sign-off.

What was reviewed across the earlier rounds (all resolved):

  • structuredClone of ChaCha20-Poly1305/AKP keys — reachable RELEASE_ASSERT, extractable round-trip, BoringSSL error-queue hygiene in readAKPKey.
  • importKey keyData converter dispatch — format-first branch, no clearException, zero getter invocations on buffer formats; kty-optional consistency across both JWK converters.
  • Format aliasing at all four import/export/wrap/unwrap sites; wrapKey extractable-check ordering; deriveKey/encapsulateKey inner-import format for ChaCha/HKDF/PBKDF2.
  • JWK validation ordering and duplicate key_ops for HMAC/ChaCha/AKP; HMAC's dead importJwk deleted; test vacuity (un-awaited .rejects, RSAES parameterization, AES-GCM undersized-decrypt message).
Extended reasoning...

Overview

This PR implements Node v26's WebCrypto surface across 85 files: ML-DSA-44/65/87 and ML-KEM-768/1024 (new CryptoKeyAKP class over BoringSSL EVP), ChaCha20-Poly1305 (EVP_aead_chacha20_poly1305), the raw-secret/raw-public/raw-seed key formats with per-algorithm aliasing, SubtleCrypto methods encapsulate{Bits,Key}/decapsulate{Bits,Key}/getPublicKey/supports, KeyObject.prototype.toCryptoKey, four new KEM key-usages, and a broad error-contract sweep. It bumps the SerializedScriptValue key-format version to 2, moves the subtle accessor onto Crypto.prototype with an ERR_INVALID_THIS brand check, and vendors 10 upstream Node tests.

Security risks

Everything here is security-sensitive: post-quantum key generation/import/export, AEAD encrypt/decrypt, KEM encapsulation, key-format parsing of untrusted DER/JWK bytes, and structured-clone deserialization of key material. The BoringSSL error-queue discipline, EVP_PKEY ownership (up_ref in toCryptoKey, EvpPKeyPtr RAII everywhere else), and the memcmp on JWK pub/seed consistency all look correct. The SubtleCrypto::supports implementation intentionally uses tryClearException inside a "probe" pattern — that's the one place exception clearing is by design.

Level of scrutiny

High. This is production crypto exposed to user-controlled inputs, adds new native key classes with their own lifetime/serialization, and changes globally-visible surface (Crypto.prototype.subtle placement, JsonWebKey.kty no longer required). The PR has been through ~8 bot-review rounds with every thread resolved by follow-up commits, but the sheer breadth (new algorithm classes, new SubtleCrypto methods, format-version bump) is beyond what I'd auto-approve.

Other factors

Test coverage is substantial (10 vendored Node tests + expanded web-crypto.test.ts, moving webcrypto compat 58%→76%). Every prior finding — including a reachable RELEASE_ASSERT_NOT_REACHED on structuredClone, an exception-swallowing tryClearException in importKey, and several vacuous test assertions — was confirmed against live Node v26.3.0 and fixed with a regression test. No unresolved threads remain. ML-KEM-512 is deliberately unregistered (BoringSSL lacks EVP support), which the test fixtures skip; the AsymmetricKeyValue switch omits ML_KEM_512 accordingly, which is consistent.

@dylan-conway
dylan-conway merged commit 50bb3bd into main Jul 24, 2026
52 checks passed
@dylan-conway
dylan-conway deleted the claude/webcrypto-chacha20-raw-secret branch July 24, 2026 04:43
cirospaciari added a commit that referenced this pull request Jul 24, 2026
… size]

supports() disagreed with Node's own vectors in 262 places. The two causes:

exportKey and getPublicKey normalized their algorithm as importKey, so a bare
name like "RSA-PSS" was rejected for missing a hash even though neither
operation takes algorithm members. Resolve the name only, matching
normalizeAlgorithm(alg, 'exportKey'). This also fixes
supports('wrapKey', 'AES-KW', 'HMAC').

The parameter checks Bun's algorithms perform at execution time were missing
from the probe, so supports() promised operations that would have thrown:
HMAC lengths that are zero or not a multiple of 8, AES lengths other than
128/192/256, EC named curves outside P-256/P-384/P-521, PBKDF2 with zero
iterations, ChaCha20-Poly1305 with an iv that is not 12 bytes or a tag that is
not 128 bits, and ECDH/X25519 deriveBits without a public key. A zero
deriveBits length is now accepted (a null one still is not), matching Node.

That leaves 68 disagreements, all of one class: the fixtures gate SHA-3,
TurboSHAKE and KAngarooTwelve on process.features.openssl_is_boringssl, which
Bun reports as true while its WebCrypto does implement SHA-3.
test-webcrypto-supports.mjs therefore stays out of the vendored set.

The ChaCha20-Poly1305 iv and tag lengths become named constants on the
algorithm class so the probe and the implementation cannot drift.

[allow size] the binary-size gate baselines against main, and this branch is
stacked on #34838, whose ML-DSA and ML-KEM support accounts for the growth.
Jarred-Sumner pushed a commit that referenced this pull request Aug 3, 2026
…#36833)

Net -285 LOC (349 deletions, 64 insertions including the source-lint
test).

Each symbol was verified with `rg -w <symbol> src/ build/debug/codegen/`
to have zero references outside its own definition, then confirmed by a
full `bun bd` build and `bun run rust:check-all` across all 10 targets.

### `src/platform/darwin.rs` (whole file, 210 LOC)

The entire file duplicates `bun_sys::darwin`: the
`OSLog`/`Signpost`/`Interval` API and the `nocancel` extern block have
zero callers. `bun_perf` (the only signpost consumer) imports
`bun_sys::darwin::OSLog` and `bun_sys::darwin::os_log::signpost::*`.
`bun_platform` is force-linked only for `linux.rs`'s `#[no_mangle]`
export. Also drops the now-unused `bun_opaque`/`strum` deps from
`bun_platform/Cargo.toml` and the stale doc comment in `src/sys/lib.rs`
that pointed here.

### webcrypto C++

- `CryptoKeyHMAC::create` + const-ref ctor, `CryptoKeyAES::create` +
const-ref ctor: never called; `generate`/`importRaw`/`importJwk` all
construct via the rvalue ctor directly.
- `JSCryptoKey::fromJS`: declared, never defined, never called.
- `JSSubtleCrypto::toWrapped`: only reachable via
`convert<IDLInterface<SubtleCrypto>>`; no such call exists.
- `OpenSSLCryptoUniquePtr.h`: `X509Ptr`/`BIOPtr` aliases (zero refs) and
the `OPENSSL_VERSION_NUMBER >= 0x30000000L` block (BoringSSL defines
`0x1010107f`, so it never compiles, and no code references
`OsslParamBldPtr`/`OsslParamPtr`/`EVPKDFCtxPtr`/`EVPKDFPtr`).
- `CommonCryptoDERUtilities.h`: `extraBytesNeededForEncodedLength` is
only called from the same TU; header decl removed, made `static` in the
.cpp.
- `ScriptExecutionContext.h`: `wrapCryptoKey`/`unwrapCryptoKey` stubs
plus the commented-out virtual decls; leftover from the earlier
`SerializedCryptoKeyWrap` removal.

### sqlite / NodeVM C++

- `lazy_sqlite3.h`: `sqlite3_column_int` / `sqlite3_memory_used` /
`sqlite3_prepare16_v3` typedef+var+define+dlsym lines (code uses
`sqlite3_column_int64`, tracks memory via `sqlite_malloc_amount`, uses
`sqlite3_prepare_v3`).
- `SQLiteSingleton::schema_versions`: never read, never appended to.
-
`JSStatementSync::allowBareNamedParams`/`allowUnknownNamedParams`/`rowStructure`
getters: members are accessed directly.
-
`DOMIsoSubspaces`/`DOMClientIsoSubspaces::m_*subspaceForJSSQLStatementConstructor`:
`JSSQLStatementConstructor` lives in `JSFunction`'s subspace per the
`static_assert` in `JSSQLStatement.h`.
- `NodeVMGlobalObject::sigintReceived`: not virtual, never called;
`SigintWatcher::signalAll` invokes `vm().notifyNeedTermination()`
directly.
- `NodeVMModuleRequest::specifier`/`importAttributes` getters: `toJS`
reads `m_specifier`/`m_importAttributes` directly.

### Rust

- `bun_ast::PartTag::{JsxImport, CjsImports, ReactFastRefresh}`: never
assigned or compared.
- `bun_ast::flags::JSXElement::HasAnyDynamic`: never inserted or tested.
- `bun_ast::import_record::Tag::Tailwind`: last variant, never
constructed or matched.
- `bun_ast::BindingNodeList` type alias + its unused re-export in
`bun_js_parser::parser`.
- `bun_ast::StoreAstAllocHeap::reset`: callers invoke the free fn
`store_ast_alloc_heap::reset()` directly.
- `bun_jsc::JSPromise::reject_task`: sibling `resolve_task` has 4
callers, `reject_task` has zero.
- `bun_jsc::JSRuntimeType::UNDEFINED`: only `NOTHING` is referenced.

### src/js

- `readline.js`: unused `ObjectSetPrototypeOf` primordial destructure.
- `repl.js`: unused `ArrayPrototypeSlice` primordial destructure.
- `zlib.ts`: collapse redundant `ArrayBufferIsView` intermediate alias.

### Verification

- `bun bd` builds clean
- `bun run rust:check-all` passes all 10 targets (incl.
`aarch64-apple-darwin` for the `bun_platform` change)
- Smoke tests pass: `web-crypto.test.ts`, `sqlite.test.js`,
`node-sqlite.test.ts`, `zlib.test.js`, `transpiler.test.js`
-
`test/internal/source-lints/dead-symbols-platform-webcrypto-sqlite-vm.test.ts`
fails on main, passes on this branch

### Followups (not deleted; left for review)

- `src/runtime/api/bun/h2/connection.rs:1731-1941` outbound-stream API
(`begin_header_block`/`encode_header`/`send_header_block`/`send_data`/`send_push_promise`
+ transitively `SendWindow::{available,consume}`,
`Coder::{take_pending_size_update,encode}`, `write_table_size_update`,
~215 LOC): only called from `#[cfg(test)]`. File carries
`#![allow(dead_code)]` and was authored in #31584; likely intentional
WIP scaffolding for migrating `h2_frame_parser`'s outbound path.
- `src/jsc/bindings/webcrypto/*.idl` (29 files, ~1055 LOC): not
processed by any build step (`scripts/glob-sources.ts` globs only
`*.cpp`), but #34838 edited them recently so they may be maintained as
documentation.
- `src/jsc/ErrorCode.rs`: `Zig_ErrorCodeJSErrorObject` `#[no_mangle]`
static with zero refs in any `.cpp`/`.h` (sibling
`Zig_ErrorCodeParserError` is declared in `headers-handwritten.h`; this
one is not).

<!-- robobun:evidence:begin -->

---

**[review]** gate passed · iteration 0 · 34 files touched

<details><summary>fails on main (without fix)</summary>

```console
ASAN without fix: 3 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/internal/source-lints/dead-symbols-platform-webcrypto-sqlite-vm.test.ts
bun test v1.4.0 (e2a9bd9)

test/internal/source-lints/dead-symbols-platform-webcrypto-sqlite-vm.test.ts:
19 | function src(p: string): string {
20 |   return readFileSync(path.join(repoRoot, p), "utf8");
21 | }
22 | 
23 | test("bun_platform no longer declares a darwin module (duplicated bun_sys::darwin)", () => {
24 |   expect(src("src/platform/lib.rs")).not.toMatch(/pub mod darwin;/);
                                              ^
error: expect(received).not.toMatch(expected)

Expected substring or pattern: not /pub mod darwin;/
Received: "#![allow(non_snake_case, non_camel_case_types, non_upper_case_globals)]\n#![warn(unused_must_use)]\n//! Per-OS APIs that don't fit in `bun_sys` (signposts, the `sys_epoll_pwait2` export).\n\n// Android is listed alongside Linux so the `#[no_mangle]` C exports\n// (`sys_epoll_pwait2`, …) reach the linker on the `*-linux-android` targets.\n#[cfg(target_os = \"macos\")]\npub mod darwin;\n#[cfg(any(target_os = \"linux\", t
... (truncated)

release without fix: 3 FAILED
bun test v1.4.0-canary.1 (1498d7b)

test/internal/source-lints/dead-symbols-platform-webcrypto-sqlite-vm.test.ts:
19 | function src(p: string): string {
20 |   return readFileSync(path.join(repoRoot, p), "utf8");
21 | }
22 | 
23 | test("bun_platform no longer declares a darwin module (duplicated bun_sys::darwin)", () => {
24 |   expect(src("src/platform/lib.rs")).not.toMatch(/pub mod darwin;/);
                                              ^
error: expect(received).not.toMatch(expected)

Expected substring or pattern: not /pub mod darwin;/
Received: "#![allow(non_snake_case, non_camel_case_types, non_upper_case_globals)]\n#![warn(unused_must_use)]\n//! Per-OS APIs that don't fit in `bun_sys` (signposts, the `sys_epoll_pwait2` export).\n\n// Android is listed alongside Linux so the `#[no_mangle]` C exports\n// (`sys_epoll_pwait2`, …) reach the linker on the `*-linux-android` targets.\n#[cfg(target_os = \"macos\")]\npub mod darwin;\n#[cfg(any(target_os = \"linux\", target_os = \"android\"))]\npub(crate) mod linux;\n"

      at <anonymous> (/workspace/bun/test/internal/source-lints/dead-symbols-platform-webcrypto-sqlite-vm.test.ts:24:42)
(fail) bun_platform no long
... (truncated)
```

</details>

<details><summary>passes on PR (with fix)</summary>

```console
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/internal/source-lints/dead-symbols-platform-webcrypto-sqlite-vm.test.ts
bun test v1.4.0 (e2a9bd9)

test/internal/source-lints/dead-symbols-platform-webcrypto-sqlite-vm.test.ts:
(pass) bun_platform no longer declares a darwin module (duplicated bun_sys::darwin) [10.05ms]
(pass) dead C++ symbols in webcrypto/sqlite/NodeVM do not reappear [23.34ms]
(pass) dead Rust symbols in ast/jsc do not reappear [14.33ms]

 3 pass
 0 fail
 3 expect() calls
Ran 3 tests across 1 file. [2.09s]
__F:0:S:0

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 733ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/139] gen generated_host_exports.rs
generated_host_exports.rs: 94 exports (host=3, lazy=10, generic=81, rust=0); 238 extern-C blocks audited
[2/139] gen cpp.rs (cppbind)
[3/139] gen JS modules (bundle-modules)
Preprocess modules (9279ms)
Bundle modules (59ms)
Postprocesss modules (259ms)
Bundle Functions (943ms)
Generate Code (32ms)

[10.59s] Bundled "src/js" for production
  2558 kb
  193 internal modules
  13 native modules
  90 internal functions across 19 files
[3/138] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)

  nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)

�[1m�[92m   Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core)
�[1m�[92m   Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno)
�[1m�[92m   Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr)
�[1m�[92m   Compiling�[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys)
�[1m�[92m   Compiling�[0m bun_safety v0.0.0 (/workspace/bun/src/safety)
�[1
... (truncated)
```

</details>

<details><summary>diff hotspot</summary>

```
Cargo.lock                                         |   2 -
 src/ast/import_record.rs                           |   2 -
 src/ast/lib.rs                                     |   7 +-
 src/ast/nodes.rs                                   |   6 +-
 src/js/node/readline.js                            |   1 -
 src/js/node/repl.js                                |   1 -
 src/js/node/zlib.ts                                |   3 +-
 src/js_parser/parser.rs                            |   4 +-
 src/jsc/JSPromise.rs                               |  12 --
 src/jsc/JSRuntimeType.rs                           |   1 -
 src/jsc/bindings/NodeVM.cpp                        |   5 -
 src/jsc/bindings/NodeVM.h                          |   1 -
 src/jsc/bindings/NodeVMModule.h                    |   3 -
 src/jsc/bindings/ScriptExecutionContext.h          |  16 --
 src/jsc/bindings/sqlite/JSSQLStatement.cpp         |   2 -
 src/jsc/bindings/sqlite/NodeSqlite.h               |   3 -
 src/jsc/bindings/sqlite/lazy_sqlite3.h             |  18 --
 src/jsc/bindings/webcore/DOMClientIsoSubspaces.h   |   1 -
 src/jsc/bindings/webcore/DOMIsoSubspaces.h         |   1 -
 .../webcrypto/CommonCryptoDERUtilities.cpp         |   2 +-
 .../bindings/webcrypto/CommonCryptoDERUtilities.h  |   1 -
 src/jsc/bindings/webcrypto/CryptoKeyAES.cpp        |   7 -
 src/jsc/bindings/webcrypto/CryptoKeyAES.h          |   5 -
 src/jsc/bindings/webcrypto/CryptoKeyHMAC.cpp       |   7 -
 src/jsc/bindings/webcrypto/CryptoKeyHMAC.h         |   6 -
 src/jsc/bindings/webcrypto/JSCryptoKey.h           |   2 -
 src/jsc/bindings/webcrypto/JSSubtleCrypto.cpp      |   7 -
 src/jsc/bindings/webcrypto/JSSubtleCrypto.h        |   1 -
 .../bindings/webcrypto/OpenSSLCryptoUniquePtr.h    |  13 --
 src/platform/Cargo.toml                            |   2 -
 src/platform/darwin.rs                             | 210 ---------------------
 src/platform/lib.rs                                |   4 +-
 src/sys/lib.rs                    
... (truncated)
```

</details>

**gate history** · 1 passed · 0 rejected · iteration 0

<details><summary>evidence per changed file</summary>

```
file                                        reads  edits  tests
Cargo.lock                                      0      0      0
src/ast/import_record.rs                        1      1      0
src/ast/lib.rs                                  3      3      0
src/ast/nodes.rs                                2      2      0
src/js/node/readline.js                         1      1      0
src/js/node/repl.js                             1      1      0
src/js/node/zlib.ts                             1      1      0
src/js_parser/parser.rs                         1      1      0
src/jsc/JSPromise.rs                            1      1      0
src/jsc/JSRuntimeType.rs                        1      1      0
src/jsc/bindings/NodeVM.cpp                     1      1      0
src/jsc/bindings/NodeVM.h                       1      1      0
src/jsc/bindings/NodeVMModule.h                 1      1      0
src/jsc/bindings/ScriptExecutionContext.h       2      1      0
src/jsc/bindings/sqlite/JSSQLStatement.cpp      1      1      0
src/jsc/bindings/sqlite/NodeSqlite.h            2      2      0
(+ 18 more files)
```

</details>

<!-- robobun:evidence:end -->

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
springmin pushed a commit to springmin/bun that referenced this pull request Aug 3, 2026
…oven-sh#36833)

Net -285 LOC (349 deletions, 64 insertions including the source-lint
test).

Each symbol was verified with `rg -w <symbol> src/ build/debug/codegen/`
to have zero references outside its own definition, then confirmed by a
full `bun bd` build and `bun run rust:check-all` across all 10 targets.

### `src/platform/darwin.rs` (whole file, 210 LOC)

The entire file duplicates `bun_sys::darwin`: the
`OSLog`/`Signpost`/`Interval` API and the `nocancel` extern block have
zero callers. `bun_perf` (the only signpost consumer) imports
`bun_sys::darwin::OSLog` and `bun_sys::darwin::os_log::signpost::*`.
`bun_platform` is force-linked only for `linux.rs`'s `#[no_mangle]`
export. Also drops the now-unused `bun_opaque`/`strum` deps from
`bun_platform/Cargo.toml` and the stale doc comment in `src/sys/lib.rs`
that pointed here.

### webcrypto C++

- `CryptoKeyHMAC::create` + const-ref ctor, `CryptoKeyAES::create` +
const-ref ctor: never called; `generate`/`importRaw`/`importJwk` all
construct via the rvalue ctor directly.
- `JSCryptoKey::fromJS`: declared, never defined, never called.
- `JSSubtleCrypto::toWrapped`: only reachable via
`convert<IDLInterface<SubtleCrypto>>`; no such call exists.
- `OpenSSLCryptoUniquePtr.h`: `X509Ptr`/`BIOPtr` aliases (zero refs) and
the `OPENSSL_VERSION_NUMBER >= 0x30000000L` block (BoringSSL defines
`0x1010107f`, so it never compiles, and no code references
`OsslParamBldPtr`/`OsslParamPtr`/`EVPKDFCtxPtr`/`EVPKDFPtr`).
- `CommonCryptoDERUtilities.h`: `extraBytesNeededForEncodedLength` is
only called from the same TU; header decl removed, made `static` in the
.cpp.
- `ScriptExecutionContext.h`: `wrapCryptoKey`/`unwrapCryptoKey` stubs
plus the commented-out virtual decls; leftover from the earlier
`SerializedCryptoKeyWrap` removal.

### sqlite / NodeVM C++

- `lazy_sqlite3.h`: `sqlite3_column_int` / `sqlite3_memory_used` /
`sqlite3_prepare16_v3` typedef+var+define+dlsym lines (code uses
`sqlite3_column_int64`, tracks memory via `sqlite_malloc_amount`, uses
`sqlite3_prepare_v3`).
- `SQLiteSingleton::schema_versions`: never read, never appended to.
-
`JSStatementSync::allowBareNamedParams`/`allowUnknownNamedParams`/`rowStructure`
getters: members are accessed directly.
-
`DOMIsoSubspaces`/`DOMClientIsoSubspaces::m_*subspaceForJSSQLStatementConstructor`:
`JSSQLStatementConstructor` lives in `JSFunction`'s subspace per the
`static_assert` in `JSSQLStatement.h`.
- `NodeVMGlobalObject::sigintReceived`: not virtual, never called;
`SigintWatcher::signalAll` invokes `vm().notifyNeedTermination()`
directly.
- `NodeVMModuleRequest::specifier`/`importAttributes` getters: `toJS`
reads `m_specifier`/`m_importAttributes` directly.

### Rust

- `bun_ast::PartTag::{JsxImport, CjsImports, ReactFastRefresh}`: never
assigned or compared.
- `bun_ast::flags::JSXElement::HasAnyDynamic`: never inserted or tested.
- `bun_ast::import_record::Tag::Tailwind`: last variant, never
constructed or matched.
- `bun_ast::BindingNodeList` type alias + its unused re-export in
`bun_js_parser::parser`.
- `bun_ast::StoreAstAllocHeap::reset`: callers invoke the free fn
`store_ast_alloc_heap::reset()` directly.
- `bun_jsc::JSPromise::reject_task`: sibling `resolve_task` has 4
callers, `reject_task` has zero.
- `bun_jsc::JSRuntimeType::UNDEFINED`: only `NOTHING` is referenced.

### src/js

- `readline.js`: unused `ObjectSetPrototypeOf` primordial destructure.
- `repl.js`: unused `ArrayPrototypeSlice` primordial destructure.
- `zlib.ts`: collapse redundant `ArrayBufferIsView` intermediate alias.

### Verification

- `bun bd` builds clean
- `bun run rust:check-all` passes all 10 targets (incl.
`aarch64-apple-darwin` for the `bun_platform` change)
- Smoke tests pass: `web-crypto.test.ts`, `sqlite.test.js`,
`node-sqlite.test.ts`, `zlib.test.js`, `transpiler.test.js`
-
`test/internal/source-lints/dead-symbols-platform-webcrypto-sqlite-vm.test.ts`
fails on main, passes on this branch

### Followups (not deleted; left for review)

- `src/runtime/api/bun/h2/connection.rs:1731-1941` outbound-stream API
(`begin_header_block`/`encode_header`/`send_header_block`/`send_data`/`send_push_promise`
+ transitively `SendWindow::{available,consume}`,
`Coder::{take_pending_size_update,encode}`, `write_table_size_update`,
~215 LOC): only called from `#[cfg(test)]`. File carries
`#![allow(dead_code)]` and was authored in oven-sh#31584; likely intentional
WIP scaffolding for migrating `h2_frame_parser`'s outbound path.
- `src/jsc/bindings/webcrypto/*.idl` (29 files, ~1055 LOC): not
processed by any build step (`scripts/glob-sources.ts` globs only
`*.cpp`), but oven-sh#34838 edited them recently so they may be maintained as
documentation.
- `src/jsc/ErrorCode.rs`: `Zig_ErrorCodeJSErrorObject` `#[no_mangle]`
static with zero refs in any `.cpp`/`.h` (sibling
`Zig_ErrorCodeParserError` is declared in `headers-handwritten.h`; this
one is not).

<!-- robobun:evidence:begin -->

---

**[review]** gate passed · iteration 0 · 34 files touched

<details><summary>fails on main (without fix)</summary>

```console
ASAN without fix: 3 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/internal/source-lints/dead-symbols-platform-webcrypto-sqlite-vm.test.ts
bun test v1.4.0 (e2a9bd9)

test/internal/source-lints/dead-symbols-platform-webcrypto-sqlite-vm.test.ts:
19 | function src(p: string): string {
20 |   return readFileSync(path.join(repoRoot, p), "utf8");
21 | }
22 | 
23 | test("bun_platform no longer declares a darwin module (duplicated bun_sys::darwin)", () => {
24 |   expect(src("src/platform/lib.rs")).not.toMatch(/pub mod darwin;/);
                                              ^
error: expect(received).not.toMatch(expected)

Expected substring or pattern: not /pub mod darwin;/
Received: "#![allow(non_snake_case, non_camel_case_types, non_upper_case_globals)]\n#![warn(unused_must_use)]\n//! Per-OS APIs that don't fit in `bun_sys` (signposts, the `sys_epoll_pwait2` export).\n\n// Android is listed alongside Linux so the `#[no_mangle]` C exports\n// (`sys_epoll_pwait2`, …) reach the linker on the `*-linux-android` targets.\n#[cfg(target_os = \"macos\")]\npub mod darwin;\n#[cfg(any(target_os = \"linux\", t
... (truncated)

release without fix: 3 FAILED
bun test v1.4.0-canary.1 (1498d7b)

test/internal/source-lints/dead-symbols-platform-webcrypto-sqlite-vm.test.ts:
19 | function src(p: string): string {
20 |   return readFileSync(path.join(repoRoot, p), "utf8");
21 | }
22 | 
23 | test("bun_platform no longer declares a darwin module (duplicated bun_sys::darwin)", () => {
24 |   expect(src("src/platform/lib.rs")).not.toMatch(/pub mod darwin;/);
                                              ^
error: expect(received).not.toMatch(expected)

Expected substring or pattern: not /pub mod darwin;/
Received: "#![allow(non_snake_case, non_camel_case_types, non_upper_case_globals)]\n#![warn(unused_must_use)]\n//! Per-OS APIs that don't fit in `bun_sys` (signposts, the `sys_epoll_pwait2` export).\n\n// Android is listed alongside Linux so the `#[no_mangle]` C exports\n// (`sys_epoll_pwait2`, …) reach the linker on the `*-linux-android` targets.\n#[cfg(target_os = \"macos\")]\npub mod darwin;\n#[cfg(any(target_os = \"linux\", target_os = \"android\"))]\npub(crate) mod linux;\n"

      at <anonymous> (/workspace/bun/test/internal/source-lints/dead-symbols-platform-webcrypto-sqlite-vm.test.ts:24:42)
(fail) bun_platform no long
... (truncated)
```

</details>

<details><summary>passes on PR (with fix)</summary>

```console
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/internal/source-lints/dead-symbols-platform-webcrypto-sqlite-vm.test.ts
bun test v1.4.0 (e2a9bd9)

test/internal/source-lints/dead-symbols-platform-webcrypto-sqlite-vm.test.ts:
(pass) bun_platform no longer declares a darwin module (duplicated bun_sys::darwin) [10.05ms]
(pass) dead C++ symbols in webcrypto/sqlite/NodeVM do not reappear [23.34ms]
(pass) dead Rust symbols in ast/jsc do not reappear [14.33ms]

 3 pass
 0 fail
 3 expect() calls
Ran 3 tests across 1 file. [2.09s]
__F:0:S:0

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 733ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/139] gen generated_host_exports.rs
generated_host_exports.rs: 94 exports (host=3, lazy=10, generic=81, rust=0); 238 extern-C blocks audited
[2/139] gen cpp.rs (cppbind)
[3/139] gen JS modules (bundle-modules)
Preprocess modules (9279ms)
Bundle modules (59ms)
Postprocesss modules (259ms)
Bundle Functions (943ms)
Generate Code (32ms)

[10.59s] Bundled "src/js" for production
  2558 kb
  193 internal modules
  13 native modules
  90 internal functions across 19 files
[3/138] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)

  nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)

�[1m�[92m   Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core)
�[1m�[92m   Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno)
�[1m�[92m   Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr)
�[1m�[92m   Compiling�[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys)
�[1m�[92m   Compiling�[0m bun_safety v0.0.0 (/workspace/bun/src/safety)
�[1
... (truncated)
```

</details>

<details><summary>diff hotspot</summary>

```
Cargo.lock                                         |   2 -
 src/ast/import_record.rs                           |   2 -
 src/ast/lib.rs                                     |   7 +-
 src/ast/nodes.rs                                   |   6 +-
 src/js/node/readline.js                            |   1 -
 src/js/node/repl.js                                |   1 -
 src/js/node/zlib.ts                                |   3 +-
 src/js_parser/parser.rs                            |   4 +-
 src/jsc/JSPromise.rs                               |  12 --
 src/jsc/JSRuntimeType.rs                           |   1 -
 src/jsc/bindings/NodeVM.cpp                        |   5 -
 src/jsc/bindings/NodeVM.h                          |   1 -
 src/jsc/bindings/NodeVMModule.h                    |   3 -
 src/jsc/bindings/ScriptExecutionContext.h          |  16 --
 src/jsc/bindings/sqlite/JSSQLStatement.cpp         |   2 -
 src/jsc/bindings/sqlite/NodeSqlite.h               |   3 -
 src/jsc/bindings/sqlite/lazy_sqlite3.h             |  18 --
 src/jsc/bindings/webcore/DOMClientIsoSubspaces.h   |   1 -
 src/jsc/bindings/webcore/DOMIsoSubspaces.h         |   1 -
 .../webcrypto/CommonCryptoDERUtilities.cpp         |   2 +-
 .../bindings/webcrypto/CommonCryptoDERUtilities.h  |   1 -
 src/jsc/bindings/webcrypto/CryptoKeyAES.cpp        |   7 -
 src/jsc/bindings/webcrypto/CryptoKeyAES.h          |   5 -
 src/jsc/bindings/webcrypto/CryptoKeyHMAC.cpp       |   7 -
 src/jsc/bindings/webcrypto/CryptoKeyHMAC.h         |   6 -
 src/jsc/bindings/webcrypto/JSCryptoKey.h           |   2 -
 src/jsc/bindings/webcrypto/JSSubtleCrypto.cpp      |   7 -
 src/jsc/bindings/webcrypto/JSSubtleCrypto.h        |   1 -
 .../bindings/webcrypto/OpenSSLCryptoUniquePtr.h    |  13 --
 src/platform/Cargo.toml                            |   2 -
 src/platform/darwin.rs                             | 210 ---------------------
 src/platform/lib.rs                                |   4 +-
 src/sys/lib.rs                    
... (truncated)
```

</details>

**gate history** · 1 passed · 0 rejected · iteration 0

<details><summary>evidence per changed file</summary>

```
file                                        reads  edits  tests
Cargo.lock                                      0      0      0
src/ast/import_record.rs                        1      1      0
src/ast/lib.rs                                  3      3      0
src/ast/nodes.rs                                2      2      0
src/js/node/readline.js                         1      1      0
src/js/node/repl.js                             1      1      0
src/js/node/zlib.ts                             1      1      0
src/js_parser/parser.rs                         1      1      0
src/jsc/JSPromise.rs                            1      1      0
src/jsc/JSRuntimeType.rs                        1      1      0
src/jsc/bindings/NodeVM.cpp                     1      1      0
src/jsc/bindings/NodeVM.h                       1      1      0
src/jsc/bindings/NodeVMModule.h                 1      1      0
src/jsc/bindings/ScriptExecutionContext.h       2      1      0
src/jsc/bindings/sqlite/JSSQLStatement.cpp      1      1      0
src/jsc/bindings/sqlite/NodeSqlite.h            2      2      0
(+ 18 more files)
```

</details>

<!-- robobun:evidence:end -->

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants