Bug 1521907 part 1. Add a version of CheckedUnwrap that can do a dynamic security check. r=jandem,sfink
authorBoris Zbarsky <bzbarsky@mit.edu>
Sat, 02 Feb 2019 03:22:29 +0000
changeset 456529 026c691e29c66aa0c3f01c8198b331e9afc26405
parent 456528 e53f607940cb5e1db1bb7eeb3c5c0bc6f4b0d608
child 456530 46854f5097bbe5c3ab485667753d3e368f152193
push id111656
push userdvarga@mozilla.com
push dateSat, 02 Feb 2019 09:51:54 +0000
treeherdermozilla-inbound@d8cebb3b46cf [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjandem, sfink
bugs1521907
milestone67.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 1521907 part 1. Add a version of CheckedUnwrap that can do a dynamic security check. r=jandem,sfink We're going to need this because we will have multiple Realms in the same compartment which want different CheckedUnwrap behavior in some cases. So we need to be able to check which Realm we're in. Differential Revision: https://phabricator.services.mozilla.com/D17881
js/public/Wrapper.h
js/src/proxy/Wrapper.cpp
--- a/js/public/Wrapper.h
+++ b/js/public/Wrapper.h
@@ -126,16 +126,27 @@ class JS_FRIEND_API Wrapper : public For
  public:
   explicit constexpr Wrapper(unsigned aFlags, bool aHasPrototype = false,
                              bool aHasSecurityPolicy = false)
       : ForwardingProxyHandler(&family, aHasPrototype, aHasSecurityPolicy),
         mFlags(aFlags) {}
 
   virtual bool finalizeInBackground(const Value& priv) const override;
 
+  /**
+   * A hook subclasses can override to implement CheckedUnwrapDynamic
+   * behavior.  The JSContext represents the "who is trying to unwrap?" Realm.
+   * The JSObject is the wrapper that the caller is trying to unwrap.
+   */
+  virtual bool dynamicCheckedUnwrapAllowed(HandleObject obj,
+                                           JSContext* cx) const {
+    MOZ_ASSERT(hasSecurityPolicy(), "Why are you asking?");
+    return false;
+  }
+
   using BaseProxyHandler::Action;
 
   enum Flags { CROSS_COMPARTMENT = 1 << 0, LAST_USED_FLAG = CROSS_COMPARTMENT };
 
   static JSObject* New(JSContext* cx, JSObject* obj, const Wrapper* handler,
                        const WrapperOptions& options = WrapperOptions());
 
   static JSObject* Renew(JSObject* existing, JSObject* obj,
@@ -382,32 +393,76 @@ inline bool IsCrossCompartmentWrapper(co
 //
 // ExposeToActiveJS is called on wrapper targets to allow gray marking
 // assertions to work while an incremental GC is in progress, but this means
 // that this cannot be called from the GC or off the main thread.
 JS_FRIEND_API JSObject* UncheckedUnwrap(JSObject* obj,
                                         bool stopAtWindowProxy = true,
                                         unsigned* flagsp = nullptr);
 
-// Given a JSObject, returns that object stripped of wrappers. At each stage,
-// the security wrapper has the opportunity to veto the unwrap. If
-// stopAtWindowProxy is true, then this returns the WindowProxy if it was
-// previously wrapped.
+// Given a JSObject, returns that object stripped of wrappers, except
+// WindowProxy wrappers.  At each stage, the wrapper has the opportunity to veto
+// the unwrap. Null is returned if there are security wrappers that can't be
+// unwrapped.
+//
+// This does a static-only unwrap check: it basically checks whether _all_
+// globals in the wrapper's source compartment should be able to access the
+// wrapper target.  This won't necessarily return the right thing for the HTML
+// spec's cross-origin objects (WindowProxy and Location), but is fine to use
+// when failure to unwrap one of those objects wouldn't be a problem.  For
+// example, if you want to test whether your target object is a specific class
+// that's not WindowProxy or Location, you can use this.
 //
 // ExposeToActiveJS is called on wrapper targets to allow gray marking
 // assertions to work while an incremental GC is in progress, but this means
 // that this cannot be called from the GC or off the main thread.
+JS_FRIEND_API JSObject* CheckedUnwrapStatic(JSObject* obj);
+
+// Old CheckedUnwrap API that we would like to remove once we convert all
+// callers to CheckedUnwrapStatic or CheckedUnwrapDynamic.  If stopAtWindowProxy
+// is true, then this returns the WindowProxy if a WindowProxy is encountered;
+// otherwise it will unwrap the WindowProxy and return a Window.
 JS_FRIEND_API JSObject* CheckedUnwrap(JSObject* obj,
                                       bool stopAtWindowProxy = true);
 
 // Unwrap only the outermost security wrapper, with the same semantics as
 // above. This is the checked version of Wrapper::wrappedObject.
 JS_FRIEND_API JSObject* UnwrapOneChecked(JSObject* obj,
                                          bool stopAtWindowProxy = true);
 
+// Given a JSObject, returns that object stripped of wrappers. At each stage,
+// the security wrapper has the opportunity to veto the unwrap. If
+// stopAtWindowProxy is true, then this returns the WindowProxy if it was
+// previously wrapped.  Null is returned if there are security wrappers that
+// can't be unwrapped.
+//
+// ExposeToActiveJS is called on wrapper targets to allow gray marking
+// assertions to work while an incremental GC is in progress, but this means
+// that this cannot be called from the GC or off the main thread.
+//
+// The JSContext argument will be used for dynamic checks (needed by WindowProxy
+// and Location) and should represent the Realm doing the unwrapping.  It is not
+// used to throw exceptions; this function never throws.
+//
+// This function may be able to GC (and the static analysis definitely thinks it
+// can), but it still takes a JSObject* argument, because some of its callers
+// would actually have a bit of a hard time producing a Rooted.  And it ends up
+// having to root internally anyway, because it wants to use the value in a loop
+// and you can't assign to a HandleObject.  What this means is that callers who
+// plan to use the argument object after they have called this function will
+// need to root it to avoid hazard failures, even though this function doesn't
+// require a Handle.
+JS_FRIEND_API JSObject* CheckedUnwrapDynamic(JSObject* obj, JSContext* cx,
+                                             bool stopAtWindowProxy = true);
+
+// Unwrap only the outermost security wrapper, with the same semantics as
+// above. This is the checked version of Wrapper::wrappedObject.
+JS_FRIEND_API JSObject* UnwrapOneCheckedDynamic(HandleObject obj, JSContext* cx,
+                                                bool stopAtWindowProxy = true);
+
 // Given a JSObject, returns that object stripped of wrappers. This returns the
 // WindowProxy if it was previously wrapped.
 //
 // ExposeToActiveJS is not called on wrapper targets so this can be called from
 // the GC or off the main thread.
 JS_FRIEND_API JSObject* UncheckedUnwrapWithoutExpose(JSObject* obj);
 
 void ReportAccessDenied(JSContext* cx);
--- a/js/src/proxy/Wrapper.cpp
+++ b/js/src/proxy/Wrapper.cpp
@@ -350,16 +350,22 @@ JS_FRIEND_API JSObject* js::UncheckedUnw
     wrapped = Wrapper::wrappedObject(wrapped);
   }
   if (flagsp) {
     *flagsp = flags;
   }
   return wrapped;
 }
 
+JS_FRIEND_API JSObject* js::CheckedUnwrapStatic(JSObject* obj) {
+  // For now, just forward to the old API.  Once we remove it, we can
+  // inline it here, without the stopAtWindowProxy bits.
+  return CheckedUnwrap(obj);
+}
+
 JS_FRIEND_API JSObject* js::CheckedUnwrap(JSObject* obj,
                                           bool stopAtWindowProxy) {
   while (true) {
     JSObject* wrapper = obj;
     obj = UnwrapOneChecked(obj, stopAtWindowProxy);
     if (!obj || obj == wrapper) {
       return obj;
     }
@@ -375,16 +381,52 @@ JS_FRIEND_API JSObject* js::UnwrapOneChe
       MOZ_UNLIKELY(stopAtWindowProxy && IsWindowProxy(obj))) {
     return obj;
   }
 
   const Wrapper* handler = Wrapper::wrapperHandler(obj);
   return handler->hasSecurityPolicy() ? nullptr : Wrapper::wrappedObject(obj);
 }
 
+JS_FRIEND_API JSObject* js::CheckedUnwrapDynamic(JSObject* obj, JSContext* cx,
+                                                 bool stopAtWindowProxy) {
+  RootedObject wrapper(cx, obj);
+  while (true) {
+    JSObject* unwrapped =
+        UnwrapOneCheckedDynamic(wrapper, cx, stopAtWindowProxy);
+    if (!unwrapped || unwrapped == wrapper) {
+      return unwrapped;
+    }
+    wrapper = unwrapped;
+  }
+}
+
+JS_FRIEND_API JSObject* js::UnwrapOneCheckedDynamic(HandleObject obj,
+                                                    JSContext* cx,
+                                                    bool stopAtWindowProxy) {
+  MOZ_ASSERT(!JS::RuntimeHeapIsCollecting());
+  MOZ_ASSERT(CurrentThreadCanAccessRuntime(obj->runtimeFromAnyThread()));
+  // We should know who's asking.
+  MOZ_ASSERT(cx);
+  MOZ_ASSERT(cx->realm());
+
+  if (!obj->is<WrapperObject>() ||
+      MOZ_UNLIKELY(stopAtWindowProxy && IsWindowProxy(obj))) {
+    return obj;
+  }
+
+  const Wrapper* handler = Wrapper::wrapperHandler(obj);
+  if (!handler->hasSecurityPolicy() ||
+      handler->dynamicCheckedUnwrapAllowed(obj, cx)) {
+    return Wrapper::wrappedObject(obj);
+  }
+
+  return nullptr;
+}
+
 void js::ReportAccessDenied(JSContext* cx) {
   JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                             JSMSG_OBJECT_ACCESS_DENIED);
 }
 
 const char Wrapper::family = 0;
 const Wrapper Wrapper::singleton((unsigned)0);
 const Wrapper Wrapper::singletonWithPrototype((unsigned)0, true);