[6] TypedArray constructor length read keyed on opaque-taint bit instead of Proxy identity
Low. The patch narrows which opaque-tainted slots take an observable [[Get]], and the supplied context shows no path from that difference to a size mismatch between allocation and fill. A memory-safety consequence would need the allocation length handed to createUninitialized to diverge from what the copy loop assumes.
Engine builtins that size a fresh buffer from an object's length must choose between a cheap side-effect-free lookup and a full observable property read. When new Float64Array(x) receives a non-iterable object, JSC looks up length with a PropertySlot (the stack record of a property lookup), and exotic objects such as Proxies and module namespaces can mark that slot as opaque-tainted, meaning the result cannot be trusted as a plain value. The expectation is that the recovery path chosen for a tainted slot is valid for every object kind that can produce the taint.
The angle: any script can build array-likes backed by a Proxy or a module namespace and steer which route JSC takes to compute a typed array's length; the open question is whether that steering can ever desync allocation from fill.
Don't use a module namespace
lengthbinding as a TypedArray length.VMInquirytaints both a Proxy and a module namespace. The earlier Proxy fix then readlengthfor every tainted object, so a namespace export namedlengthbecame the typed array's length. The patch readslengththrough a full[[Get]]only when the inquiry hit a Proxy, including one on the prototype chain. A module namespace keeps the inquiry result, which does not read thelengthbinding.
Source/JavaScriptCore/runtime/JSGenericTypedArrayViewConstructorInlines.h
JSTests/stress/typedarray-constructor-proxy-array-like.js
Patch Details
The patch rewrites the array-like length computation in constructGenericTypedArrayViewWithArguments, the path taken when a TypedArray is built from a non-iterable object. Before the patch there were three branches: an opaque-tainted slot did a full object->get(globalObject, vm.propertyNames->length), an unset slot used length 0, and anything else called lengthSlot.getValue(...). Now the code reads lengthSlot.slotBase() first and performs the full [[Get]] only when the slot is opaque-tainted and the base is a non-null ProxyObjectType. Every other populated slot goes through getValue(...), and every unset slot keeps jsUndefined(), which toLength converts to 0. The three tails merge into one toLength call with a single exception check.
The net behavioral change is confined to opaque-tainted slots whose base is not a ProxyObject: a populated one uses its own value instead of a re-read, and an unset one yields length 0 instead of a full [[Get]]. The two new stress cases put a Proxy on the prototype chain. The first requires the trap log Symbol(Symbol.iterator),length,0,1 for Object.create(protoProxy), and the second requires TypeError for a revoked Proxy on the chain. Both guard the Proxy path the narrowed condition must keep, and neither exercises a non-Proxy tainted slot.
An overloaded opaque-result flag shared by heterogeneous object kinds is consumed as if it identified one kind.
Background
Array-like construction.
When new Float64Array(x) receives an object that is not an ArrayBuffer, a typed array or an iterable, the spec computes LengthOfArrayLike(x) (ToLength(Get(x, "length"))), allocates a typed array of that size, then reads x[0] through x[length-1] and stores each converted element.
PropertySlot and VMInquiry.
PropertySlot is JSC's stack-allocated record of a property lookup. It holds the found value or accessor, the slotBase() object where the property was found (possibly an ancestor on the prototype chain), a cacheability flag and an m_isTaintedByOpaqueObject bit. InternalMethodType::VMInquiry is a lookup mode in which the engine is only poking around: the slot installs DisallowVMEntry, and getOwnPropertySlot implementations must not perform user-observable actions such as calling JS.
Opaque taint.
Exotic objects whose lookups cannot be answered purely mark the slot with setIsTaintedByOpaqueObject(), and PropertySlot::getPureResult then reports jsNull() instead of a cached value.
Proxies and module namespaces.
ProxyObject (JSType ProxyObjectType) forwards property operations to user-supplied handler traps. A get trap is user JS and runs only under a real [[Get]], and a revoked Proxy throws TypeError on any trap operation. JSModuleNamespaceObject exposes a module's exports as non-extensible own properties whose values live in the module environment's variable slots. Deferred namespaces, created by import defer, evaluate the underlying module synchronously on first non-inquiry property access via ensureDeferredNamespaceEvaluation.
Analysis
This is a logic error: a mis-dispatch of the length-recovery path on an overloaded taint flag. The pre-fix consumer chose its recovery from the aggregate bit isTaintedByOpaqueObject() alone, discarding any tainted slot and replacing it with a full object->get(length). That recovery is correct for a Proxy, whose get trap is user JS and whose answer cannot be known without the observable [[Get]]; the prototype-chain tests confirm this is the behavior the fix keeps. The bit is not Proxy-specific, though. JSModuleNamespaceObject::getOwnPropertySlotCommon also calls slot.setIsTaintedByOpaqueObject(), so the old consumer applied Proxy-shaped recovery to every producer of the bit. The fix keys the full [[Get]] on the concrete identity of slotBase() instead.
tainted length slot, by producer
Before: tainted ──► full object->get(length) (Proxy and namespace alike)
unset ──► 0
else ──► slot.getValue(...)
After: tainted && slotBase is ProxyObject ──► full object->get(length)
populated (incl. tainted non-Proxy) ──► slot.getValue(...)
unset (incl. tainted non-Proxy) ──► undefined ──► toLength ──► 0
For a module namespace, the commit message states the motivation: with the old logic an export named length was read as the typed array length, and the fixed code keeps the inquiry result, which does not read the length binding. What this changes observably is not settled by the supplied context. The non-inquiry branch of getOwnPropertySlotCommon fills the slot via setValueModuleNamespace(this, ..., value, ...) with the export binding's value, and a full [[Get]] on a namespace reads that same binding, which is what spec LengthOfArrayLike requires. A synchronous evaluation of a deferred namespace on a real [[Get]] is likewise expected behavior. It also cannot be determined whether lengthSlot is populated under VMInquiry, since its construction and the VMInquiry branch of getOwnPropertySlotCommon fall in the truncated part of the source. So which non-Proxy producer observably changes behavior, and what concrete harm the pre-fix full [[Get]] caused for it, remains open. No non-Proxy test was added.
Exploitability: a memory-safety consequence would require the allocation length passed to ViewClass::createUninitialized to diverge from what the element-copy loop assumes. That loop is outside the supplied context, and both new tests appear to pass under the pre-patch code too, since a Proxy-based tainted slot took the full [[Get]] path before and after. A secondary consequence of the narrowing is that unset-but-tainted slots from non-Proxy producers now resolve to length 0. If any such producer can leave the slot unset while the property exists, the length would be 0 where it was previously read.
The boundary at stake is internal: the contract between a PropertySlot lookup's taint signal and the native builtin that decides how to recover from it. No cross-origin or sandbox boundary is involved, and the likely consequence is a spec-consistency or observability difference in how the length is sourced.
Insight: a coarse "result is incomplete" bit tells the consumer that it must recover, but not how. Recovery should be keyed on the specific source of opacity, which is what the added prototype-chain tests check: that the narrower slotBase() key still catches a Proxy that is an ancestor rather than the receiver, including a revoked one.
Audit directions
- One flag, several producers. A single taint or opacity flag is set by several object kinds and consumed as if it identified one. The invariant is that the recovery path chosen for an opaque result is valid for every producer of that flag. Narrow: grep JavaScriptCore/runtime for
isTaintedByOpaqueObject()consumers (for examplePropertySlot::getPureResult, other builtins and IC helpers) and, for each, enumerate whichgetOwnPropertySlotimplementations callsetIsTaintedByOpaqueObject()(ProxyObject, JSModuleNamespaceObject, and any others). Wider: other aggregate bits onPropertySlotandPutPropertySlot(cacheability,disableCaching, custom-accessor flags) for consumers assuming a single producer. Widest: any engine or analyzer that collapses heterogeneous "unknown" sources into one bit, such as V8 interceptor-backed lookups, SpiderMonkey resolve hooks, or taint tracking in static analyzers. The tell is a branch on the aggregate flag whose body performs an operation (an observable Get, a re-read, a cache bypass) correct for only one producer. - Allocation length and fill length from one read. Check native builtins that compute an array-like length once and then fill a freshly allocated, uninitialized store. The invariant is that the element count used for allocation and the count used for filling come from one read with no divergent path. Narrow: start with the remainder of
constructGenericTypedArrayViewWithArguments,TypedArray.fromand%TypedArray%.prototype.set's array-like path, and confirm every index written to acreateUninitializedresult is bounded by the samelengthlocal. Wider:Array.from,Function.prototype.applyargument spreading, andfillArgList-style helpers whose length can come from either a pure inquiry or an observable Get. Widest: this is the double-fetch / TOCTOU class, the same shape as kernelcopy_from_userdouble-fetch bugs and FFI layers that size a buffer from one query and copy with another. The tell is two distinct expressions producing "the length" in one function, at least one of which can run user code or take a different source. - Tainted-but-unset lookups defaulted without a real Get. Audit consumers that turn a tainted-but-unset lookup into a default value (0, undefined, absent) instead of performing the real
[[Get]]. The invariant is that a default is safe only when the producer guarantees "unset" means "property absent" rather than "answer withheld". Narrow: for each non-Proxy caller ofsetIsTaintedByOpaqueObject()(start withJSModuleNamespaceObject::getOwnPropertySlotCommon), check whether it can leave the slot unset underVMInquirywhile the property exists, which would now makeconstructGenericTypedArrayViewWithArgumentscompute length 0. Wider: WebCore bindings with named-property getters and lazily materialized properties, where an inquiry can legitimately decline to answer. Widest: any pure-query/effectful-read split, such as Python'shasattrversus__getattr__or reflection APIs that skip getters, where "no answer" is easy to conflate with "no property". The tell is anisUnset()check that maps to a default without first ruling out the opaque-producer case.