Skip to content

Commit 8f27e23

Browse files
committed
[JSC] TypedArray.from() spuriously throws when mapFn detaches or shrinks
https://bugs.webkit.org/show_bug.cgi?id=318596 Reviewed by Yusuke Suzuki. The guards added in 316137@main stop writing when mapFn detaches or shrinks the source/result typed array, but they consult @typedArrayLength before (or instead of) checking out-of-bounds-ness, and @typedArrayLength throws a TypeError for detached or out-of-bounds views. As a result, const rab = new ArrayBuffer(32, { maxByteLength: 64 }); const source = new Int32Array(rab, 0, 8); source[Symbol.iterator] = null; Int32Array.from(source, (v, k) => { if (k === 4) rab.resize(16); return v; }); throws a TypeError where the spec requires Get/Set on the now out-of-bounds view to be silent. The same happens when mapFn detaches the result's buffer, where the intended @isDetached(result) operand was unreachable dead code. This change adds a non-throwing @isTypedArrayOutOfBounds private function exposing JSArrayBufferView::isOutOfBounds() (the spec's IsArrayBufferViewOutOfBounds, which also covers detached views) and makes both guards check it before calling @typedArrayLength. Test: JSTests/stress/typedarray-from-oob-not-detached.js * JSTests/stress/typedarray-from-oob-not-detached.js: Added. (shouldBe): (testResultShrunkOutOfBounds.FixedLengthOnResizable): (testResultShrunkOutOfBounds): (testResultDetached.ViewOnTransferableBuffer): (testResultDetached): * Source/JavaScriptCore/builtins/BuiltinNames.h: * Source/JavaScriptCore/builtins/TypedArrayConstructor.js: (from): * Source/JavaScriptCore/bytecode/LinkTimeConstant.h: * Source/JavaScriptCore/runtime/JSGlobalObject.cpp: (JSC::JSGlobalObject::init): * Source/JavaScriptCore/runtime/JSTypedArrayViewPrototype.cpp: (JSC::JSC_DEFINE_HOST_FUNCTION): * Source/JavaScriptCore/runtime/JSTypedArrayViewPrototype.h: Canonical link: https://commits.webkit.org/316525@main
1 parent 7f655a4 commit 8f27e23

7 files changed

Lines changed: 89 additions & 2 deletions

File tree

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
function shouldBe(actual, expected) {
2+
if (actual !== expected)
3+
throw new Error(`bad value: ${actual}, expected: ${expected}`);
4+
}
5+
6+
// Source is a fixed-length view on a resizable ArrayBuffer. Shrinking the
7+
// buffer inside mapFn makes the view out-of-bounds without detaching it.
8+
// TypedArray.from must stop writing without throwing.
9+
function testSourceShrunkOutOfBounds() {
10+
const rab = new ArrayBuffer(32, { maxByteLength: 64 });
11+
const source = new Int32Array(rab, 0, 8);
12+
for (let i = 0; i < 8; i++)
13+
source[i] = i + 1;
14+
source[Symbol.iterator] = null;
15+
16+
let calls = 0;
17+
const result = Int32Array.from(source, (value, k) => {
18+
calls++;
19+
if (k === 4)
20+
rab.resize(16);
21+
return value;
22+
});
23+
shouldBe(calls, 5);
24+
shouldBe(result.length, 8);
25+
shouldBe(result[4], 5);
26+
shouldBe(result[5], 0);
27+
}
28+
29+
// Result is a fixed-length view on a resizable ArrayBuffer. Shrinking the
30+
// buffer inside mapFn makes the result out-of-bounds without detaching it.
31+
function testResultShrunkOutOfBounds() {
32+
let rab;
33+
class FixedLengthOnResizable extends Int32Array {
34+
constructor(length) {
35+
rab = new ArrayBuffer(4 * length, { maxByteLength: 8 * length });
36+
super(rab, 0, length);
37+
}
38+
}
39+
40+
let calls = 0;
41+
const result = FixedLengthOnResizable.from({ length: 8 }, (value, k) => {
42+
calls++;
43+
if (k === 2)
44+
rab.resize(4);
45+
return k;
46+
});
47+
shouldBe(calls, 3);
48+
}
49+
50+
// Result's buffer is detached by transfer() inside mapFn.
51+
function testResultDetached() {
52+
let buffer;
53+
class ViewOnTransferableBuffer extends Int32Array {
54+
constructor(length) {
55+
buffer = new ArrayBuffer(4 * length);
56+
super(buffer);
57+
}
58+
}
59+
60+
let calls = 0;
61+
const result = ViewOnTransferableBuffer.from({ length: 8 }, (value, k) => {
62+
calls++;
63+
if (k === 2)
64+
buffer.transfer();
65+
return k;
66+
});
67+
shouldBe(calls, 3);
68+
}
69+
70+
for (let i = 0; i < testLoopCount; i++) {
71+
testSourceShrunkOutOfBounds();
72+
testResultShrunkOutOfBounds();
73+
testResultDetached();
74+
}

‎Source/JavaScriptCore/builtins/BuiltinNames.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ namespace JSC {
142142
macro(isSharedTypedArrayView) \
143143
macro(isResizableOrGrowableSharedTypedArrayView) \
144144
macro(isDetached) \
145+
macro(isTypedArrayOutOfBounds) \
145146
macro(typedArrayFromFast) \
146147
macro(instanceOf) \
147148
macro(isArray) \

‎Source/JavaScriptCore/builtins/TypedArrayConstructor.js‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,14 +109,14 @@ function from(items /* [ , mapfn [ , thisArg ] ] */)
109109
@throwTypeError("TypedArray.from constructed typed array of insufficient length");
110110

111111
for (var k = 0; k < arrayLikeLength; k++) {
112-
if (@isTypedArrayView(arrayLike) && (@isDetached(arrayLike) || k >= @typedArrayLength(arrayLike)))
112+
if (@isTypedArrayView(arrayLike) && (@isTypedArrayOutOfBounds(arrayLike) || k >= @typedArrayLength(arrayLike)))
113113
break;
114114
var value = arrayLike[k];
115115
if (mapFn === @undefined)
116116
result[k] = value;
117117
else {
118118
var mapped = thisArg === @undefined ? mapFn(value, k) : mapFn.@call(thisArg, value, k);
119-
if (@isTypedArrayView(result) && (k >= @typedArrayLength(result) || @isDetached(result)))
119+
if (@isTypedArrayView(result) && (@isTypedArrayOutOfBounds(result) || k >= @typedArrayLength(result)))
120120
break;
121121
result[k] = mapped;
122122
}

‎Source/JavaScriptCore/bytecode/LinkTimeConstant.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ class JSGlobalObject;
8080
v(isResizableOrGrowableSharedTypedArrayView, nullptr) \
8181
v(typedArrayFromFast, nullptr) \
8282
v(isDetached, nullptr) \
83+
v(isTypedArrayOutOfBounds, nullptr) \
8384
v(isFinite, nullptr) \
8485
v(instanceOf, nullptr) \
8586
v(BuiltinLog, nullptr) \

‎Source/JavaScriptCore/runtime/JSGlobalObject.cpp‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2076,6 +2076,9 @@ capitalName ## Constructor* lowerName ## Constructor = featureFlag ? capitalName
20762076
m_linkTimeConstants[static_cast<unsigned>(LinkTimeConstant::isDetached)].initLater([] (const Initializer<JSCell>& init) {
20772077
init.set(JSFunction::create(init.vm, init.owner, 1, "typedArrayViewIsDetached"_s, typedArrayViewPrivateFuncIsDetached, ImplementationVisibility::Private));
20782078
});
2079+
m_linkTimeConstants[static_cast<unsigned>(LinkTimeConstant::isTypedArrayOutOfBounds)].initLater([] (const Initializer<JSCell>& init) {
2080+
init.set(JSFunction::create(init.vm, init.owner, 1, "typedArrayViewIsOutOfBounds"_s, typedArrayViewPrivateFuncIsOutOfBounds, ImplementationVisibility::Private));
2081+
});
20792082
m_linkTimeConstants[static_cast<unsigned>(LinkTimeConstant::instanceOf)].initLater([] (const Initializer<JSCell>& init) {
20802083
init.set(JSFunction::create(init.vm, init.owner, 0, "instanceOf"_s, objectPrivateFuncInstanceOf, ImplementationVisibility::Private));
20812084
});

‎Source/JavaScriptCore/runtime/JSTypedArrayViewPrototype.cpp‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,13 @@ JSC_DEFINE_HOST_FUNCTION(typedArrayViewPrivateFuncIsDetached, (JSGlobalObject*,
6868
return JSValue::encode(jsBoolean(uncheckedDowncast<JSArrayBufferView>(argument)->isDetached()));
6969
}
7070

71+
JSC_DEFINE_HOST_FUNCTION(typedArrayViewPrivateFuncIsOutOfBounds, (JSGlobalObject*, CallFrame* callFrame))
72+
{
73+
JSValue argument = callFrame->uncheckedArgument(0);
74+
ASSERT(argument.isCell() && isTypedView(argument.asCell()->type()));
75+
return JSValue::encode(jsBoolean(uncheckedDowncast<JSArrayBufferView>(argument)->isOutOfBounds()));
76+
}
77+
7178
JSC_DEFINE_HOST_FUNCTION(typedArrayViewPrivateFuncLength, (JSGlobalObject* globalObject, CallFrame* callFrame))
7279
{
7380
VM& vm = globalObject->vm();

‎Source/JavaScriptCore/runtime/JSTypedArrayViewPrototype.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ JSC_DECLARE_HOST_FUNCTION(typedArrayViewPrivateFuncIsSharedTypedArrayView);
5656
JSC_DECLARE_HOST_FUNCTION(typedArrayViewPrivateFuncIsResizableOrGrowableSharedTypedArrayView);
5757
JSC_DECLARE_HOST_FUNCTION(typedArrayViewPrivateFuncTypedArrayFromFast);
5858
JSC_DECLARE_HOST_FUNCTION(typedArrayViewPrivateFuncIsDetached);
59+
JSC_DECLARE_HOST_FUNCTION(typedArrayViewPrivateFuncIsOutOfBounds);
5960
JSC_DECLARE_HOST_FUNCTION(typedArrayViewPrivateFuncLength);
6061

6162
} // namespace JSC

0 commit comments

Comments
 (0)