57df341619 [Wasm] Add missing exception check after rope resolution
Triage note: Adds RETURN_IF_EXCEPTION after value()/rope resolution, preventing continued execution with a pending exception.
Contents
The bug at a glance
Medium. OBSERVED: two WebAssembly builtin paths call JSString::value()/asString(nextValue)->value() to resolve a possibly-rope string and then continue using the result without a RETURN_IF_EXCEPTION check. JSString::value() can throw an OutOfMemory exception while resolving a rope; when it does, the returned string view is empty/invalid and there is a pending exception on the VM. Continuing to iterate over stringLength char codes (intoCharCodeArray) or to build makeString(“wasm:”, StringView(contents)) with a null/empty resolved string, while an exception is live, is an exception-safety violation that can corrupt control flow / produce inconsistent results. INFERRED: exploitability is limited — triggering requires forcing rope-resolution OOM — and the immediate effect is a missing bail-out rather than a direct write, so medium.
JSString::value() can throw (OOM) when flattening a rope, but two Wasm builtin paths ignored the pending exception and kept operating on the null/empty resolved string.
Root cause
In JSC, a JSString may be a rope — an unresolved concatenation tree. Calling value(globalObject) forces the rope to be flattened into a contiguous backing store. That resolution allocates, and for very large ropes it can fail and throw an OutOfMemory error; on that path value() returns an empty/invalid string result and sets a pending exception on the VM. The universal JSC discipline is to follow any operation that can throw with RETURN_IF_EXCEPTION(scope, …) so execution unwinds instead of proceeding with a bogus value while an exception is live.
The first site, WebAssemblyBuiltin.cpp’s jsstring.intoCharCodeArray implementation, resolves auto stringValue = string->value(globalObject) and then immediately dereferences stringValue.data — checking stringValue.data.is8Bit() and looping for size_t i = 0; i < stringLength; ++i over the character data to fill an array. If value() threw, stringValue.data is not a valid resolved buffer, yet the loop reads stringLength characters from it, and the builtin returns a result computed from garbage while the VM carries a pending exception. The fix inserts RETURN_IF_EXCEPTION(scope, { }); immediately after the value() call, bailing out (returning a default I32 result) before the data is touched.
The second site, WebAssemblyCompileOptions::tryCreate, iterates a JS iterable of builtin-set names via forEachInIterable; for each string element it does auto contents = asString(nextValue)->value(globalObject) then String qualifiedName = makeString(“wasm:"_s, StringView(contents)) and appends to options.m_qualifiedBuiltinSetNames. If value() threw on a rope, contents is invalid and makeString would build a name from a null/empty view, and iteration would continue accumulating bogus entries under a pending exception. The fix inserts RETURN_IF_EXCEPTION(scope, void()); right after the value() call so the iteration callback aborts.
Both are the same class of defect: a throwing rope resolution whose exception was not checked before the resolved contents were consumed.
Key code
Missing exception checks added after rope-resolving value() calls
auto stringValue = string->value(globalObject);
+ RETURN_IF_EXCEPTION(scope, { });
if (stringValue.data.is8Bit()) {
...
auto contents = asString(nextValue)->value(globalObject);
+ RETURN_IF_EXCEPTION(scope, void());
String qualifiedName = makeString("wasm:"_s, StringView(contents));
Patch walkthrough
Source/JavaScriptCore/wasm/js/WebAssemblyBuiltin.cpp— In the jsstring.intoCharCodeArray builtin (DEFINE_BUILTIN_IMPLEMENTATION_I32), RETURN_IF_EXCEPTION(scope, { }); is added directly after auto stringValue = string->value(globalObject);, before stringValue.data is inspected and the stringLength-long copy loop runs. This bails out with a default result if rope resolution threw OOM.Source/JavaScriptCore/wasm/js/WebAssemblyCompileOptions.cpp— In WebAssemblyCompileOptions::tryCreate’s forEachInIterable callback over builtin names, RETURN_IF_EXCEPTION(scope, void()); is added after auto contents = asString(nextValue)->value(globalObject);, before makeString(“wasm:”, StringView(contents)) and the append, so a throwing resolution aborts the iteration instead of appending a bogus qualified name.
Background
JSString rope — A lazily-concatenated string represented as a tree; value()/resolve() flattens it into contiguous storage and can throw OutOfMemory for large ropes.
RETURN_IF_EXCEPTION — JSC macro that, if the VM has a pending exception, returns the given value immediately; the standard guard after any operation that may throw.
intoCharCodeArray builtin — WebAssembly JS-string builtin that copies a JSString’s characters into an array; it reads stringLength code units from the resolved data.
WebAssemblyCompileOptions::tryCreate — Parses the compile options object, iterating a builtins iterable and building ‘wasm:’-prefixed qualified builtin-set names from each string.
forEachInIterable — JSC helper to iterate a JS iterable with a callback; a pending exception in the callback must be surfaced via RETURN_IF_EXCEPTION to stop iteration.
Vulnerability window
- Design — Both builtins resolve a JSString via value() and immediately consume the contents.
- Gap — Neither follows value() with RETURN_IF_EXCEPTION, assuming resolution always succeeds.
- Fault — A large rope makes value() throw OutOfMemory, returning invalid data while a pending exception is set.
- Misbehavior — intoCharCodeArray loops over invalid data; tryCreate builds a name from a null view and keeps iterating — execution under a live exception.
- Fix (313931@main) — RETURN_IF_EXCEPTION added right after each value() call to bail out cleanly.
Triggering
No test added in the diff. Trigger concept: construct an extremely large rope string (repeated concatenation) and pass it either to the WebAssembly JS-string intoCharCodeArray builtin, or as an element of the builtins iterable in WebAssembly compile options, under memory pressure so that value()’s rope flattening throws OutOfMemory; pre-fix the code proceeds to read/format the unresolved string while an exception is pending, post-fix it bails out immediately.
Exploitation
- Build a huge rope — Create a JSString rope large enough that flattening in value() exhausts memory and throws OutOfMemory.
- Feed it to a Wasm builtin — Invoke jsstring.intoCharCodeArray on it, or include it in the builtins iterable of WebAssembly.compile options.
- Exercise the unchecked path — Pre-fix, value() throws but execution continues — reading stringLength code units from invalid data, or formatting makeString over a null StringView — an exception-safety violation with inconsistent state until the exception is eventually noticed.
Detection & hunting
For defenders and SOC / detection engineers:
- value() calls without RETURN_IF_EXCEPTION —
- Exception-scope verification under debug —
Audit directions
- All JSString::value() consumers in wasm/js —
- forEachInIterable callbacks —
- Rope-resolution OOM paths —