From 6077cdec044a6b1856f18e8e29a11bf3d0426266 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 27 Oct 2026 21:06:14 +0000 Subject: [PATCH 1/4] bindgen: throw ERR_MISSING_ARGS from a helper that takes no ThrowScope The check that `requiredArguments` emits passed the host function's ThrowScope by reference to `ERR::MISSING_ARGS`. A host function that lends its scope keeps it in memory on every path, also on the path that does not throw. The check now calls `Bun::throwMissingArgsError(globalObject, message)`. The helper is out of line and opens its own scope. The error class, code and message do not change. --- src/codegen/bindgen-emit.ts | 2 +- src/jsc/bindings/ErrorCode.cpp | 7 +++++++ src/jsc/bindings/ErrorCode.h | 3 +++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/codegen/bindgen-emit.ts b/src/codegen/bindgen-emit.ts index 3d7dd0ef6f83..e8c2717a8f0a 100644 --- a/src/codegen/bindgen-emit.ts +++ b/src/codegen/bindgen-emit.ts @@ -1375,7 +1375,7 @@ function cppThrowIfArgumentsMissing(signature: Signature): string { if (names.length === 0) return ""; const message = `The ${new Intl.ListFormat("en").format(names)} argument${names.length > 1 ? "s" : ""} must be specified`; return ` if (callFrame->argumentCount() < ${names.length}) [[unlikely]] - return Bun::ERR::MISSING_ARGS(scope, globalObject, ${asciiLiteral(message)}); + RELEASE_AND_RETURN(scope, Bun::throwMissingArgsError(globalObject, ${asciiLiteral(message)})); `; } diff --git a/src/jsc/bindings/ErrorCode.cpp b/src/jsc/bindings/ErrorCode.cpp index cd5f87c1d9a9..64de09f721c5 100644 --- a/src/jsc/bindings/ErrorCode.cpp +++ b/src/jsc/bindings/ErrorCode.cpp @@ -1845,6 +1845,13 @@ JSC::EncodedJSValue Bun::throwInvalidThisCallError(JSC::JSGlobalObject* globalOb return {}; } +JSC::EncodedJSValue Bun::throwMissingArgsError(JSC::JSGlobalObject* globalObject, const ASCIILiteral message) +{ + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + return ERR::MISSING_ARGS(scope, globalObject, message); +} + void Bun::throwInvalidThisError(JSC::JSGlobalObject* globalObject, JSC::ThrowScope& scope, JSC::JSValue thisValue, const ASCIILiteral typeName) { auto& vm = JSC::getVM(globalObject); diff --git a/src/jsc/bindings/ErrorCode.h b/src/jsc/bindings/ErrorCode.h index 84761a1c0f77..cac11d77fac7 100644 --- a/src/jsc/bindings/ErrorCode.h +++ b/src/jsc/bindings/ErrorCode.h @@ -94,6 +94,9 @@ JSC::JSObject* createError(VM& vm, Zig::GlobalObject* globalObject, ErrorCode co void throwInvalidThisError(JSGlobalObject* globalObject, JSC::ThrowScope&, JSValue thisValue, const ASCIILiteral typeName); // throwInvalidThisError(callFrame->thisValue()) and returns the empty value; one call at each generated host-function's invalid-this branch. JSC::EncodedJSValue throwInvalidThisCallError(JSGlobalObject* globalObject, JSC::CallFrame* callFrame, const ASCIILiteral typeName); +// ERR::MISSING_ARGS in a ThrowScope of its own; one call at each generated host-function's missing-arguments branch. +// Out of line, and it takes no scope: a host function that lends its own scope keeps it in memory on every path. +NEVER_INLINE JSC::EncodedJSValue throwMissingArgsError(JSGlobalObject* globalObject, const ASCIILiteral message); JSObject* createInvalidThisError(JSGlobalObject* globalObject, const String& message); JSC_DECLARE_HOST_FUNCTION(jsFunctionMakeErrorWithCode); From 180a990a6e74e1b132b7edecaf135b965b40b9b0 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 27 Oct 2026 21:06:15 +0000 Subject: [PATCH 2/4] crypto.getRandomValues: throw a TypeError for a missing argument and for a non-view The IDL argument is an ArrayBufferView. A call without it and a value that is not a view threw the DOMException TypeMismatchError of the method's own steps. WebIDL, browsers and Node.js v26.12.1 throw a TypeError for both. - The schema entry declares `requiredArguments: 1`, so a call without an argument throws ERR_MISSING_ARGS. - The body throws ERR_INVALID_ARG_TYPE for a value that is not a typed array or a DataView. A Float16Array, Float32Array, Float64Array or DataView keeps TypeMismatchError. No call that returned before throws now. A view on a shared or resizable buffer is still taken. The vendored test-webcrypto-random.js takes the text of Node.js v26.12.1 for these rows. --- src/runtime/webcore/Crypto.bind.ts | 4 +- src/runtime/webcore/Crypto.rs | 31 ++++-- .../test/parallel/test-webcrypto-random.js | 5 +- test/js/web/crypto/web-crypto.test.ts | 100 +++++++++++++++--- 4 files changed, 116 insertions(+), 24 deletions(-) diff --git a/src/runtime/webcore/Crypto.bind.ts b/src/runtime/webcore/Crypto.bind.ts index 2acca475b9a2..d8af136a6a75 100644 --- a/src/runtime/webcore/Crypto.bind.ts +++ b/src/runtime/webcore/Crypto.bind.ts @@ -58,8 +58,8 @@ export const Crypto = b.class({ rustPath: "crate::webcore::crypto::Crypto", construct: "illegal", proto: { - /** What is not an integer typed array throws WebCrypto's `TypeMismatchError`, which no type here throws. */ - getRandomValues: { args: { array: b.any }, returns: b.any, capability: "js", throws: true }, + /** The IDL type is `ArrayBufferView`, which no type here is. The body refuses what is not a view, then a view that is not an integer typed array with WebCrypto's `TypeMismatchError`. */ + getRandomValues: { args: { array: b.any }, returns: b.any, capability: "js", throws: true, requiredArguments: 1 }, randomUUID: { returns: b.string }, timingSafeEqual: equal, }, diff --git a/src/runtime/webcore/Crypto.rs b/src/runtime/webcore/Crypto.rs index b6cb48f5c0d4..1202b630316a 100644 --- a/src/runtime/webcore/Crypto.rs +++ b/src/runtime/webcore/Crypto.rs @@ -2,7 +2,7 @@ use bun_core::String as BunString; use bun_jsc::bindgen::{self, JsHeap, JsLocal, JsScope, StringOrJsBytes}; use bun_jsc::uuid::{self, UUID, UUID5, UUID7}; use bun_jsc::virtual_machine::VirtualMachine; -use bun_jsc::{JSGlobalObject, JSType, JSValue, JsClass as _}; +use bun_jsc::{JSGlobalObject, JSType, JSValue, JsClass as _, JsError}; use crate::generated_bindgen::crypto::{EncodedUUID, UUIDEncodingOrTimestamp, UUIDTimestamp}; use crate::node::Encoding; @@ -18,7 +18,7 @@ impl Crypto { let global = cx.global(); // https://w3c.github.io/webcrypto/#Crypto-method-getRandomValues accepts only // integer-typed views. This is an allow-list: DataView, ArrayBuffer and SharedArrayBuffer - // all pass `as_array_buffer` but must still raise TypeMismatchError. + // all pass `as_array_buffer` but are refused. let array_buffer = array.to_js().as_array_buffer(global).filter(|_| { matches!( array.to_js().js_type(), @@ -34,12 +34,7 @@ impl Crypto { ) }); let Some(mut array_buffer) = array_buffer else { - return Err(global - .throw_dom_exception( - bun_jsc::DOMExceptionCode::TypeMismatchError, - format_args!("The data argument must be an integer-type TypedArray"), - ) - .into()); + return Err(throw_not_integer_view(global, array.to_js()).into()); }; random_data(global, array_buffer.byte_slice_mut()); @@ -70,6 +65,26 @@ impl Crypto { } } +/// What is not a view fails the WebIDL conversion to `ArrayBufferView`, a `TypeError`. A view of +/// another type is the `TypeMismatchError` of the method's own steps. The IDL also refuses a view +/// on a shared or resizable buffer. Bun takes it as a view on any other buffer: `node:wasi` fills +/// shared memory through one. +#[cold] +fn throw_not_integer_view(global: &JSGlobalObject, value: JSValue) -> JsError { + if matches!( + value.js_type(), + JSType::Float16Array | JSType::Float32Array | JSType::Float64Array | JSType::DataView + ) { + return global.throw_dom_exception( + bun_jsc::DOMExceptionCode::TypeMismatchError, + format_args!("The data argument must be an integer-type TypedArray"), + ); + } + global.throw_invalid_arguments(format_args!( + "The \"array\" argument must be an instance of ArrayBufferView" + )) +} + pub(crate) fn timing_safe_equal<'a>( heap: &JsHeap<'a>, buf1: &[u8], diff --git a/test/js/node/test/parallel/test-webcrypto-random.js b/test/js/node/test/parallel/test-webcrypto-random.js index 6712dbad2fca..b32eb50d953a 100644 --- a/test/js/node/test/parallel/test-webcrypto-random.js +++ b/test/js/node/test/parallel/test-webcrypto-random.js @@ -9,8 +9,11 @@ const { Buffer } = require('buffer'); const assert = require('assert'); const { crypto } = globalThis; +for (const value of [undefined, null, '', 1, {}, [], new ArrayBuffer(1)]) { + assert.throws(() => crypto.getRandomValues(value), TypeError); +} + [ - undefined, null, '', 1, {}, [], new Float32Array(1), new Float64Array(1), new DataView(new ArrayBuffer(1)), diff --git a/test/js/web/crypto/web-crypto.test.ts b/test/js/web/crypto/web-crypto.test.ts index efa5d8f3d49f..db9713e2c2f9 100644 --- a/test/js/web/crypto/web-crypto.test.ts +++ b/test/js/web/crypto/web-crypto.test.ts @@ -1,7 +1,7 @@ import { spawnSync } from "bun"; import { describe, expect, it } from "bun:test"; import { bunEnv, bunExe } from "harness"; -import { createSecretKey, type webcrypto } from "node:crypto"; +import { createSecretKey, getRandomValues as nodeGetRandomValues, type webcrypto } from "node:crypto"; type AlgorithmIdentifier = webcrypto.AlgorithmIdentifier; type HmacImportParams = webcrypto.HmacImportParams; @@ -1448,9 +1448,28 @@ describe("CryptoKey.usages ordering", () => { }); }); -// getRandomValues takes integer-typed views only; every other ArrayBufferView-ish -// argument raises TypeMismatchError rather than being filled. +// The IDL argument of getRandomValues is an ArrayBufferView. As for every WebIDL operation, a +// call without it and a value that is not a view are a TypeError. The method's own steps then +// raise TypeMismatchError for a view that is not an integer typed array. describe("crypto.getRandomValues argument types", () => { + /** What `call` throws, or `undefined` when it returns. */ + const thrownBy = (call: () => unknown) => { + try { + call(); + } catch (error: any) { + return { class: error.constructor.name, code: error.code, message: error.message }; + } + }; + const notAView = { + class: "TypeError", + code: "ERR_INVALID_ARG_TYPE", + message: 'The "array" argument must be an instance of ArrayBufferView', + }; + const typeMismatch = expect.objectContaining({ + name: "TypeMismatchError", + message: "The data argument must be an integer-type TypedArray", + }); + const integerViews = [ ["Int8Array", () => new Int8Array(4)], ["Uint8Array", () => new Uint8Array(4)], @@ -1470,26 +1489,81 @@ describe("crypto.getRandomValues argument types", () => { }); } - const rejected = [ + const otherViews = [ ["Float16Array", () => new Float16Array(4)], ["Float32Array", () => new Float32Array(4)], ["Float64Array", () => new Float64Array(4)], ["DataView", () => new DataView(new ArrayBuffer(4))], + ] as const; + + for (const [name, make] of otherViews) { + it(`rejects ${name} with TypeMismatchError`, () => { + expect(() => crypto.getRandomValues(make() as any)).toThrow(typeMismatch); + }); + } + + // Bun deviates from the IDL here on purpose. The argument has no [AllowShared] or + // [AllowResizable], so WebIDL and Node.js refuse every view on these buffers with a TypeError. + // Bun takes the view: a call that worked keeps working, and a view of another type gets the + // same TypeMismatchError as on a plain buffer. + const otherBuffers = [ + ["a SharedArrayBuffer", () => new SharedArrayBuffer(32)], + ["a resizable ArrayBuffer", () => new ArrayBuffer(32, { maxByteLength: 64 })], + ] as const; + + for (const [name, make] of otherBuffers) { + it(`fills a Uint8Array on ${name}`, () => { + const view = new Uint8Array(make()); + expect(crypto.getRandomValues(view)).toBe(view); + expect(view.some(byte => byte !== 0)).toBe(true); + }); + + it(`rejects a Float32Array and a DataView on ${name} with TypeMismatchError`, () => { + expect(() => crypto.getRandomValues(new Float32Array(make()))).toThrow(typeMismatch); + expect(() => crypto.getRandomValues(new DataView(make()))).toThrow(typeMismatch); + }); + } + + const notViews = [ + ["undefined", () => undefined], + ["null", () => null], + ["a string", () => ""], + ["a number", () => 1], + ["a plain object", () => ({})], + ["an array", () => []], ["ArrayBuffer", () => new ArrayBuffer(4)], ["SharedArrayBuffer", () => new SharedArrayBuffer(4)], - ["plain object", () => ({})], + ["a Proxy of a Uint8Array", () => new Proxy(new Uint8Array(4), {})], ] as const; - for (const [name, make] of rejected) { - it(`rejects ${name} with TypeMismatchError`, () => { - expect(() => crypto.getRandomValues(make() as any)).toThrow( - expect.objectContaining({ - name: "TypeMismatchError", - message: "The data argument must be an integer-type TypedArray", - }), - ); + for (const [name, make] of notViews) { + it(`rejects ${name} with a TypeError`, () => { + expect(thrownBy(() => crypto.getRandomValues(make() as any))).toEqual(notAView); }); } + + it("rejects a call without an argument with a TypeError", () => { + // @ts-expect-error + expect(thrownBy(() => crypto.getRandomValues())).toEqual({ + class: "TypeError", + code: "ERR_MISSING_ARGS", + message: 'The "array" argument must be specified', + }); + }); + + it("checks `this` before the arguments", () => { + // @ts-expect-error + expect(thrownBy(() => crypto.getRandomValues.call({}))).toMatchObject({ + class: "TypeError", + code: "ERR_INVALID_THIS", + }); + }); + + // It passes its one parameter on, so the argument is never missing. + it("node:crypto's getRandomValues() without an argument is a TypeError", () => { + // @ts-expect-error + expect(thrownBy(() => nodeGetRandomValues())).toEqual(notAView); + }); }); describe("exception scope discipline", () => { From 9ab418ca554a540e7f4129ff9d076e90b7a448cf Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 29 Oct 2026 09:58:46 +0000 Subject: [PATCH 3/4] crypto.getRandomValues: state the shared and resizable buffer behaviour as a fact in two comments The doc comment of `throw_not_integer_view` and the comment above the tests for views on a SharedArrayBuffer or a resizable ArrayBuffer said that Bun takes these views on purpose. They now say what WebIDL and Node.js do and what this code does. No code or test outcome changes. --- src/runtime/webcore/Crypto.rs | 6 +++--- test/js/web/crypto/web-crypto.test.ts | 7 +++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/runtime/webcore/Crypto.rs b/src/runtime/webcore/Crypto.rs index 1202b630316a..44f2a99cb169 100644 --- a/src/runtime/webcore/Crypto.rs +++ b/src/runtime/webcore/Crypto.rs @@ -66,9 +66,9 @@ impl Crypto { } /// What is not a view fails the WebIDL conversion to `ArrayBufferView`, a `TypeError`. A view of -/// another type is the `TypeMismatchError` of the method's own steps. The IDL also refuses a view -/// on a shared or resizable buffer. Bun takes it as a view on any other buffer: `node:wasi` fills -/// shared memory through one. +/// another type is the `TypeMismatchError` of the method's own steps. That conversion also refuses +/// a view on a shared or resizable buffer. Nothing here tests for one: it gets what a view on any +/// other buffer gets, and `node:wasi` passes one for a shared memory. #[cold] fn throw_not_integer_view(global: &JSGlobalObject, value: JSValue) -> JsError { if matches!( diff --git a/test/js/web/crypto/web-crypto.test.ts b/test/js/web/crypto/web-crypto.test.ts index db9713e2c2f9..4f68ac77d7df 100644 --- a/test/js/web/crypto/web-crypto.test.ts +++ b/test/js/web/crypto/web-crypto.test.ts @@ -1502,10 +1502,9 @@ describe("crypto.getRandomValues argument types", () => { }); } - // Bun deviates from the IDL here on purpose. The argument has no [AllowShared] or - // [AllowResizable], so WebIDL and Node.js refuse every view on these buffers with a TypeError. - // Bun takes the view: a call that worked keeps working, and a view of another type gets the - // same TypeMismatchError as on a plain buffer. + // The IDL argument has no [AllowShared] or [AllowResizable], so WebIDL and Node.js refuse every + // view on these buffers with a TypeError. Bun does not test the buffer: it fills an integer + // view, and a view of another type gets the same TypeMismatchError as on a plain buffer. const otherBuffers = [ ["a SharedArrayBuffer", () => new SharedArrayBuffer(32)], ["a resizable ArrayBuffer", () => new ArrayBuffer(32, { maxByteLength: 64 })], From 19c25d13b8f990b56ba1949eb50db1616132bce0 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 29 Oct 2026 10:39:03 +0000 Subject: [PATCH 4/4] crypto.getRandomValues: make the two new comments in src one line each The comment of `Bun::throwMissingArgsError` and the doc comment of `throw_not_integer_view` each say in one line what the function is for. The doc comment no longer describes views on a shared or resizable buffer. No code changes. --- src/jsc/bindings/ErrorCode.h | 3 +-- src/runtime/webcore/Crypto.rs | 5 +---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/jsc/bindings/ErrorCode.h b/src/jsc/bindings/ErrorCode.h index cac11d77fac7..1acd342e91ec 100644 --- a/src/jsc/bindings/ErrorCode.h +++ b/src/jsc/bindings/ErrorCode.h @@ -94,8 +94,7 @@ JSC::JSObject* createError(VM& vm, Zig::GlobalObject* globalObject, ErrorCode co void throwInvalidThisError(JSGlobalObject* globalObject, JSC::ThrowScope&, JSValue thisValue, const ASCIILiteral typeName); // throwInvalidThisError(callFrame->thisValue()) and returns the empty value; one call at each generated host-function's invalid-this branch. JSC::EncodedJSValue throwInvalidThisCallError(JSGlobalObject* globalObject, JSC::CallFrame* callFrame, const ASCIILiteral typeName); -// ERR::MISSING_ARGS in a ThrowScope of its own; one call at each generated host-function's missing-arguments branch. -// Out of line, and it takes no scope: a host function that lends its own scope keeps it in memory on every path. +// ERR::MISSING_ARGS in a ThrowScope of its own, out of line, so a generated host function lends no scope to its missing-arguments branch. NEVER_INLINE JSC::EncodedJSValue throwMissingArgsError(JSGlobalObject* globalObject, const ASCIILiteral message); JSObject* createInvalidThisError(JSGlobalObject* globalObject, const String& message); diff --git a/src/runtime/webcore/Crypto.rs b/src/runtime/webcore/Crypto.rs index 44f2a99cb169..a415cee805ab 100644 --- a/src/runtime/webcore/Crypto.rs +++ b/src/runtime/webcore/Crypto.rs @@ -65,10 +65,7 @@ impl Crypto { } } -/// What is not a view fails the WebIDL conversion to `ArrayBufferView`, a `TypeError`. A view of -/// another type is the `TypeMismatchError` of the method's own steps. That conversion also refuses -/// a view on a shared or resizable buffer. Nothing here tests for one: it gets what a view on any -/// other buffer gets, and `node:wasi` passes one for a shared memory. +/// WebIDL's conversion refuses what is not a view. The method's own steps refuse the other views. #[cold] fn throw_not_integer_view(global: &JSGlobalObject, value: JSValue) -> JsError { if matches!(