Bug 1317384 - Update TypedArray.prototype.set to be compliant with latest ECMA2017. r=lth
authorAndré Bargull <andre.bargull@gmail.com>
Fri, 17 Feb 2017 08:27:13 -0800
changeset 372831 4a930acd18f8de343fc8ae6eafcd0b12798ea69c
parent 372830 f6603d52cd90e9b7b883067fba9278cbdb2ebdbf
child 372832 9f91a792c4f9fa1323823ca08ad30a862ab83879
push id10863
push userjlorenzo@mozilla.com
push dateMon, 06 Mar 2017 23:02:23 +0000
treeherdermozilla-aurora@0931190cd725 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerslth
bugs1317384
milestone54.0a1
Bug 1317384 - Update TypedArray.prototype.set to be compliant with latest ECMA2017. r=lth
js/src/tests/ecma_6/TypedArray/set-detached.js
js/src/tests/ecma_6/TypedArray/set-tointeger.js
js/src/tests/ecma_6/TypedArray/set-toobject.js
js/src/tests/js1_8_5/extensions/typedarray-set-neutering.js
js/src/vm/TypedArrayObject-inl.h
js/src/vm/TypedArrayObject.cpp
new file mode 100644
--- /dev/null
+++ b/js/src/tests/ecma_6/TypedArray/set-detached.js
@@ -0,0 +1,240 @@
+// Tests for detached ArrayBuffer checks in %TypedArray%.prototype.set(array|typedArray, offset).
+
+function* createTypedArrays(lengths = [0, 1, 4, 4096]) {
+    for (let length of lengths) {
+        let buffer = new ArrayBuffer(length * Int32Array.BYTES_PER_ELEMENT);
+        let typedArray = new Int32Array(buffer);
+
+        yield {typedArray, buffer};
+    }
+}
+
+if (typeof detachArrayBuffer === "function") {
+    class ExpectedError extends Error {}
+
+    // No detached check on function entry.
+    for (let {typedArray, buffer} of createTypedArrays()) {
+        detachArrayBuffer(buffer);
+
+        assertThrowsInstanceOf(() => typedArray.set(null, {
+            valueOf() {
+                throw new ExpectedError();
+            }
+        }), ExpectedError);
+    }
+
+    // Check for detached buffer after calling ToInteger(offset). Test with:
+    // - valid offset,
+    // - too large offset,
+    // - and negative offset.
+    for (let [offset, error] of [[0, TypeError], [1000000, TypeError], [-1, RangeError]]) {
+        for (let source of [[], [0], new Int32Array(0), new Int32Array(1)]) {
+            for (let {typedArray, buffer} of createTypedArrays()) {
+                assertThrowsInstanceOf(() => typedArray.set(source, {
+                    valueOf() {
+                        detachArrayBuffer(buffer);
+                        return offset;
+                    }
+                }), error);
+            }
+        }
+    }
+
+    // Tests when called with detached typed array as source.
+    for (let {typedArray} of createTypedArrays()) {
+        for (let {typedArray: source, buffer: sourceBuffer} of createTypedArrays()) {
+            detachArrayBuffer(sourceBuffer);
+
+            assertThrowsInstanceOf(() => typedArray.set(source, {
+                valueOf() {
+                    throw new ExpectedError();
+                }
+            }), ExpectedError);
+        }
+    }
+
+    // Check when detaching source buffer in ToInteger(offset). Test with:
+    // - valid offset,
+    // - too large offset,
+    // - and negative offset.
+    for (let [offset, error] of [[0, TypeError], [1000000, TypeError], [-1, RangeError]]) {
+        for (let {typedArray} of createTypedArrays()) {
+            for (let {typedArray: source, buffer: sourceBuffer} of createTypedArrays()) {
+                assertThrowsInstanceOf(() => typedArray.set(source, {
+                    valueOf() {
+                        detachArrayBuffer(sourceBuffer);
+                        return offset;
+                    }
+                }), error);
+            }
+        }
+    }
+
+    // Test when target and source use the same underlying buffer and
+    // ToInteger(offset) detaches the buffer. Test with:
+    // - same typed array,
+    // - different typed array, but same element type,
+    // - and different element type.
+    for (let src of [ta => ta, ta => new Int32Array(ta.buffer), ta => new Float32Array(ta.buffer)]) {
+        for (let {typedArray, buffer} of createTypedArrays()) {
+            let source = src(typedArray);
+            assertThrowsInstanceOf(() => typedArray.set(source, {
+                valueOf() {
+                    detachArrayBuffer(buffer);
+                    return 0;
+                }
+            }), TypeError);
+        }
+    }
+
+    // Test when Get(src, "length") detaches the buffer, but srcLength is 0.
+    // Also use different offsets to ensure bounds checks use the typed array's
+    // length value from before detaching the buffer.
+    for (let offset of [() => 0, ta => Math.min(1, ta.length), ta => Math.max(0, ta.length - 1)]) {
+        for (let {typedArray, buffer} of createTypedArrays()) {
+            let source = {
+                get length() {
+                    detachArrayBuffer(buffer);
+                    return 0;
+                }
+            };
+            typedArray.set(source, offset(typedArray));
+        }
+    }
+
+    // Test when ToLength(Get(src, "length")) detaches the buffer, but
+    // srcLength is 0. Also use different offsets to ensure bounds checks use
+    // the typed array's length value from before detaching the buffer.
+    for (let offset of [() => 0, ta => Math.min(1, ta.length), ta => Math.max(0, ta.length - 1)]) {
+        for (let {typedArray, buffer} of createTypedArrays()) {
+            let source = {
+                length: {
+                    valueOf() {
+                        detachArrayBuffer(buffer);
+                        return 0;
+                    }
+                }
+            };
+            typedArray.set(source, offset(typedArray));
+        }
+    }
+
+    // Test a TypeError is thrown when the typed array is detached and
+    // srcLength > 0.
+    for (let {typedArray, buffer} of createTypedArrays()) {
+        let source = {
+            length: {
+                valueOf() {
+                    detachArrayBuffer(buffer);
+                    return 1;
+                }
+            }
+        };
+        let err = typedArray.length === 0 ? RangeError : TypeError;
+        assertThrowsInstanceOf(() => typedArray.set(source), err);
+    }
+
+    // Same as above, but with side-effect when executing Get(src, "0").
+    for (let {typedArray, buffer} of createTypedArrays()) {
+        let source = {
+            get 0() {
+                throw new ExpectedError();
+            },
+            length: {
+                valueOf() {
+                    detachArrayBuffer(buffer);
+                    return 1;
+                }
+            }
+        };
+        let err = typedArray.length === 0 ? RangeError : ExpectedError;
+        assertThrowsInstanceOf(() => typedArray.set(source), err);
+    }
+
+    // Same as above, but with side-effect when executing ToNumber(Get(src, "0")).
+    for (let {typedArray, buffer} of createTypedArrays()) {
+        let source = {
+            get 0() {
+                return {
+                    valueOf() {
+                        throw new ExpectedError();
+                    }
+                };
+            },
+            length: {
+                valueOf() {
+                    detachArrayBuffer(buffer);
+                    return 1;
+                }
+            }
+        };
+        let err = typedArray.length === 0 ? RangeError : ExpectedError;
+        assertThrowsInstanceOf(() => typedArray.set(source), err);
+    }
+
+    // Side-effects when getting the source elements detach the buffer.
+    for (let {typedArray, buffer} of createTypedArrays()) {
+        let source = Object.defineProperties([], {
+            0: {
+                get() {
+                    detachArrayBuffer(buffer);
+                    return 1;
+                }
+            }
+        });
+        let err = typedArray.length === 0 ? RangeError : TypeError;
+        assertThrowsInstanceOf(() => typedArray.set(source), err);
+    }
+
+    // Side-effects when getting the source elements detach the buffer. Also
+    // ensure other elements aren't accessed.
+    for (let {typedArray, buffer} of createTypedArrays()) {
+        let source = Object.defineProperties([], {
+            0: {
+                get() {
+                    detachArrayBuffer(buffer);
+                    return 1;
+                }
+            },
+            1: {
+                get() {
+                    throw new Error("Unexpected access");
+                }
+            }
+        });
+        let err = typedArray.length <= 1 ? RangeError : TypeError;
+        assertThrowsInstanceOf(() => typedArray.set(source), err);
+    }
+
+    // Side-effects when converting the source elements detach the buffer.
+    for (let {typedArray, buffer} of createTypedArrays()) {
+        let source = [{
+            valueOf() {
+                detachArrayBuffer(buffer);
+                return 1;
+            }
+        }];
+        let err = typedArray.length === 0 ? RangeError : TypeError;
+        assertThrowsInstanceOf(() => typedArray.set(source), err);
+    }
+
+    // Side-effects when converting the source elements detach the buffer. Also
+    // ensure other elements aren't accessed.
+    for (let {typedArray, buffer} of createTypedArrays()) {
+        let source = [{
+            valueOf() {
+                detachArrayBuffer(buffer);
+                return 1;
+            }
+        }, {
+            valueOf() {
+                throw new Error("Unexpected access");
+            }
+        }];
+        let err = typedArray.length <= 1 ? RangeError : TypeError;
+        assertThrowsInstanceOf(() => typedArray.set(source), err);
+    }
+}
+
+if (typeof reportCompare === "function")
+    reportCompare(true, true);
new file mode 100644
--- /dev/null
+++ b/js/src/tests/ecma_6/TypedArray/set-tointeger.js
@@ -0,0 +1,95 @@
+// Test ToInteger conversion in %TypedArray%.prototype.set(array|typedArray, offset).
+
+let ta = new Int32Array(4);
+
+// %TypedArray%.prototype.set has two different implementations for typed array
+// and non-typed array arguments. Test with both input types.
+let emptySources = [[], new Int32Array(0)];
+let nonEmptySource = [[0], new Int32Array(1)];
+let sources = [...emptySources, ...nonEmptySource];
+
+// Test when ToInteger(offset) is in (-1, 4).
+let validOffsets = [
+    // Values in [+0, 4).
+    0,
+    0.1,
+    3,
+    3.9,
+
+    // Values in (-1, -0].
+    -0,
+    -0.1,
+    -0.9,
+
+    NaN,
+
+    // Also include some non-number values.
+    undefined,
+    null,
+    true,
+    "",
+    "3",
+    "  1\t\n",
+    "some string",
+    {valueOf() { return 2; }},
+];
+
+for (let offset of validOffsets) {
+    for (let source of sources) {
+        ta.set(source, offset);
+    }
+}
+
+// Test when ToInteger(offset) isn't in (-1, 4).
+let invalidOffsets = [
+    // Values exceeding the typed array's length.
+    5,
+    2147483647,
+    2147483648,
+    2147483649,
+    4294967295,
+    4294967296,
+    4294967297,
+    Infinity,
+
+    // Negative values.
+    -1,
+    -1.1,
+    -2147483647,
+    -2147483648,
+    -2147483649,
+    -4294967295,
+    -4294967296,
+    -4294967297,
+    -Infinity,
+
+    // Also include some non-number values.
+    "8",
+    "Infinity",
+    "  Infinity  ",
+    {valueOf() { return 10; }},
+];
+
+for (let offset of invalidOffsets) {
+    for (let source of sources) {
+        assertThrowsInstanceOf(() => ta.set(source, offset), RangeError);
+    }
+}
+
+// Test when ToInteger(offset) is in [4, 5).
+for (let source of emptySources) {
+    ta.set(source, 4);
+    ta.set(source, 4.9);
+}
+for (let source of nonEmptySource) {
+    assertThrowsInstanceOf(() => ta.set(source, 4), RangeError);
+    assertThrowsInstanceOf(() => ta.set(source, 4.9), RangeError);
+}
+
+// ToInteger(symbol value) throws a TypeError.
+for (let source of sources) {
+    assertThrowsInstanceOf(() => ta.set(source, Symbol()), TypeError);
+}
+
+if (typeof reportCompare === "function")
+    reportCompare(true, true);
new file mode 100644
--- /dev/null
+++ b/js/src/tests/ecma_6/TypedArray/set-toobject.js
@@ -0,0 +1,53 @@
+// Test ToObject in %TypedArray%.prototype.set(array|typedArray, offset).
+
+let ta = new Int32Array(4);
+
+for (let nullOrUndefined of [null, undefined]) {
+    // ToObject(array) throws a TypeError when |array| is null or undefined.
+    assertThrowsInstanceOf(() => ta.set(nullOrUndefined), TypeError);
+
+    // ToInteger(offset) is called before ToObject(array).
+    class ExpectedError extends Error {}
+    assertThrowsInstanceOf(() => ta.set(nullOrUndefined, {
+        valueOf() {
+            throw new ExpectedError();
+        }
+    }), ExpectedError);
+}
+
+// Ensure ta is still initialized with zeros.
+assertEqArray(ta, [0, 0, 0, 0]);
+
+// %TypedArray%.prototype.set can be called with a string primitive values.
+ta.set("");
+assertEqArray(ta, [0, 0, 0, 0]);
+
+ta.set("123");
+assertEqArray(ta, [1, 2, 3, 0]);
+
+// Throws a RangeError if the length is too large.
+assertThrowsInstanceOf(() => ta.set("456789"), RangeError);
+assertEqArray(ta, [1, 2, 3, 0]);
+
+// When called with other primitive values the typed array contents don't
+// change since ToObject(<primitive>).length is zero, i.e. the source object is
+// treated the same as an empty array.
+for (let value of [true, false, 0, NaN, 123, Infinity, Symbol()]) {
+    ta.set(value);
+    assertEqArray(ta, [1, 2, 3, 0]);
+}
+
+// Repeat test from above when the primitive wrapper prototype has been changed
+// to include "length" and an indexed property.
+Number.prototype.length = 4;
+Number.prototype[3] = -1;
+try {
+    ta.set(456);
+    assertEqArray(ta, [0, 0, 0, -1]);
+} finally {
+    delete Number.prototype.length;
+    delete Number.prototype[3];
+}
+
+if (typeof reportCompare === "function")
+    reportCompare(true, true);
--- a/js/src/tests/js1_8_5/extensions/typedarray-set-neutering.js
+++ b/js/src/tests/js1_8_5/extensions/typedarray-set-neutering.js
@@ -30,17 +30,17 @@ var src = [ 10, 20, 30, 40,
 Object.defineProperty(src, 4, {
   get: function () {
     detachArrayBuffer(ab);
     gc();
     return 200;
   }
 });
 
-a.set(src);
+assertThrowsInstanceOf(() => a.set(src), TypeError);
 
 // Not really needed
 Array.reverse(a_2);
 
 /******************************************************************************/
 
 if (typeof reportCompare === "function")
   reportCompare(true, true);
--- a/js/src/vm/TypedArrayObject-inl.h
+++ b/js/src/vm/TypedArrayObject-inl.h
@@ -270,16 +270,18 @@ class ElementSpecific
      */
     static bool
     setFromTypedArray(JSContext* cx,
                       Handle<TypedArrayObject*> target, Handle<TypedArrayObject*> source,
                       uint32_t offset)
     {
         MOZ_ASSERT(TypeIDOfType<T>::id == target->type(),
                    "calling wrong setFromTypedArray specialization");
+        MOZ_ASSERT(!target->hasDetachedBuffer(), "target isn't detached");
+        MOZ_ASSERT(!source->hasDetachedBuffer(), "source isn't detached");
 
         MOZ_ASSERT(offset <= target->length());
         MOZ_ASSERT(source->length() <= target->length() - offset);
 
         if (TypedArrayObject::sameBuffer(target, source))
             return setFromOverlappingTypedArray(cx, target, source, offset);
 
         SharedMem<T*> dest = target->viewDataEither().template cast<T*>() + offset;
@@ -363,16 +365,17 @@ class ElementSpecific
      * typed array.
      */
     static bool
     setFromNonTypedArray(JSContext* cx, Handle<TypedArrayObject*> target, HandleObject source,
                          uint32_t len, uint32_t offset = 0)
     {
         MOZ_ASSERT(target->type() == TypeIDOfType<T>::id,
                    "target type and NativeType must match");
+        MOZ_ASSERT(!target->hasDetachedBuffer(), "target isn't detached");
         MOZ_ASSERT(!source->is<TypedArrayObject>(),
                    "use setFromTypedArray instead of this method");
 
         uint32_t i = 0;
         if (source->isNative()) {
             // Attempt fast-path infallible conversion of dense elements up to
             // the first potentially side-effectful lookup or conversion.
             uint32_t bound = Min(source->as<NativeObject>().getDenseInitializedLength(), len);
@@ -401,33 +404,35 @@ class ElementSpecific
             T n;
             if (!valueToNative(cx, v, &n))
                 return false;
 
             len = Min(len, target->length());
             if (i >= len)
                 break;
 
-            // Compute every iteration in case getElement/valueToNative is wacky.
+            // Compute every iteration in case getElement/valueToNative
+            // detaches the underlying array buffer or GC moves the data.
             SharedMem<T*> dest = target->viewDataEither().template cast<T*>() + offset + i;
             Ops::store(dest, n);
         }
 
         return true;
     }
 
     /*
      * Copy |source| into the typed array |target|.
      */
     static bool
     initFromIterablePackedArray(JSContext* cx, Handle<TypedArrayObject*> target,
                                 HandleArrayObject source)
     {
         MOZ_ASSERT(target->type() == TypeIDOfType<T>::id,
                    "target type and NativeType must match");
+        MOZ_ASSERT(!target->hasDetachedBuffer(), "target isn't detached");
         MOZ_ASSERT(IsPackedArray(source), "source array must be packed");
         MOZ_ASSERT(source->getDenseInitializedLength() <= target->length());
 
         uint32_t len = source->getDenseInitializedLength();
         uint32_t i = 0;
 
         // Attempt fast-path infallible conversion of dense elements up to the
         // first potentially side-effectful conversion.
@@ -474,16 +479,18 @@ class ElementSpecific
     static bool
     setFromOverlappingTypedArray(JSContext* cx,
                                  Handle<TypedArrayObject*> target,
                                  Handle<TypedArrayObject*> source,
                                  uint32_t offset)
     {
         MOZ_ASSERT(TypeIDOfType<T>::id == target->type(),
                    "calling wrong setFromTypedArray specialization");
+        MOZ_ASSERT(!target->hasDetachedBuffer(), "target isn't detached");
+        MOZ_ASSERT(!source->hasDetachedBuffer(), "source isn't detached");
         MOZ_ASSERT(TypedArrayObject::sameBuffer(target, source),
                    "the provided arrays don't actually overlap, so it's "
                    "undesirable to use this method");
 
         MOZ_ASSERT(offset <= target->length());
         MOZ_ASSERT(source->length() <= target->length() - offset);
 
         SharedMem<T*> dest = target->viewDataEither().template cast<T*>() + offset;
--- a/js/src/vm/TypedArrayObject.cpp
+++ b/js/src/vm/TypedArrayObject.cpp
@@ -1515,84 +1515,164 @@ SetFromNonTypedArray(JSContext* cx, Hand
 {
     MOZ_ASSERT(!source->is<TypedArrayObject>(), "use SetFromTypedArray");
 
     if (target->isSharedMemory())
         return ElementSpecific<T, SharedOps>::setFromNonTypedArray(cx, target, source, len, offset);
     return ElementSpecific<T, UnsharedOps>::setFromNonTypedArray(cx, target, source, len, offset);
 }
 
-/* set(array[, offset]) */
+// ES2017 draft rev c57ef95c45a371f9c9485bb1c3881dbdc04524a2
+// 22.2.3.23 %TypedArray%.prototype.set ( overloaded [ , offset ] )
+// 22.2.3.23.1 %TypedArray%.prototype.set ( array [ , offset ] )
+// 22.2.3.23.2 %TypedArray%.prototype.set( typedArray [ , offset ] )
 /* static */ bool
 TypedArrayObject::set_impl(JSContext* cx, const CallArgs& args)
 {
     MOZ_ASSERT(TypedArrayObject::is(args.thisv()));
 
+    // Steps 1-5 (Validation performed as part of CallNonGenericMethod).
     Rooted<TypedArrayObject*> target(cx, &args.thisv().toObject().as<TypedArrayObject>());
 
-    // The first argument must be either a typed array or arraylike.
-    if (args.length() == 0 || !args[0].isObject()) {
-        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_BAD_ARGS);
-        return false;
-    }
-
-    int32_t offset = 0;
+    // Steps 6-7.
+    double targetOffset = 0;
     if (args.length() > 1) {
-        if (!ToInt32(cx, args[1], &offset))
+        // Step 6.
+        if (!ToInteger(cx, args[1], &targetOffset))
             return false;
 
-        if (offset < 0 || uint32_t(offset) > target->length()) {
-            // the given offset is bogus
+        // Step 7.
+        if (targetOffset < 0) {
             JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_BAD_INDEX);
             return false;
         }
     }
 
-    RootedObject arg0(cx, &args[0].toObject());
-    if (arg0->is<TypedArrayObject>()) {
-        Handle<TypedArrayObject*> source = arg0.as<TypedArrayObject>();
-        if (source->length() > target->length() - offset) {
+    // Steps 8-9.
+    if (target->hasDetachedBuffer()) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_DETACHED);
+        return false;
+    }
+
+    if (args.get(0).isObject() && args[0].toObject().is<TypedArrayObject>()) {
+        // Remaining steps of 22.2.3.23.2.
+        Rooted<TypedArrayObject*> source(cx, &args[0].toObject().as<TypedArrayObject>());
+
+        // Steps 11-12.
+        if (source->hasDetachedBuffer()) {
+            JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPED_ARRAY_DETACHED);
+            return false;
+        }
+
+        // Step 10 (Reordered).
+        uint32_t targetLength = target->length();
+
+        // Step 22 (Split into two checks to provide better error messages).
+        if (targetOffset > targetLength) {
+            JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_BAD_INDEX);
+            return false;
+        }
+
+        // Step 22 (Cont'd).
+        uint32_t offset = uint32_t(targetOffset);
+        if (source->length() > targetLength - offset) {
             JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_BAD_ARRAY_LENGTH);
             return false;
         }
 
+        // Steps 13-21, 23-28.
         switch (target->type()) {
 #define SET_FROM_TYPED_ARRAY(T, N) \
           case Scalar::N: \
             if (!SetFromTypedArray<T>(cx, target, source, offset)) \
                 return false; \
             break;
 JS_FOR_EACH_TYPED_ARRAY(SET_FROM_TYPED_ARRAY)
 #undef SET_FROM_TYPED_ARRAY
           default:
             MOZ_CRASH("Unsupported TypedArray type");
         }
     } else {
-        uint32_t len;
-        if (!GetLengthProperty(cx, arg0, &len))
+        // Remaining steps of 22.2.3.23.1.
+
+        // Step 10.
+        // We can't reorder this step because side-effects in step 16 can
+        // detach the underlying array buffer from the typed array.
+        uint32_t targetLength = target->length();
+
+        // Step 15.
+        RootedObject src(cx, ToObject(cx, args.get(0)));
+        if (!src)
             return false;
 
-        if (uint32_t(offset) > target->length() || len > target->length() - offset) {
+        // Step 16.
+        uint32_t srcLength;
+        if (!GetLengthProperty(cx, src, &srcLength))
+            return false;
+
+        // Step 17 (Split into two checks to provide better error messages).
+        if (targetOffset > targetLength) {
+            JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_BAD_INDEX);
+            return false;
+        }
+
+        // Step 17 (Cont'd).
+        uint32_t offset = uint32_t(targetOffset);
+        if (srcLength > targetLength - offset) {
             JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_BAD_ARRAY_LENGTH);
             return false;
         }
 
-        switch (target->type()) {
+        // Steps 11-14, 18-21.
+        if (srcLength > 0) {
+            // GetLengthProperty in step 16 can lead to the execution of user
+            // code which may detach the buffer. Handle this case here to
+            // ensure SetFromNonTypedArray is never called with a detached
+            // buffer. We still need to execute steps 21.a-b for their
+            // possible side-effects.
+            if (target->hasDetachedBuffer()) {
+                // Steps 21.a-b.
+                RootedValue v(cx);
+                if (!GetElement(cx, src, src, 0, &v))
+                   return false;
+
+                double unused;
+                if (!ToNumber(cx, v, &unused))
+                    return false;
+
+                // Step 21.c.
+                JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                          JSMSG_TYPED_ARRAY_DETACHED);
+                return false;
+            }
+
+            switch (target->type()) {
 #define SET_FROM_NON_TYPED_ARRAY(T, N) \
-          case Scalar::N: \
-            if (!SetFromNonTypedArray<T>(cx, target, arg0, len, offset)) \
-                return false; \
-            break;
+              case Scalar::N: \
+                if (!SetFromNonTypedArray<T>(cx, target, src, srcLength, offset)) \
+                    return false; \
+                break;
 JS_FOR_EACH_TYPED_ARRAY(SET_FROM_NON_TYPED_ARRAY)
 #undef SET_FROM_NON_TYPED_ARRAY
-          default:
-            MOZ_CRASH("Unsupported TypedArray type");
+              default:
+                MOZ_CRASH("Unsupported TypedArray type");
+            }
+
+            // Step 21.c.
+            // SetFromNonTypedArray doesn't throw when the array buffer gets
+            // detached.
+            if (target->hasDetachedBuffer()) {
+                JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                          JSMSG_TYPED_ARRAY_DETACHED);
+                return false;
+            }
         }
     }
 
+    // Step 29/22.
     args.rval().setUndefined();
     return true;
 }
 
 /* static */ bool
 TypedArrayObject::set(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);