From 07a1e624c6839a5c3ef5dfbf75e0edb122e30225 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 9 Sep 2026 01:19:53 +0000 Subject: [PATCH] [JSC] WebAssembly.Exception.prototype.getArg: an index that is not an unsigned long is a TypeError, not a RangeError getArg(tag, index) declares index as [EnforceRange] unsigned long (WebAssembly/exception-handling#257, now in the WebAssembly/spec js-api). A WebIDL [EnforceRange] conversion that fails, because the value is NaN, an infinity, or has an integer part outside [0, 2^32 - 1], throws TypeError. Only an index that converts and is then past the payload is a RangeError. V8 (EnforceUint32 in wasm-js.cc) and SpiderMonkey (EnforceRangeU32, JSMSG_WASM_BAD_ENFORCE_RANGE) both throw TypeError. JSC threw TypeError too until 282520@main (bug 278413), which passed ErrorType::RangeError from getArg to toNonWrappingUint32() so that the imported WPT test wasm/jsapi/exception/getArg.tentative.any.js would pass. That WPT copy predates exception-handling#257: the copy of the same test in WebAssembly/spec asserts TypeError, V8 lists the WPT copy as an expected failure in test/wasm-js/wasm-js.status, and Firefox marks its "Getting out-of-range argument" subtest as an expected FAIL. This reverts the call-site half of 282520@main and restores the previous FAIL expectation for that subtest. toNonWrappingUint32() keeps its ErrorType parameter, unused for now, so the helper stays as upstream has it. * JSTests/wasm/js-api/exception-getarg-index-enforce-range.js: Added. * JSTests/wasm/v8/exceptions-api.js: (TestGetArg): Restore the getArg(tag, undefined) TypeError assertion with JSC's message. * LayoutTests/imported/w3c/web-platform-tests/wasm/jsapi/exception/getArg.tentative.any-expected.txt: * LayoutTests/imported/w3c/web-platform-tests/wasm/jsapi/exception/getArg.tentative.any.worker-expected.txt: * Source/JavaScriptCore/wasm/js/WebAssemblyExceptionPrototype.cpp: (JSC::JSC_DEFINE_HOST_FUNCTION): --- .../exception-getarg-index-enforce-range.js | 75 +++++++++++++++++++ JSTests/wasm/v8/exceptions-api.js | 4 +- .../getArg.tentative.any-expected.txt | 4 +- .../getArg.tentative.any.worker-expected.txt | 4 +- .../wasm/js/WebAssemblyExceptionPrototype.cpp | 2 +- 5 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 JSTests/wasm/js-api/exception-getarg-index-enforce-range.js diff --git a/JSTests/wasm/js-api/exception-getarg-index-enforce-range.js b/JSTests/wasm/js-api/exception-getarg-index-enforce-range.js new file mode 100644 index 0000000000000..aa635be3e57d8 --- /dev/null +++ b/JSTests/wasm/js-api/exception-getarg-index-enforce-range.js @@ -0,0 +1,75 @@ +import * as assert from '../assert.js'; + +// https://webassembly.github.io/spec/js-api/#exceptions +// any getArg(Tag exceptionTag, [EnforceRange] unsigned long index); +// A WebIDL [EnforceRange] conversion that fails is a TypeError. Only an index that converts and is +// then >= the payload size is a RangeError. + +const tag = new WebAssembly.Tag({ parameters: ["i32", "f64"] }); +const exception = new WebAssembly.Exception(tag, [5, 6.5]); + +const notAnUnsignedLong = [ + -1, + -1.5, + 2 ** 32, + 2 ** 32 + 0.5, + 2 ** 53, + Number.MAX_VALUE, + NaN, + Infinity, + -Infinity, + undefined, + "x", + "0x100000000", + {}, + { valueOf() { return -1; } }, + { valueOf() { return 2 ** 32; } }, +]; +for (const index of notAnUnsignedLong) + assert.throws(() => exception.getArg(tag, index), TypeError, "Expect an integer argument in the range: [0, 2^32 - 1]"); + +// These convert to 0 or 1: ToNumber, -0 becomes +0, then the integer part is taken. +assert.eq(exception.getArg(tag, 0), 5); +assert.eq(exception.getArg(tag, -0), 5); +assert.eq(exception.getArg(tag, -0.5), 5); +assert.eq(exception.getArg(tag, 0.9), 5); +assert.eq(exception.getArg(tag, null), 5); +assert.eq(exception.getArg(tag, false), 5); +assert.eq(exception.getArg(tag, ""), 5); +assert.eq(exception.getArg(tag, 1), 6.5); +assert.eq(exception.getArg(tag, 1.99), 6.5); +assert.eq(exception.getArg(tag, "1"), 6.5); +assert.eq(exception.getArg(tag, true), 6.5); +assert.eq(exception.getArg(tag, [1]), 6.5); + +let valueOfCalls = 0; +assert.eq(exception.getArg(tag, { valueOf() { ++valueOfCalls; return 1; } }), 6.5); +assert.eq(valueOfCalls, 1); + +// A valid unsigned long past the payload is still a RangeError. +for (const index of [2, 2.5, "2", 2 ** 31, 2 ** 32 - 1]) + assert.throws(() => exception.getArg(tag, index), RangeError, "WebAssembly.Exception.getArg(): Index out of range"); + +// An empty payload: every index is out of range, but the conversion still comes first. +{ + const emptyTag = new WebAssembly.Tag({ parameters: [] }); + const empty = new WebAssembly.Exception(emptyTag, []); + assert.throws(() => empty.getArg(emptyTag, 0), RangeError, "WebAssembly.Exception.getArg(): Index out of range"); + assert.throws(() => empty.getArg(emptyTag, -1), TypeError, "Expect an integer argument in the range: [0, 2^32 - 1]"); + assert.throws(() => empty.getArg(emptyTag, undefined), TypeError, "Expect an integer argument in the range: [0, 2^32 - 1]"); +} + +// The index is converted before the tag is compared with the exception's tag. +{ + const otherTag = new WebAssembly.Tag({ parameters: ["i32", "f64"] }); + assert.throws(() => exception.getArg(otherTag, -1), TypeError, "Expect an integer argument in the range: [0, 2^32 - 1]"); + assert.throws(() => exception.getArg(otherTag, 0), TypeError, "WebAssembly.Exception.getArg(): First argument does not match the exception tag"); +} + +// ToNumber itself can throw; that exception propagates unchanged. +assert.throws(() => exception.getArg(tag, Symbol()), TypeError, ""); +assert.throws(() => exception.getArg(tag, 0n), TypeError, ""); +{ + const error = new Error("from valueOf"); + assert.throwsExactly(() => exception.getArg(tag, { valueOf() { throw error; } }), error); +} diff --git a/JSTests/wasm/v8/exceptions-api.js b/JSTests/wasm/v8/exceptions-api.js index 91d53681456c6..9a48dbd2e6de5 100644 --- a/JSTests/wasm/v8/exceptions-api.js +++ b/JSTests/wasm/v8/exceptions-api.js @@ -200,8 +200,8 @@ function TestGetArgHelper(types_str, types, values) { /First argument must be a WebAssembly.Tag/); assertThrows(() => exception.getArg({}, 0), TypeError, /First argument must be a WebAssembly.Tag/); - //assertThrows(() => exception.getArg(tag, undefined), TypeError, - ///Index must be convertible to a valid number/); + assertThrows(() => exception.getArg(tag, undefined), TypeError, + /Expect an integer argument in the range: \[0, 2\^32 - 1\]/); assertThrows(() => exception.getArg(tag, 0xFFFFFFFF), RangeError, /Index out of range/); let wrong_tag = new WebAssembly.Tag({parameters: ['i32']}); diff --git a/LayoutTests/imported/w3c/web-platform-tests/wasm/jsapi/exception/getArg.tentative.any-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/wasm/jsapi/exception/getArg.tentative.any-expected.txt index 383f6cad7eee1..723d5821bffa0 100644 --- a/LayoutTests/imported/w3c/web-platform-tests/wasm/jsapi/exception/getArg.tentative.any-expected.txt +++ b/LayoutTests/imported/w3c/web-platform-tests/wasm/jsapi/exception/getArg.tentative.any-expected.txt @@ -2,6 +2,8 @@ PASS Missing arguments PASS Invalid exception argument PASS Index out of bounds -PASS Getting out-of-range argument +FAIL Getting out-of-range argument assert_throws_js: function "() => exn.getArg(tag, value)" threw object "TypeError: Expect an integer argument in the range: [0, 2^32 - 1]" ("TypeError") expected instance of function "function RangeError() { + [native code] +}" ("RangeError") PASS getArg diff --git a/LayoutTests/imported/w3c/web-platform-tests/wasm/jsapi/exception/getArg.tentative.any.worker-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/wasm/jsapi/exception/getArg.tentative.any.worker-expected.txt index 383f6cad7eee1..723d5821bffa0 100644 --- a/LayoutTests/imported/w3c/web-platform-tests/wasm/jsapi/exception/getArg.tentative.any.worker-expected.txt +++ b/LayoutTests/imported/w3c/web-platform-tests/wasm/jsapi/exception/getArg.tentative.any.worker-expected.txt @@ -2,6 +2,8 @@ PASS Missing arguments PASS Invalid exception argument PASS Index out of bounds -PASS Getting out-of-range argument +FAIL Getting out-of-range argument assert_throws_js: function "() => exn.getArg(tag, value)" threw object "TypeError: Expect an integer argument in the range: [0, 2^32 - 1]" ("TypeError") expected instance of function "function RangeError() { + [native code] +}" ("RangeError") PASS getArg diff --git a/Source/JavaScriptCore/wasm/js/WebAssemblyExceptionPrototype.cpp b/Source/JavaScriptCore/wasm/js/WebAssemblyExceptionPrototype.cpp index 8c1fe0f3b42be..5405b9708992a 100644 --- a/Source/JavaScriptCore/wasm/js/WebAssemblyExceptionPrototype.cpp +++ b/Source/JavaScriptCore/wasm/js/WebAssemblyExceptionPrototype.cpp @@ -115,7 +115,7 @@ JSC_DEFINE_HOST_FUNCTION(webAssemblyExceptionProtoFuncGetArg, (JSGlobalObject* g if (!tag) [[unlikely]] return throwVMTypeError(globalObject, throwScope, formatMessage("First argument must be a WebAssembly.Tag"_s)); - uint32_t index = toNonWrappingUint32(globalObject, callFrame->argument(1), ErrorType::RangeError); + uint32_t index = toNonWrappingUint32(globalObject, callFrame->argument(1)); RETURN_IF_EXCEPTION(throwScope, { }); if (jsException->tag() != tag->tag()) [[unlikely]]