Bug 1502355 - Add support for unwrapping a value/object, known at one time to have a given class, as an object of that class. r=arai
authorJeff Walden <jwalden@mit.edu>
Tue, 18 Aug 2020 20:50:18 +0000
changeset 609849 deb00e023f3b91410ce7bcc1897c9eb6e30ff465
parent 609848 eeff6597f16f39192c4e05c5155b09163b719aa6
child 609850 254c794085e14436bb27255a528e8f940b20193d
push id13553
push userffxbld-merge
push dateMon, 24 Aug 2020 12:51:36 +0000
treeherdermozilla-beta@a54f8b5d0977 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersarai
bugs1502355
milestone81.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 1502355 - Add support for unwrapping a value/object, known at one time to have a given class, as an object of that class. r=arai Differential Revision: https://phabricator.services.mozilla.com/D87378
js/src/vm/Compartment-inl.h
js/src/vm/JSObject.h
--- a/js/src/vm/Compartment-inl.h
+++ b/js/src/vm/Compartment-inl.h
@@ -18,16 +18,18 @@
 #include "gc/Marking.h"
 #include "js/CallArgs.h"
 #include "js/Wrapper.h"
 #include "vm/Iteration.h"
 #include "vm/JSObject.h"
 
 #include "vm/JSContext-inl.h"
 
+struct JSClass;
+
 inline js::StringWrapperMap::Ptr JS::Compartment::lookupWrapper(
     JSString* str) const {
   return zone()->crossZoneStringWrappers().lookup(str);
 }
 
 inline bool JS::Compartment::wrap(JSContext* cx, JS::MutableHandleValue vp) {
   /* Only GC things have to be wrapped or copied. */
   if (!vp.isGCThing()) {
@@ -147,40 +149,91 @@ MOZ_MUST_USE T* UnwrapAndTypeCheckValueS
   if (!obj || !obj->is<T>()) {
     throwTypeError();
     return nullptr;
   }
 
   return &obj->as<T>();
 }
 
+template <class ErrorCallback>
+MOZ_MUST_USE JSObject* UnwrapAndTypeCheckValueSlowPath(
+    JSContext* cx, HandleValue value, const JSClass* clasp,
+    ErrorCallback throwTypeError) {
+  JSObject* obj = nullptr;
+  if (value.isObject()) {
+    obj = &value.toObject();
+    if (IsWrapper(obj)) {
+      obj = CheckedUnwrapStatic(obj);
+      if (!obj) {
+        ReportAccessDenied(cx);
+        return nullptr;
+      }
+    }
+  }
+
+  if (!obj || !obj->hasClass(clasp)) {
+    throwTypeError();
+    return nullptr;
+  }
+
+  return obj;
+}
+
 }  // namespace detail
 
 /**
  * Remove all wrappers from `val` and try to downcast the result to class `T`.
  *
  * DANGER: The result may not be same-compartment with `cx`.
  *
  * This calls `throwTypeError` if the value isn't an object, cannot be
  * unwrapped, or isn't an instance of the expected type. `throwTypeError` must
  * in fact throw a TypeError (or OOM trying).
  */
 template <class T, class ErrorCallback>
 inline MOZ_MUST_USE T* UnwrapAndTypeCheckValue(JSContext* cx, HandleValue value,
                                                ErrorCallback throwTypeError) {
+  cx->check(value);
+
   static_assert(!std::is_convertible_v<T*, Wrapper*>,
                 "T can't be a Wrapper type; this function discards wrappers");
-  cx->check(value);
+
   if (value.isObject() && value.toObject().is<T>()) {
     return &value.toObject().as<T>();
   }
+
   return detail::UnwrapAndTypeCheckValueSlowPath<T>(cx, value, throwTypeError);
 }
 
 /**
+ * Remove all wrappers from |val| and try to downcast the result to an object of
+ * the class |clasp|.
+ *
+ * DANGER: The result may not be same-compartment with |cx|.
+ *
+ * This calls |throwTypeError| if the value isn't an object, cannot be
+ * unwrapped, or isn't an instance of the expected type.  |throwTypeError| must
+ * in fact throw a TypeError (or OOM trying).
+ */
+template <class ErrorCallback>
+inline MOZ_MUST_USE JSObject* UnwrapAndTypeCheckValue(
+    JSContext* cx, HandleValue value, const JSClass* clasp,
+    ErrorCallback throwTypeError) {
+  cx->check(value);
+
+  if (value.isObject() && value.toObject().hasClass(clasp)) {
+    return &value.toObject();
+  }
+
+  return detail::UnwrapAndTypeCheckValueSlowPath(cx, value, clasp,
+                                                 throwTypeError);
+}
+
+/**
  * Remove all wrappers from `args.thisv()` and try to downcast the result to
  * class `T`.
  *
  * DANGER: The result may not be same-compartment with `cx`.
  *
  * This throws a TypeError if the value isn't an object, cannot be unwrapped,
  * or isn't an instance of the expected type.
  */
@@ -229,17 +282,17 @@ inline MOZ_MUST_USE T* UnwrapAndTypeChec
  * `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 `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* UnwrapAndDowncastObject(JSContext* cx, JSObject* obj) {
+inline MOZ_MUST_USE T* UnwrapAndDowncastObject(JSContext* cx, JSObject* obj) {
   static_assert(!std::is_convertible_v<T*, Wrapper*>,
                 "T can't be a Wrapper type; this function discards wrappers");
 
   if (IsProxy(obj)) {
     if (JS_IsDeadWrapper(obj)) {
       JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                                 JSMSG_DEAD_OBJECT);
       return nullptr;
@@ -253,25 +306,70 @@ MOZ_MUST_USE T* UnwrapAndDowncastObject(
       return nullptr;
     }
   }
 
   return &obj->as<T>();
 }
 
 /**
+ * Unwrap an object of a known (but not compile-time-known) class.
+ *
+ * If |obj| is an object with class |clasp|, this returns |obj|.  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 |obj| is
+ * known to have had class |clasp|, or been a wrapper to such an object, at some
+ * point.
+ */
+inline MOZ_MUST_USE JSObject* UnwrapAndDowncastObject(JSContext* cx,
+                                                      JSObject* obj,
+                                                      const JSClass* clasp) {
+  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.
+    obj = obj->maybeUnwrapAs(clasp);
+    if (!obj) {
+      ReportAccessDenied(cx);
+      return nullptr;
+    }
+  }
+
+  MOZ_ASSERT(obj->hasClass(clasp));
+  return obj;
+}
+
+/**
  * 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());
 }
 
 /**
+ * Unwrap an object of a known (but not compile-time-known) class.  See
+ * UnwrapAndDowncastObject.
+ */
+inline MOZ_MUST_USE JSObject* UnwrapAndDowncastValue(JSContext* cx,
+                                                     const Value& value,
+                                                     const JSClass* clasp) {
+  return UnwrapAndDowncastObject(cx, &value.toObject(), clasp);
+}
+
+/**
  * 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
  * sometimes store a cross-compartment wrapper in that slot. And since wrappers
  * can be nuked, that wrapper may become a dead object proxy.
  *
--- a/js/src/vm/JSObject.h
+++ b/js/src/vm/JSObject.h
@@ -542,21 +542,29 @@ class JSObject
    * whether js::Nuke* could have been called in the meantime, check again.
    */
   template <class T>
   T& unwrapAs();
 
   /*
    * Tries to unwrap and downcast to class T. Returns nullptr if (and only if) a
    * wrapper with a security policy is involved. Crashes in all builds if the
-   * (possibly unwrapped) object is not of class T (for example because it's a
+   * (possibly unwrapped) object is not of class T (for example, because it's a
    * dead wrapper).
    */
   template <class T>
-  T* maybeUnwrapAs();
+  inline T* maybeUnwrapAs();
+
+  /*
+   * Tries to unwrap and downcast to an object with class |clasp|.  Returns
+   * nullptr if (and only if) a wrapper with a security policy is involved.
+   * Crashes in all builds if the (possibly unwrapped) object doesn't have class
+   * |clasp| (for example, because it's a dead wrapper).
+   */
+  inline JSObject* maybeUnwrapAs(const JSClass* clasp);
 
   /*
    * Tries to unwrap and downcast to class T. Returns nullptr if a wrapper with
    * a security policy is involved or if the object does not have class T.
    */
   template <class T>
   T* maybeUnwrapIf();
 
@@ -640,17 +648,17 @@ T& JSObject::unwrapAs() {
   // CheckedUnwrap, this does not need to repeat the security check.
   JSObject* unwrapped = js::UncheckedUnwrap(this);
   MOZ_ASSERT(js::CheckedUnwrapStatic(this) == unwrapped,
              "check that the security check we skipped really is redundant");
   return unwrapped->as<T>();
 }
 
 template <class T>
-T* JSObject::maybeUnwrapAs() {
+inline T* JSObject::maybeUnwrapAs() {
   static_assert(!std::is_convertible_v<T*, js::Wrapper*>,
                 "T can't be a Wrapper type; this function discards wrappers");
 
   if (is<T>()) {
     return &as<T>();
   }
 
   JSObject* unwrapped = js::CheckedUnwrapStatic(this);
@@ -660,16 +668,33 @@ T* JSObject::maybeUnwrapAs() {
 
   if (MOZ_LIKELY(unwrapped->is<T>())) {
     return &unwrapped->as<T>();
   }
 
   MOZ_CRASH("Invalid object. Dead wrapper?");
 }
 
+inline JSObject* JSObject::maybeUnwrapAs(const JSClass* clasp) {
+  if (hasClass(clasp)) {
+    return this;
+  }
+
+  JSObject* unwrapped = js::CheckedUnwrapStatic(this);
+  if (!unwrapped) {
+    return nullptr;
+  }
+
+  if (MOZ_LIKELY(unwrapped->hasClass(clasp))) {
+    return unwrapped;
+  }
+
+  MOZ_CRASH("Invalid object. Dead wrapper?");
+}
+
 template <class T>
 T* JSObject::maybeUnwrapIf() {
   static_assert(!std::is_convertible_v<T*, js::Wrapper*>,
                 "T can't be a Wrapper type; this function discards wrappers");
 
   if (is<T>()) {
     return &as<T>();
   }