Medium CVSS 8.8 webkit Type Confusion 🔧 Commit mapped

Overview

Medium
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
DescriptionProcessing maliciously crafted web content may lead to memory corruption
ComponentWebCore Streams
Bug ClassType Confusion
Tracker314528
Fix commit8fd92b1021d3 (WebKit/WebKit) +102/-2
CWECWE-843 (Type confusion)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
Crediteddr3dd
Disclosed2026-06-29

Background

TransformStream internals
WebKit builds a TransformStream’s readable/writable from a JS-created 3-element result read back into C++.
IDLSequence conversion
convert<IDLSequence<IDLObject>> iterates a JS iterable using Symbol.iterator, which script can override (‘poison’).
dynamicDowncast
Returns nullptr when the value is not the expected type; dereferencing the result without a null check is a type-confusion bug.

Root Cause Analysis

This fixes a type confusion in TransformStream construction reachable by poisoning Array iteration. createInternalTransformStream calls into JS (createInternalTransformStreamFromTransformer) which returns a 3-element result that WebKit reads via convert<IDLSequence<IDLObject>> — an iteration that goes through Array.prototype[Symbol.iterator] and is therefore attacker-overridable. The pre-patch code ASSERTed results.size() == 3 (compiled out in release) and then built CreateInternalTransformStreamResult with dynamicDowncast<JSReadableStream>(results[1].get())->wrapped() and dynamicDowncast<JSWritableStream>(results[2].get())->wrapped() WITHOUT null-checking the downcasts. By overriding Array.prototype[Symbol.iterator] to yield arbitrary objects (or fewer than three), an attacker substitutes non-stream objects for the readable/writable slots; dynamicDowncast returns nullptr for a wrong type, and calling ->wrapped() on that nullptr, or treating an arbitrary object as a JSReadableStream/JSWritableStream, dereferences attacker-controlled memory — a type confusion leading to memory corruption.

The fix replaces the ASSERT with a runtime size check that returns a TypeError when results.size() != 3, and null-checks both dynamicDowncasts (returning TypeError if readable or writable is null) before constructing the result with readable->wrapped()/writable->wrapped().

The restored invariant is that values crossing the JS-iterable boundary are validated for count and type before being treated as internal stream objects. The regression test poisons Symbol.iterator to substitute plain objects, an object holding a heap pointer, and a truncated iterator, expecting TypeError (and no crash) in all three cases.

Key insight
Values returned across a JS-iterable boundary (Array Symbol.iterator) were trusted for both count and type: a compiled-out ASSERT plus unchecked dynamicDowncasts let a poisoned iterator substitute arbitrary objects for the readable/writable streams.

Attack Path

  1. Poison Array iteration Override Array.prototype[Symbol.iterator] to return attacker-chosen objects for the internal 3-element result.
  2. Construct a TransformStream new TransformStream() triggers createInternalTransformStream, which reads the poisoned results via convert<IDLSequence<IDLObject>>.
  3. Substitute wrong types Supply a non-stream object for the readable/writable slot so dynamicDowncast yields nullptr / a confused object.
  4. Type-confused dereference ->wrapped() on the null/attacker object dereferences controlled memory, corrupting WebContent process state.

Impact Assessment

A type-confusion / memory-corruption primitive in the WebContent process, triggerable from ordinary script by overriding Symbol.iterator so attacker-chosen objects are treated as internal stream objects. The advisory explicitly cites memory corruption; treating a controlled object as a JSReadableStream/JSWritableStream and dereferencing it is a strong route toward arbitrary read/write and code execution.

Changed Functions

FunctionChangeNotes
createInternalTransformStream
Source/WebCore/Modules/streams/TransformStream.cpp
modified Replaces ASSERT(results.size()==3) with a runtime TypeError check and null-checks the JSReadableStream/JSWritableStream downcasts before calling wrapped(), rejecting substituted/wrong-type values.

Files Changed

  • LayoutTests/streams/transform-stream-poisoned-iterator-crash-expected.txt
  • LayoutTests/streams/transform-stream-poisoned-iterator-crash.html
  • Source/WebCore/Modules/streams/TransformStream.cpp

Audit Directions

  • Same pattern: unchecked downcasts after iteration
    Grep WebCore for dynamicDowncast<…>(…)->… immediately after a convert<IDLSequence<…>> / JS-iterable read, without a null check.
  • ASSERT on JS-controlled shape
    Look for ASSERT(results.size()==N) or similar assumptions about the count/type of values returned from built-in JS builtins that a poisoned iterator can violate.

Original Bug Report

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