Medium CVSS 7.5 webkit Logic Error 🔧 Commit mapped

Overview

Medium
Severity
7.5
CVSS
No
Exploited ITW
Fixed
Fix Status
DescriptionProcessing maliciously crafted web content may lead to an unexpected process crash
ComponentWebKit
Bug ClassLogic Error
Tracker309628
Fix commit9161e71798e9 (WebKit/WebKit) +53/-0
CWECWE-119, CWE-120 (Buffer bounds error, Buffer overflow)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedMaher Azzouzi
Disclosed2026-05-11

Background

WebAssembly locals
Function-scoped variables declared in the code section that the engine must reserve and zero-initialize in the function’s stack frame before executing the body.
LEB128
A variable-length integer encoding used throughout the Wasm binary format for counts and lengths, which the test’s leb() helper reproduces.
Stack-limit check
A guard the VM performs on function entry to ensure the frame (including all locals) fits within the remaining native/JS stack, throwing a catchable error instead of overrunning.
Agent / worker
A separate execution thread (started via $.agent.start in JSC test shells) that generally has a smaller native stack, lowering the threshold at which frame allocation overflows.
Guard page / stack overflow
A protected region past the stack whose access faults; writing beyond a mis-checked frame into it causes an uncontrolled process crash rather than a language-level exception.

Root Cause Analysis

The commit adds only a WebAssembly regression test (JSTests/wasm/regress/309628.js); the actual fix to the JSC WebAssembly engine is NOT contained in this diff, so the mechanism below is an inference grounded in what the test exercises. The test hand-assembles a minimal Wasm module whose single exported function ‘f’ declares a very large number of i64 locals (the local-declaration entry ‘0x01, …leb(numLocals), 0x7e’ where 0x7e is i64), followed by a trivial body ’local.get 0; drop; end’. A driver sweeps numLocals from 35000 down to 8000 in steps of 2000, instantiating and calling the function until one succeeds without throwing. Crucially the whole thing runs inside a spawned agent/worker ($.agent.start) which typically has a smaller, differently-sized native stack than the main thread. The pattern is the classic ‘many locals’ stress case: each declared local must be reserved and zero-initialized in the function’s stack frame, so a function with tens of thousands of i64 locals demands a very large frame. The violated invariant is that the engine must reserve stack space for all locals and verify it fits within the available native/JS stack (a stack-limit check) before entering the function body; if the required frame size exceeds the thread stack and the check is missing, mis-ordered, or computed with a value that can overflow/wrap, execution runs off the end of the guard region and the process crashes instead of throwing a catchable Wasm/RangeError. The sweep from high to low local counts and the try/catch that breaks on success show the author probing for the exact frame size that lands in the unhandled window between ‘still throws cleanly’ and ‘crashes’, which is the signature of a boundary/stack-check logic error. Running in a worker with a tighter stack makes the vulnerable window reachable at a lower local count. The upstream fix (not shown) most plausibly tightens the stack/frame-size check so that an over-large locals frame produces a clean out-of-memory/stack-exhaustion exception on every thread rather than an uncontrolled overrun. This is consistent with the bug class (LogicError) and stated impact (unexpected process crash).
Key insight
This is a stack/frame-size boundary check failure in WebAssembly function entry: a function declaring an enormous number of locals can produce a frame that overruns a smaller worker-thread stack when the stack-limit check is missing or mis-computed, turning a should-be-catchable resource error into a process crash (mechanism inferred; only the regression test is in this commit).

Attack Path

  1. Craft an oversized-locals module Programmatically emit a valid Wasm binary whose exported function declares a huge count of i64 locals (test uses up to 35000) and an otherwise trivial body (local.get 0; drop; end).
  2. Move execution onto a constrained stack Instantiate and invoke the function from a worker/agent thread ($.agent.start), whose smaller native stack makes the required locals frame overflow at achievable local counts.
  3. Sweep the local count to hit the crash window Loop the local count across a range (35000 down to 8000), instantiating and calling each module, catching exceptions, to find the size that falls in the gap where the missing/incorrect stack check fails to reject the frame.
  4. Trigger the overrun Calling the function allocates/initializes the oversized frame past the guard region because the stack-limit check does not reject it, corrupting or exhausting the stack (inferred mechanism).
  5. Observe the process crash The WebContent process terminates abnormally rather than throwing a catchable error, achieving denial of service.

Impact Assessment

The demonstrated primitive is an uncontrolled process crash (denial of service) in the WebContent process, driven by native stack exhaustion from an over-large Wasm locals frame; this matches the CVE’s ‘unexpected process crash’ impact and medium severity. As shown, the attacker controls only the frame size, not written contents, so this is a reliability/DoS bug rather than a memory-write primitive; escalation to code execution would require an additional, separate primitive not established by this commit. It is confined to the sandboxed WebContent (JavaScript/Wasm) process. Because the crash depends on hitting a narrow size window that varies with thread stack size, exploitation as a stable, weaponizable memory corruption is not supported by anything in this diff (inference, since the engine fix is not shown).

Changed Functions

FunctionChangeNotes
leb
JSTests/wasm/regress/309628.js
added Test helper that encodes an integer as unsigned LEB128, used to emit the local count and section lengths in the raw module bytes.
makeModule
JSTests/wasm/regress/309628.js
added Builds a Uint8Array Wasm module with an exported function declaring numLocals i64 locals and a local.get/drop body; the payload generator for the bug.
(worker driver / regression harness)
JSTests/wasm/regress/309628.js
added Inline worker source that sweeps local counts, instantiates and calls the module inside $.agent.start, then sleeps; reproduces the crash under a constrained stack. The actual engine fix is not present in this commit's diff.

Files Changed

  • JSTests/wasm/regress/309628.js

Audit Directions

  • Wasm locals-frame stack checks
    In JSC’s WebAssembly frame-setup and function-entry code, review where the locals count drives frame size; grep for ’numLocals’, ’localCount’, ‘reserveStack’, ‘stackCheck’, ‘frameSize’ and confirm the size computation cannot overflow and is validated against the actual thread stack limit before initialization.
  • Thread-dependent stack limits
    Audit code that assumes main-thread stack size; search for ‘stackLimit’, ’m_softStackLimit’, ‘Thread::stack’, ‘WTFThreadData’ usage in Wasm compilation/interpretation to ensure worker/agent threads with smaller stacks enforce the same guards the reproducer relies on.
  • Other unbounded per-function counts
    Look for other Wasm section entities scaled directly by attacker-controlled counts (locals, parameters, results, br_table targets, function-body size); grep the binary parser/validator for count reads via LEB128 that lack an upper bound or overflow-safe multiplication before allocation.

Original Bug Report

The reporter's bug is still restricted on the tracker.