Bug 1478850 - Safely handle nuked wrappers as arguments to stream-related JSAPI entry points. r=tcampbell
authorJason Orendorff <jorendorff@mozilla.com>
Tue, 20 Nov 2018 18:24:45 +0000
changeset 447342 b4bc0fa77a1e3f0828e7b0c53a6910820b0cdedc
parent 447341 a0fb2044780dd738d8fbd9b5c6ec074cf7b05a3e
child 447343 3eb49d9e73bb4db202f6c8bb8bda4e9d89d34990
push id35075
push usershindli@mozilla.com
push dateWed, 21 Nov 2018 04:04:02 +0000
treeherdermozilla-central@8540104bb0bd [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerstcampbell
bugs1478850
milestone65.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1478850 - Safely handle nuked wrappers as arguments to stream-related JSAPI entry points. r=tcampbell Differential Revision: https://phabricator.services.mozilla.com/D12196
js/src/builtin/Stream.cpp
js/src/vm/Compartment-inl.h
--- a/js/src/builtin/Stream.cpp
+++ b/js/src/builtin/Stream.cpp
@@ -53,50 +53,16 @@ uint8_t
 ReadableStream::embeddingFlags() const
 {
     uint8_t flags = controller()->flags() >> ReadableStreamController::EmbeddingFlagsOffset;
     MOZ_ASSERT_IF(flags, mode() == JS::ReadableStreamMode::ExternalSource);
     return flags;
 }
 
 /**
- * Checks that |obj| is an unwrapped instance of T or throws an error.
- *
- * This overload must only be used if the caller can ensure that failure to
- * unwrap is the only possible source of exceptions.
- */
-template<class T>
-static T*
-ToUnwrapped(JSContext* cx, JSObject* obj)
-{
-    if (IsWrapper(obj)) {
-        obj = CheckedUnwrap(obj);
-        if (!obj) {
-            ReportAccessDenied(cx);
-            return nullptr;
-        }
-    }
-
-    return &obj->as<T>();
-}
-
-/**
- * Unwrap v as an object of type T, throwing if it can't be unwrapped.
- *
- * This overload must be used only if v is an ObjectValue and the result of a
- * successful unwrap is certain to be of type T.
- */
-template <class T>
-static T*
-ToUnwrapped(JSContext* cx, HandleValue v)
-{
-    return ToUnwrapped<T>(cx, &v.toObject());
-}
-
-/**
  * Returns the stream associated with the given reader.
  */
 static MOZ_MUST_USE ReadableStream*
 UnwrapStreamFromReader(JSContext *cx, Handle<ReadableStreamReader*> reader)
 {
     MOZ_ASSERT(reader->hasStream());
     return UnwrapInternalSlot<ReadableStream>(cx, reader, ReadableStreamReader::Slot_Stream);
 }
@@ -2608,17 +2574,18 @@ ReadableStreamControllerCancelSteps(JSCo
     if (!unwrappedController->is<ReadableStreamDefaultController>()) {
         RootedNativeObject unwrappedPendingPullIntos(cx,
             unwrappedController->as<ReadableByteStreamController>().pendingPullIntos());
 
         if (unwrappedPendingPullIntos->getDenseInitializedLength() != 0) {
             // Step a: Let firstDescriptor be the first element of
             //         this.[[pendingPullIntos]].
             PullIntoDescriptor* unwrappedDescriptor =
-                ToUnwrapped<PullIntoDescriptor>(cx, PeekList<JSObject>(unwrappedPendingPullIntos));
+                UnwrapAndDowncastObject<PullIntoDescriptor>(
+                    cx, PeekList<JSObject>(unwrappedPendingPullIntos));
             if (!unwrappedDescriptor) {
                 return nullptr;
             }
 
             // Step b: Set firstDescriptor.[[bytesFilled]] to 0.
             unwrappedDescriptor->setBytesFilled(0);
         }
     }
@@ -3479,17 +3446,18 @@ ReadableByteStreamControllerPullSteps(JS
             queueTotalSize = queueTotalSize - bytesWritten;
         } else {
             // Step 3.b: Let entry be the first element of this.[[queue]].
             // Step 3.c: Remove entry from this.[[queue]], shifting all other
             //           elements downward (so that the second becomes the
             //           first, and so on).
             RootedNativeObject unwrappedQueue(cx, unwrappedController->queue());
             Rooted<ByteStreamChunk*> unwrappedEntry(cx,
-                ToUnwrapped<ByteStreamChunk>(cx, ShiftFromList<JSObject>(cx, unwrappedQueue)));
+                UnwrapAndDowncastObject<ByteStreamChunk>(
+                    cx, ShiftFromList<JSObject>(cx, unwrappedQueue)));
             if (!unwrappedEntry) {
                 return nullptr;
             }
 
             queueTotalSize = queueTotalSize - unwrappedEntry->byteLength();
 
             // Step 3.f: Let view be ! Construct(%Uint8Array%,
             //                                   « entry.[[buffer]],
@@ -3679,17 +3647,18 @@ ReadableByteStreamControllerClose(JSCont
     }
 
     // Step 5: If controller.[[pendingPullIntos]] is not empty,
     RootedNativeObject unwrappedPendingPullIntos(cx, unwrappedController->pendingPullIntos());
     if (unwrappedPendingPullIntos->getDenseInitializedLength() != 0) {
         // Step a: Let firstPendingPullInto be the first element of
         //         controller.[[pendingPullIntos]].
         Rooted<PullIntoDescriptor*> unwrappedFirstPendingPullInto(cx,
-            ToUnwrapped<PullIntoDescriptor>(cx, PeekList<JSObject>(unwrappedPendingPullIntos)));
+            UnwrapAndDowncastObject<PullIntoDescriptor>(
+                cx, PeekList<JSObject>(unwrappedPendingPullIntos)));
         if (!unwrappedFirstPendingPullInto) {
             return false;
         }
 
         // Step b: If firstPendingPullInto.[[bytesFilled]] > 0,
         if (unwrappedFirstPendingPullInto->bytesFilled() > 0) {
             // Step i: Let e be a new TypeError exception.
             JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
@@ -3767,17 +3736,17 @@ ReadableByteStreamControllerInvalidateBY
 {
     // Step 1: If controller.[[byobRequest]] is undefined, return.
     RootedValue unwrappedBYOBRequestVal(cx, unwrappedController->byobRequest());
     if (unwrappedBYOBRequestVal.isUndefined()) {
         return true;
     }
 
     RootedNativeObject unwrappedBYOBRequest(cx,
-        ToUnwrapped<NativeObject>(cx, unwrappedBYOBRequestVal));
+        UnwrapAndDowncastValue<NativeObject>(cx, unwrappedBYOBRequestVal));
     if (!unwrappedBYOBRequest) {
         return false;
     }
 
     // Step 2: Set controller.[[byobRequest]]
     //                       .[[associatedReadableByteStreamController]]
     //         to undefined.
     unwrappedBYOBRequest->setFixedSlot(BYOBRequestSlot_Controller, UndefinedValue());
@@ -4258,120 +4227,123 @@ JS::IsReadableStreamReader(JSObject* obj
 JS_PUBLIC_API bool
 JS::IsReadableStreamDefaultReader(JSObject* obj)
 {
     return obj->canUnwrapAs<ReadableStreamDefaultReader>();
 }
 
 template<class T>
 static MOZ_MUST_USE T*
-APIToUnwrapped(JSContext* cx, JSObject* obj)
+APIUnwrapAndDowncast(JSContext* cx, JSObject* obj)
 {
     cx->check(obj);
-    return ToUnwrapped<T>(cx, obj);
+    return UnwrapAndDowncastObject<T>(cx, obj);
 }
 
 JS_PUBLIC_API bool
 JS::ReadableStreamIsReadable(JSContext* cx, HandleObject streamObj, bool* result)
 {
-    ReadableStream* unwrappedStream = APIToUnwrapped<ReadableStream>(cx, streamObj);
+    ReadableStream* unwrappedStream = APIUnwrapAndDowncast<ReadableStream>(cx, streamObj);
     if (!unwrappedStream) {
         return false;
     }
 
     *result = unwrappedStream->readable();
     return true;
 }
 
 JS_PUBLIC_API bool
 JS::ReadableStreamIsLocked(JSContext* cx, HandleObject streamObj, bool* result)
 {
-    ReadableStream* unwrappedStream = APIToUnwrapped<ReadableStream>(cx, streamObj);
+    ReadableStream* unwrappedStream = APIUnwrapAndDowncast<ReadableStream>(cx, streamObj);
     if (!unwrappedStream) {
         return false;
     }
 
     *result = unwrappedStream->locked();
     return true;
 }
 
 JS_PUBLIC_API bool
 JS::ReadableStreamIsDisturbed(JSContext* cx, HandleObject streamObj, bool* result)
 {
-    ReadableStream* unwrappedStream = APIToUnwrapped<ReadableStream>(cx, streamObj);
+    ReadableStream* unwrappedStream = APIUnwrapAndDowncast<ReadableStream>(cx, streamObj);
     if (!unwrappedStream) {
         return false;
     }
 
     *result = unwrappedStream->disturbed();
     return true;
 }
 
 JS_PUBLIC_API bool
 JS::ReadableStreamGetEmbeddingFlags(JSContext* cx, HandleObject streamObj, uint8_t* flags)
 {
-    ReadableStream* unwrappedStream = APIToUnwrapped<ReadableStream>(cx, streamObj);
+    ReadableStream* unwrappedStream = APIUnwrapAndDowncast<ReadableStream>(cx, streamObj);
     if (!unwrappedStream) {
         return false;
     }
 
     *flags = unwrappedStream->embeddingFlags();
     return true;
 }
 
 JS_PUBLIC_API JSObject*
 JS::ReadableStreamCancel(JSContext* cx, HandleObject streamObj, HandleValue reason)
 {
     AssertHeapIsIdle();
     CHECK_THREAD(cx);
     cx->check(reason);
 
-    Rooted<ReadableStream*> unwrappedStream(cx, APIToUnwrapped<ReadableStream>(cx, streamObj));
+    Rooted<ReadableStream*> unwrappedStream(cx,
+        APIUnwrapAndDowncast<ReadableStream>(cx, streamObj));
     if (!unwrappedStream) {
         return nullptr;
     }
 
     return ::ReadableStreamCancel(cx, unwrappedStream, reason);
 }
 
 JS_PUBLIC_API bool
 JS::ReadableStreamGetMode(JSContext* cx, HandleObject streamObj, JS::ReadableStreamMode* mode)
 {
-    ReadableStream* unwrappedStream = APIToUnwrapped<ReadableStream>(cx, streamObj);
+    ReadableStream* unwrappedStream = APIUnwrapAndDowncast<ReadableStream>(cx, streamObj);
     if (!unwrappedStream) {
         return false;
     }
 
     *mode = unwrappedStream->mode();
     return true;
 }
 
 JS_PUBLIC_API JSObject*
 JS::ReadableStreamGetReader(JSContext* cx, HandleObject streamObj, ReadableStreamReaderMode mode)
 {
     AssertHeapIsIdle();
     CHECK_THREAD(cx);
 
-    Rooted<ReadableStream*> unwrappedStream(cx, APIToUnwrapped<ReadableStream>(cx, streamObj));
+    Rooted<ReadableStream*> unwrappedStream(cx,
+        APIUnwrapAndDowncast<ReadableStream>(cx, streamObj));
     if (!unwrappedStream) {
         return nullptr;
     }
 
     JSObject* result = CreateReadableStreamDefaultReader(cx, unwrappedStream);
     MOZ_ASSERT_IF(result, IsObjectInContextCompartment(result, cx));
     return result;
 }
 
 JS_PUBLIC_API bool
 JS::ReadableStreamGetExternalUnderlyingSource(JSContext* cx, HandleObject streamObj, void** source)
 {
     AssertHeapIsIdle();
     CHECK_THREAD(cx);
 
-    Rooted<ReadableStream*> unwrappedStream(cx, APIToUnwrapped<ReadableStream>(cx, streamObj));
+    Rooted<ReadableStream*> unwrappedStream(cx,
+        APIUnwrapAndDowncast<ReadableStream>(cx, streamObj));
     if (!unwrappedStream) {
         return false;
     }
 
     MOZ_ASSERT(unwrappedStream->mode() == JS::ReadableStreamMode::ExternalSource);
     if (unwrappedStream->locked()) {
         JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_READABLESTREAM_LOCKED);
         return false;
@@ -4387,17 +4359,17 @@ JS::ReadableStreamGetExternalUnderlyingS
     unwrappedController->setSourceLocked();
     *source = unwrappedController->underlyingSource().toPrivate();
     return true;
 }
 
 JS_PUBLIC_API bool
 JS::ReadableStreamReleaseExternalUnderlyingSource(JSContext* cx, HandleObject streamObj)
 {
-    ReadableStream* unwrappedStream = APIToUnwrapped<ReadableStream>(cx, streamObj);
+    ReadableStream* unwrappedStream = APIUnwrapAndDowncast<ReadableStream>(cx, streamObj);
     if (!unwrappedStream) {
         return false;
     }
 
     MOZ_ASSERT(unwrappedStream->mode() == JS::ReadableStreamMode::ExternalSource);
     MOZ_ASSERT(unwrappedStream->locked());
     MOZ_ASSERT(unwrappedStream->controller()->sourceLocked());
     unwrappedStream->controller()->clearSourceLocked();
@@ -4406,17 +4378,18 @@ JS::ReadableStreamReleaseExternalUnderly
 
 JS_PUBLIC_API bool
 JS::ReadableStreamUpdateDataAvailableFromSource(JSContext* cx, JS::HandleObject streamObj,
                                                 uint32_t availableData)
 {
     AssertHeapIsIdle();
     CHECK_THREAD(cx);
 
-    Rooted<ReadableStream*> unwrappedStream(cx, APIToUnwrapped<ReadableStream>(cx, streamObj));
+    Rooted<ReadableStream*> unwrappedStream(cx,
+        APIUnwrapAndDowncast<ReadableStream>(cx, streamObj));
     if (!unwrappedStream) {
         return false;
     }
 
     // This is based on Streams spec 3.10.4.4. enqueue(chunk) steps 1-3 and
     // 3.12.9. ReadableByteStreamControllerEnqueue(controller, chunk) steps
     // 8-9.
     //
@@ -4521,17 +4494,18 @@ JS::ReadableStreamUpdateDataAvailableFro
 
 JS_PUBLIC_API bool
 JS::ReadableStreamTee(JSContext* cx, HandleObject streamObj,
                       MutableHandleObject branch1Obj, MutableHandleObject branch2Obj)
 {
     AssertHeapIsIdle();
     CHECK_THREAD(cx);
 
-    Rooted<ReadableStream*> unwrappedStream(cx, APIToUnwrapped<ReadableStream>(cx, streamObj));
+    Rooted<ReadableStream*> unwrappedStream(cx,
+        APIUnwrapAndDowncast<ReadableStream>(cx, streamObj));
     if (!unwrappedStream) {
         return false;
     }
 
     Rooted<ReadableStream*> branch1Stream(cx);
     Rooted<ReadableStream*> branch2Stream(cx);
     if (!ReadableStreamTee(cx, unwrappedStream, false, &branch1Stream, &branch2Stream)) {
         return false;
@@ -4541,17 +4515,17 @@ JS::ReadableStreamTee(JSContext* cx, Han
     branch2Obj.set(branch2Stream);
 
     return true;
 }
 
 JS_PUBLIC_API bool
 JS::ReadableStreamGetDesiredSize(JSContext* cx, JSObject* streamObj, bool* hasValue, double* value)
 {
-    ReadableStream* unwrappedStream = APIToUnwrapped<ReadableStream>(cx, streamObj);
+    ReadableStream* unwrappedStream = APIUnwrapAndDowncast<ReadableStream>(cx, streamObj);
     if (!unwrappedStream) {
         return false;
     }
 
     if (unwrappedStream->errored()) {
         *hasValue = false;
         return true;
     }
@@ -4568,17 +4542,18 @@ JS::ReadableStreamGetDesiredSize(JSConte
 }
 
 JS_PUBLIC_API bool
 JS::ReadableStreamClose(JSContext* cx, HandleObject streamObj)
 {
     AssertHeapIsIdle();
     CHECK_THREAD(cx);
 
-    Rooted<ReadableStream*> unwrappedStream(cx, APIToUnwrapped<ReadableStream>(cx, streamObj));
+    Rooted<ReadableStream*> unwrappedStream(cx,
+        APIUnwrapAndDowncast<ReadableStream>(cx, streamObj));
     if (!unwrappedStream) {
         return false;
     }
 
     Rooted<ReadableStreamController*> unwrappedControllerObj(cx, unwrappedStream->controller());
     if (!VerifyControllerStateForClosing(cx, unwrappedControllerObj)) {
         return false;
     }
@@ -4596,17 +4571,18 @@ JS::ReadableStreamClose(JSContext* cx, H
 
 JS_PUBLIC_API bool
 JS::ReadableStreamEnqueue(JSContext* cx, HandleObject streamObj, HandleValue chunk)
 {
     AssertHeapIsIdle();
     CHECK_THREAD(cx);
     cx->check(chunk);
 
-    Rooted<ReadableStream*> unwrappedStream(cx, APIToUnwrapped<ReadableStream>(cx, streamObj));
+    Rooted<ReadableStream*> unwrappedStream(cx,
+        APIUnwrapAndDowncast<ReadableStream>(cx, streamObj));
     if (!unwrappedStream) {
         return false;
     }
 
     if (unwrappedStream->mode() != JS::ReadableStreamMode::Default) {
         JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                                   JSMSG_READABLESTREAM_NOT_DEFAULT_CONTROLLER,
                                   "JS::ReadableStreamEnqueue");
@@ -4624,17 +4600,18 @@ JS::ReadableStreamEnqueue(JSContext* cx,
 
 JS_PUBLIC_API bool
 JS::ReadableStreamError(JSContext* cx, HandleObject streamObj, HandleValue error)
 {
     AssertHeapIsIdle();
     CHECK_THREAD(cx);
     cx->check(error);
 
-    Rooted<ReadableStream*> unwrappedStream(cx, APIToUnwrapped<ReadableStream>(cx, streamObj));
+    Rooted<ReadableStream*> unwrappedStream(cx,
+        APIUnwrapAndDowncast<ReadableStream>(cx, streamObj));
     if (!unwrappedStream) {
         return false;
     }
 
     // Step 3: If stream.[[state]] is not "readable", throw a TypeError exception.
     if (!unwrappedStream->readable()) {
         JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                                   JSMSG_READABLESTREAMCONTROLLER_NOT_READABLE, "error");
@@ -4645,51 +4622,51 @@ JS::ReadableStreamError(JSContext* cx, H
     Rooted<ReadableStreamController*> unwrappedController(cx, unwrappedStream->controller());
     return ReadableStreamControllerError(cx, unwrappedController, error);
 }
 
 JS_PUBLIC_API bool
 JS::ReadableStreamReaderIsClosed(JSContext* cx, HandleObject readerObj, bool* result)
 {
     Rooted<ReadableStreamReader*> unwrappedReader(cx,
-        APIToUnwrapped<ReadableStreamReader>(cx, readerObj));
+        APIUnwrapAndDowncast<ReadableStreamReader>(cx, readerObj));
     if (!unwrappedReader) {
         return false;
     }
 
     *result = unwrappedReader->isClosed();
     return true;
 }
 
 JS_PUBLIC_API bool
 JS::ReadableStreamReaderCancel(JSContext* cx, HandleObject readerObj, HandleValue reason)
 {
     AssertHeapIsIdle();
     CHECK_THREAD(cx);
     cx->check(reason);
 
     Rooted<ReadableStreamReader*> unwrappedReader(cx,
-        APIToUnwrapped<ReadableStreamReader>(cx, readerObj));
+        APIUnwrapAndDowncast<ReadableStreamReader>(cx, readerObj));
     if (!unwrappedReader) {
         return false;
     }
     MOZ_ASSERT(unwrappedReader->forAuthorCode() == ForAuthorCodeBool::No,
                "C++ code should not touch readers created by scripts");
 
     return ReadableStreamReaderGenericCancel(cx, unwrappedReader, reason);
 }
 
 JS_PUBLIC_API bool
 JS::ReadableStreamReaderReleaseLock(JSContext* cx, HandleObject readerObj)
 {
     AssertHeapIsIdle();
     CHECK_THREAD(cx);
 
     Rooted<ReadableStreamReader*> unwrappedReader(cx,
-        APIToUnwrapped<ReadableStreamReader>(cx, readerObj));
+        APIUnwrapAndDowncast<ReadableStreamReader>(cx, readerObj));
     if (!unwrappedReader) {
         return false;
     }
     MOZ_ASSERT(unwrappedReader->forAuthorCode() == ForAuthorCodeBool::No,
                "C++ code should not touch readers created by scripts");
 
 #ifdef DEBUG
     Rooted<ReadableStream*> unwrappedStream(cx, UnwrapStreamFromReader(cx, unwrappedReader));
@@ -4703,18 +4680,18 @@ JS::ReadableStreamReaderReleaseLock(JSCo
 }
 
 JS_PUBLIC_API JSObject*
 JS::ReadableStreamDefaultReaderRead(JSContext* cx, HandleObject readerObj)
 {
     AssertHeapIsIdle();
     CHECK_THREAD(cx);
 
-    Rooted<ReadableStreamDefaultReader*> unwrappedReader(cx);
-    unwrappedReader = APIToUnwrapped<ReadableStreamDefaultReader>(cx, readerObj);
+    Rooted<ReadableStreamDefaultReader*> unwrappedReader(cx,
+        APIUnwrapAndDowncast<ReadableStreamDefaultReader>(cx, readerObj));
     if (!unwrappedReader) {
         return nullptr;
     }
     MOZ_ASSERT(unwrappedReader->forAuthorCode() == ForAuthorCodeBool::No,
                "C++ code should not touch readers created by scripts");
 
     return ::ReadableStreamDefaultReaderRead(cx, unwrappedReader);
 }
--- a/js/src/vm/Compartment-inl.h
+++ b/js/src/vm/Compartment-inl.h
@@ -236,50 +236,59 @@ UnwrapAndTypeCheckArgument(JSContext* cx
     Value val = args.get(argIndex);
     if (val.isObject() && val.toObject().is<T>()) {
         return &val.toObject().as<T>();
     }
     return detail::UnwrapAndTypeCheckArgumentSlowPath<T>(cx, args, methodName, argIndex);
 }
 
 /**
- * Unwrap a value of a known type.
+ * Unwrap an object of a known type.
  *
- * If `value` is an object of class T, this returns a pointer to that object.
- * If `value` is a wrapper for such an object, this tries to unwrap the object
- * and return a pointer to it. If access is denied, or `value` was a wrapper
- * but has been nuked, this reports an error and returns null.
+ * If `obj` is an object of class T, this returns a pointer to that object. If
+ * `obj` is a wrapper for such an object, this tries to unwrap the object and
+ * return a pointer to it. If access is denied, or `obj` was a wrapper but has
+ * been nuked, this reports an error and returns null.
  *
- * In all other cases, the behavior is undefined, so call this only if `value`
- * is known to have been initialized with an object of class T.
+ * In all other cases, the behavior is undefined, so call this only if `obj` is
+ * known to have been an object of class T, or a wrapper to a T, at some point.
  */
 template <class T>
 MOZ_MUST_USE T*
-UnwrapAndDowncastValue(JSContext* cx, const Value& value)
+UnwrapAndDowncastObject(JSContext* cx, JSObject* obj)
 {
     static_assert(!std::is_convertible<T*, Wrapper*>::value,
                   "T can't be a Wrapper type; this function discards wrappers");
 
-    JSObject* result = &value.toObject();
-    if (IsProxy(result)) {
-        if (JS_IsDeadWrapper(result)) {
+    if (IsProxy(obj)) {
+        if (JS_IsDeadWrapper(obj)) {
             JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEAD_OBJECT);
             return nullptr;
         }
 
         // It would probably be OK to do an unchecked unwrap here, but we allow
         // arbitrary security policies, so check anyway.
-        result = CheckedUnwrap(result);
-        if (!result) {
+        obj = CheckedUnwrap(obj);
+        if (!obj) {
             ReportAccessDenied(cx);
             return nullptr;
         }
     }
 
-    return &result->as<T>();
+    return &obj->as<T>();
+}
+
+/**
+ * Unwrap a value of a known type. See UnwrapAndDowncastObject.
+ */
+template <class T>
+inline MOZ_MUST_USE T*
+UnwrapAndDowncastValue(JSContext* cx, const Value& value)
+{
+    return UnwrapAndDowncastObject<T>(cx, &value.toObject());
 }
 
 /**
  * Read a private slot that is known to point to a particular type of object.
  *
  * Some internal slots specified in various standards effectively have static
  * types. For example, the [[ownerReadableStream]] slot of a stream reader is
  * guaranteed to be a ReadableStream. However, because of compartments, we