← WebKit Silent-Fix Report — 2026-W22

57df341619  [Wasm] Add missing exception check after rope resolution

severity medium class Other confidence 0.60 JSC Wasm builtins exploitable-grade
Sosuke Suzuki Tue May 26 17:41:12 2026 -0700 full: 57df341619eda217f1553fbf6ac9f0497157296b bug report ↗ view on GitHub ↗
Primitive: execution continues with pending exception 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

  1. Design — Both builtins resolve a JSString via value() and immediately consume the contents.
  2. Gap — Neither follows value() with RETURN_IF_EXCEPTION, assuming resolution always succeeds.
  3. Fault — A large rope makes value() throw OutOfMemory, returning invalid data while a pending exception is set.
  4. Misbehavior — intoCharCodeArray loops over invalid data; tryCreate builds a name from a null view and keeps iterating — execution under a live exception.
  5. 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

  1. Build a huge rope — Create a JSString rope large enough that flattening in value() exhausts memory and throws OutOfMemory.
  2. Feed it to a Wasm builtin — Invoke jsstring.intoCharCodeArray on it, or include it in the builtins iterable of WebAssembly.compile options.
  3. 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

Before / after

Loading diff…