[mq]: share-compartments draft
authorBoris Zbarsky <bzbarsky@mit.edu>
Fri, 08 Feb 2019 17:26:18 -0500
changeset 1835468 a553e62d36f1efb6bdad88f177e5822cdf892687
parent 1833651 9880a44921d142da72876fd7a8cf88693d7248e0
child 1835469 e8e1d28fb46030ba9d1263c05fe5105dba908506
child 1835789 4f4e9e851b34163e26f7565c8b2d347ed0db3fb3
push id333178
push userbzbarsky@mozilla.com
push dateFri, 08 Feb 2019 22:26:54 +0000
treeherdertry@e8e1d28fb460 [default view] [failures only]
milestone67.0a1
[mq]: share-compartments
dom/base/nsGlobalWindowOuter.cpp
js/public/HeapAPI.h
js/src/jsapi.cpp
js/src/jsapi.h
js/src/jsfriendapi.cpp
js/src/jsfriendapi.h
js/xpconnect/src/XPCWrappedNativeScope.cpp
js/xpconnect/src/xpcprivate.h
--- a/dom/base/nsGlobalWindowOuter.cpp
+++ b/dom/base/nsGlobalWindowOuter.cpp
@@ -1781,29 +1781,73 @@ static bool InitializeLegacyNetscapeObje
 
   /* Define PrivilegeManager object with the necessary "static" methods. */
   obj = JS_DefineObject(aCx, obj, "PrivilegeManager", nullptr);
   NS_ENSURE_TRUE(obj, false);
 
   return JS_DefineFunctions(aCx, obj, EnablePrivilegeSpec);
 }
 
+namespace {
+struct CompartmentFinderState {
+  CompartmentFinderState(JS::Zone* aZone, nsIPrincipal* aPrincipal)
+      : zone(aZone), principal(aPrincipal), compartment(nullptr) {}
+
+  // Inputs: we look for a compartment in the given zone which is
+  // same-origin with the given principal.
+  JS::Zone* zone;
+  nsIPrincipal* principal;
+
+  // Output: We set this member if we find a compartment.
+  JS::Compartment* compartment;
+};
+}  // anonymous namespace
+
+static void FindSameOriginCompartment(JSContext* aCx, void* aData,
+                                      JS::Compartment* aCompartment) {
+  auto* data = static_cast<CompartmentFinderState*>(aData);
+  if (data->compartment) {
+    // We found one already.  Don't do anything else.
+    return;
+  }
+
+  if (JS::GetCompartmentZone(aCompartment) != data->zone) {
+    // Wrong zone, keep going.
+    return;
+  }
+
+  auto* compartmentPrivate = xpc::CompartmentPrivate::Get(aCompartment);
+  if (compartmentPrivate->CanShareCompartmentWith(data->principal) &&
+      // Don't allow creating new Realms in nuked compartments.
+      !js::IsNukedCompartment(aCompartment)) {
+    data->compartment = aCompartment;
+  }
+}
+
 static JS::RealmCreationOptions& SelectZone(
-    nsIPrincipal* aPrincipal, nsGlobalWindowInner* aNewInner,
+    JSContext* aCx, nsIPrincipal* aPrincipal, nsGlobalWindowInner* aNewInner,
     JS::RealmCreationOptions& aOptions) {
   // Use the shared system compartment for chrome windows.
   if (nsContentUtils::IsSystemPrincipal(aPrincipal)) {
     return aOptions.setExistingCompartment(xpc::PrivilegedJunkScope());
   }
 
   if (aNewInner->GetOuterWindow()) {
     nsGlobalWindowOuter* top = aNewInner->GetTopInternal();
 
     // If we have a top-level window, use its zone.
     if (top && top->GetGlobalJSObject()) {
+      JS::Zone* zone = JS::GetObjectZone(top->GetGlobalJSObject());
+      // Now try to find and existing compartment that's same-origin
+      // with our principal.
+      CompartmentFinderState data(zone, aPrincipal);
+      JS_IterateCompartments(aCx, &data, FindSameOriginCompartment);
+      if (data.compartment) {
+        return aOptions.setExistingCompartment(data.compartment);
+      }
       return aOptions.setNewCompartmentInExistingZone(top->GetGlobalJSObject());
     }
   }
 
   return aOptions.setNewCompartmentAndZone();
 }
 
 /**
@@ -1823,17 +1867,17 @@ static nsresult CreateNativeGlobalForInn
 
   // DOMWindow with nsEP is not supported, we have to make sure
   // no one creates one accidentally.
   nsCOMPtr<nsIExpandedPrincipal> nsEP = do_QueryInterface(aPrincipal);
   MOZ_RELEASE_ASSERT(!nsEP, "DOMWindow with nsEP is not supported");
 
   JS::RealmOptions options;
 
-  SelectZone(aPrincipal, aNewInner, options.creationOptions());
+  SelectZone(aCx, aPrincipal, aNewInner, options.creationOptions());
 
   options.creationOptions().setSecureContext(aIsSecureContext);
 
   xpc::InitGlobalObjectOptions(options, aPrincipal);
 
   // Determine if we need the Components object.
   bool needComponents = nsContentUtils::IsSystemPrincipal(aPrincipal) ||
                         TreatAsRemoteXUL(aPrincipal);
--- a/js/public/HeapAPI.h
+++ b/js/public/HeapAPI.h
@@ -100,16 +100,18 @@ inline void AssertGCThingHasType(js::gc:
 
 MOZ_ALWAYS_INLINE bool IsInsideNursery(const js::gc::Cell* cell);
 
 } /* namespace gc */
 } /* namespace js */
 
 namespace JS {
 
+class Compartment;
+
 /*
  * This list enumerates the different types of conceptual stacks we have in
  * SpiderMonkey. In reality, they all share the C stack, but we allow different
  * stack limits depending on the type of code running.
  */
 enum StackKind {
   StackForSystemCode,       // C++, such as the GC, running on behalf of the VM.
   StackForTrustedScript,    // Script running with trusted principals.
@@ -506,16 +508,18 @@ static MOZ_ALWAYS_INLINE Zone* GetString
   if (!js::gc::IsInsideNursery(reinterpret_cast<js::gc::Cell*>(str))) {
     return js::gc::detail::GetGCThingZone(reinterpret_cast<uintptr_t>(str));
   }
   return GetNurseryStringZone(str);
 }
 
 extern JS_PUBLIC_API Zone* GetObjectZone(JSObject* obj);
 
+extern JS_PUBLIC_API Zone* GetCompartmentZone(Compartment* comp);
+
 static MOZ_ALWAYS_INLINE bool GCThingIsMarkedGray(GCCellPtr thing) {
   if (thing.mayBeOwnedByOtherRuntime()) {
     return false;
   }
   return js::gc::detail::CellIsMarkedGrayIfKnown(thing.asCell());
 }
 
 extern JS_PUBLIC_API JS::TraceKind GCThingTraceKind(void* thing);
--- a/js/src/jsapi.cpp
+++ b/js/src/jsapi.cpp
@@ -1577,16 +1577,23 @@ JS::RealmCreationOptions::setNewCompartm
 
 JS::RealmCreationOptions& JS::RealmCreationOptions::setExistingCompartment(
     JSObject* obj) {
   compSpec_ = CompartmentSpecifier::ExistingCompartment;
   comp_ = obj->compartment();
   return *this;
 }
 
+JS::RealmCreationOptions& JS::RealmCreationOptions::setExistingCompartment(
+    JS::Compartment* compartment) {
+  compSpec_ = CompartmentSpecifier::ExistingCompartment;
+  comp_ = compartment;
+  return *this;
+}
+
 JS::RealmCreationOptions& JS::RealmCreationOptions::setNewCompartmentAndZone() {
   compSpec_ = CompartmentSpecifier::NewCompartmentAndZone;
   comp_ = nullptr;
   return *this;
 }
 
 const JS::RealmCreationOptions& JS::RealmCreationOptionsRef(Realm* realm) {
   return realm->creationOptions();
@@ -6084,16 +6091,20 @@ JS_PUBLIC_API bool JS::CopyAsyncStack(JS
     return false;
   }
   stackp.set(frame.get());
   return true;
 }
 
 JS_PUBLIC_API Zone* JS::GetObjectZone(JSObject* obj) { return obj->zone(); }
 
+JS_PUBLIC_API Zone* JS::GetCompartmentZone(JS::Compartment* comp) {
+  return comp->zone();
+}
+
 JS_PUBLIC_API Zone* JS::GetNurseryStringZone(JSString* str) {
   MOZ_ASSERT(!str->isTenured());
   return str->zone();
 }
 
 JS_PUBLIC_API JS::TraceKind JS::GCThingTraceKind(void* thing) {
   MOZ_ASSERT(thing);
   return static_cast<js::gc::Cell*>(thing)->getTraceKind();
--- a/js/src/jsapi.h
+++ b/js/src/jsapi.h
@@ -967,16 +967,17 @@ class JS_PUBLIC_API RealmCreationOptions
   CompartmentSpecifier compartmentSpecifier() const { return compSpec_; }
 
   // Set the compartment/zone to use for the realm. See CompartmentSpecifier
   // above.
   RealmCreationOptions& setNewCompartmentInSystemZone();
   RealmCreationOptions& setNewCompartmentInExistingZone(JSObject* obj);
   RealmCreationOptions& setNewCompartmentAndZone();
   RealmCreationOptions& setExistingCompartment(JSObject* obj);
+  RealmCreationOptions& setExistingCompartment(JS::Compartment* compartment);
 
   // Certain compartments are implementation details of the embedding, and
   // references to them should never leak out to script. This flag causes this
   // realm to skip firing onNewGlobalObject and makes addDebuggee a no-op for
   // this global.
   //
   // Debugger visibility is per-compartment, not per-realm (it's only practical
   // to enforce visibility on compartment boundaries), so if a realm is being
--- a/js/src/jsfriendapi.cpp
+++ b/js/src/jsfriendapi.cpp
@@ -1149,16 +1149,20 @@ JS_FRIEND_API JS::Realm* js::GetAnyRealm
 }
 
 JS_FRIEND_API JSObject* js::GetFirstGlobalInCompartment(JS::Compartment* comp) {
   JSObject* global = comp->firstRealm()->maybeGlobal();
   MOZ_ASSERT(global);
   return global;
 }
 
+JS_FRIEND_API bool js::IsNukedCompartment(JS::Compartment* comp) {
+  return comp->nukedOutgoingWrappers;
+}
+
 void JS::ObjectPtr::finalize(JSRuntime* rt) {
   if (IsIncrementalBarrierNeeded(rt->mainContextFromOwnThread())) {
     IncrementalPreWriteBarrier(value);
   }
   value = nullptr;
 }
 
 void JS::ObjectPtr::finalize(JSContext* cx) { finalize(cx->runtime()); }
--- a/js/src/jsfriendapi.h
+++ b/js/src/jsfriendapi.h
@@ -547,16 +547,21 @@ SizeOfDataIfCDataObject(mozilla::MallocS
 extern JS_FRIEND_API JS::Realm* GetAnyRealmInZone(JS::Zone* zone);
 
 // Returns the first realm's global in a compartment. Note: this is not
 // guaranteed to always be the same realm because individual realms can be
 // collected by the GC.
 extern JS_FRIEND_API JSObject* GetFirstGlobalInCompartment(
     JS::Compartment* comp);
 
+// Returns true if we consider this compartment nuked, in the sense
+// that all realms are nuked and we won't allow any more references
+// out of this compartment to other compartments.
+extern JS_FRIEND_API bool IsNukedCompartment(JS::Compartment* comp);
+
 /*
  * Shadow declarations of JS internal structures, for access by inline access
  * functions below. Do not use these structures in any other way. When adding
  * new fields for access by inline methods, make sure to add static asserts to
  * the original header file to ensure that offsets are consistent.
  */
 namespace shadow {
 
--- a/js/xpconnect/src/XPCWrappedNativeScope.cpp
+++ b/js/xpconnect/src/XPCWrappedNativeScope.cpp
@@ -23,42 +23,46 @@ using namespace mozilla;
 using namespace xpc;
 using namespace JS;
 
 /***************************************************************************/
 
 XPCWrappedNativeScope* XPCWrappedNativeScope::gScopes = nullptr;
 XPCWrappedNativeScope* XPCWrappedNativeScope::gDyingScopes = nullptr;
 
+static bool RemoteXULForbidsXBLScopeForPrincipal(nsIPrincipal* aPrincipal) {
+  // AllowXULXBLForPrincipal will return true for system principal, but we
+  // don't want that here.  
+  MOZ_ASSERT(nsContentUtils::IsInitialized());
+  if (aPrincipal->IsSystemPrincipal()) {
+    return false;
+  }
+
+  // If this domain isn't whitelisted, we're done.
+  if (!nsContentUtils::AllowXULXBLForPrincipal(aPrincipal)) {
+    return false;
+  }
+
+  // Check the pref to determine how we should behave.
+  return !Preferences::GetBool("dom.use_xbl_scopes_for_remote_xul", false);
+}
+
 static bool RemoteXULForbidsXBLScope(HandleObject aFirstGlobal) {
   MOZ_ASSERT(aFirstGlobal);
 
   // Certain singleton sandoxes are created very early in startup - too early
   // to call into AllowXULXBLForPrincipal. We never create XBL scopes for
   // sandboxes anway, and certainly not for these singleton scopes. So we just
   // short-circuit here.
   if (IsSandbox(aFirstGlobal)) {
     return false;
   }
 
-  // AllowXULXBLForPrincipal will return true for system principal, but we
-  // don't want that here.
   nsIPrincipal* principal = xpc::GetObjectPrincipal(aFirstGlobal);
-  MOZ_ASSERT(nsContentUtils::IsInitialized());
-  if (nsContentUtils::IsSystemPrincipal(principal)) {
-    return false;
-  }
-
-  // If this domain isn't whitelisted, we're done.
-  if (!nsContentUtils::AllowXULXBLForPrincipal(principal)) {
-    return false;
-  }
-
-  // Check the pref to determine how we should behave.
-  return !Preferences::GetBool("dom.use_xbl_scopes_for_remote_xul", false);
+  return RemoteXULForbidsXBLScopeForPrincipal(principal);
 }
 
 XPCWrappedNativeScope::XPCWrappedNativeScope(JS::Compartment* aCompartment,
                                              JS::HandleObject aFirstGlobal)
     : mWrappedNativeMap(Native2WrappedNativeMap::newMap(XPC_NATIVE_MAP_LENGTH)),
       mWrappedNativeProtoMap(
           ClassInfo2WrappedNativeProtoMap::newMap(XPC_NATIVE_PROTO_MAP_LENGTH)),
       mComponents(nullptr),
@@ -203,16 +207,20 @@ JSObject* XPCWrappedNativeScope::EnsureC
                     "nsXBLPrototypeScript compilation scope"));
 
   // We can probably remove EnsureContentXBLScope and clean up all its callers,
   // but a bunch (all?) of those callers will just go away when we remove XBL
   // support, so it's simpler to just leave it here as a no-op.
   return global;
 }
 
+bool XPCWrappedNativeScope::XBLScopeStateMatches(nsIPrincipal* aPrincipal) {
+  return mAllowContentXBLScope == !RemoteXULForbidsXBLScopeForPrincipal(aPrincipal);
+}
+
 bool XPCWrappedNativeScope::AllowContentXBLScope(Realm* aRealm) {
   // We only disallow XBL scopes in remote XUL situations.
   MOZ_ASSERT_IF(!mAllowContentXBLScope, nsContentUtils::AllowXULXBLForPrincipal(
                                             xpc::GetRealmPrincipal(aRealm)));
   return mAllowContentXBLScope;
 }
 
 namespace xpc {
--- a/js/xpconnect/src/xpcprivate.h
+++ b/js/xpconnect/src/xpcprivate.h
@@ -889,16 +889,21 @@ class XPCWrappedNativeScope final {
   static bool IsDyingScope(XPCWrappedNativeScope* scope);
 
   // Gets the appropriate scope object for XBL in this compartment. This method
   // relies on compartment-per-global still (and release-asserts this). The
   // context must be same-realm with this compartment's single global upon
   // entering, and the scope object is wrapped into this compartment.
   JSObject* EnsureContentXBLScope(JSContext* cx);
 
+  // Check whether our mAllowContentXBLScope state matches the given
+  // principal.  This is used to avoid sharing compartments on
+  // mismatch.
+  bool XBLScopeStateMatches(nsIPrincipal* aPrincipal);
+
   XPCWrappedNativeScope(JS::Compartment* aCompartment,
                         JS::HandleObject aFirstGlobal);
 
   nsAutoPtr<JSObject2JSObjectMap> mWaiverWrapperMap;
 
   JS::Compartment* Compartment() const { return mCompartment; }
 
   // Returns the global to use for new WrappedNative objects allocated in this
@@ -2681,16 +2686,28 @@ class CompartmentPrivate {
     return Get(compartment);
   }
 
   static CompartmentPrivate* Get(JSObject* object) {
     JS::Compartment* compartment = js::GetObjectCompartment(object);
     return Get(compartment);
   }
 
+  bool CanShareCompartmentWith(nsIPrincipal* principal) {
+    // Only share if we're same-origin with the principal.
+    if (!originInfo.IsSameOrigin(principal)) {
+      return false;
+    }
+
+    // Don't share if we have any weird state set.
+    return !wantXrays && !isWebExtensionContentScript &&
+           !isContentXBLCompartment && !isUAWidgetCompartment &&
+           !universalXPConnectEnabled && scope->XBLScopeStateMatches(principal);
+  }
+
   CompartmentOriginInfo originInfo;
 
   // Our XPCWrappedNativeScope.
   XPCWrappedNativeScope* scope;
 
   // Controls whether this compartment gets Xrays to same-origin. This behavior
   // is deprecated, but is still the default for sandboxes for compatibity
   // reasons.