CVE-2026-43705
Overview
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.
Attack Path
- Poison Array iteration Override Array.prototype[Symbol.iterator] to return attacker-chosen objects for the internal 3-element result.
- Construct a TransformStream new TransformStream() triggers createInternalTransformStream, which reads the poisoned results via convert<IDLSequence<IDLObject>>.
- Substitute wrong types Supply a non-stream object for the readable/writable slot so dynamicDowncast yields nullptr / a confused object.
- Type-confused dereference ->wrapped() on the null/attacker object dereferences controlled memory, corrupting WebContent process state.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
createInternalTransformStreamSource/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.txtLayoutTests/streams/transform-stream-poisoned-iterator-crash.htmlSource/WebCore/Modules/streams/TransformStream.cpp
Audit Directions
- Same pattern: unchecked downcasts after iterationGrep WebCore for dynamicDowncast<…>(…)->… immediately after a convert<IDLSequence<…>> / JS-iterable read, without a null check.
- ASSERT on JS-controlled shapeLook 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.