Bug 1322273: Cut wrappers that point out of a browser compartment when nuking it. r=bholley
authorKris Maglione <maglione.k@gmail.com>
Thu, 23 Feb 2017 13:56:10 -0800
changeset 373666 8999082d8a89381727c2eee5e26ff4ab653cde4e
parent 373665 37a4221a05122c908f37c24f40bc7bc4946a151f
child 373667 d93ecd34da85f28e71dca30633dc5a14046f56fd
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)
reviewersbholley
bugs1322273
milestone54.0a1
Bug 1322273: Cut wrappers that point out of a browser compartment when nuking it. r=bholley MozReview-Commit-ID: FLS3xRgih2u
dom/base/nsGlobalWindow.cpp
js/src/jsfriendapi.h
js/src/proxy/CrossCompartmentWrapper.cpp
js/xpconnect/src/XPCComponents.cpp
js/xpconnect/src/XPCJSContext.cpp
js/xpconnect/src/xpcpublic.h
js/xpconnect/tests/mochitest/mochitest.ini
js/xpconnect/tests/mochitest/test_nukeContentWindow.html
--- a/dom/base/nsGlobalWindow.cpp
+++ b/dom/base/nsGlobalWindow.cpp
@@ -9401,31 +9401,26 @@ public:
       JS::Rooted<JSObject*> obj(cx, currentInner->FastGetGlobalJSObject());
       if (obj && !js::IsSystemCompartment(js::GetObjectCompartment(obj))) {
         JSCompartment* cpt = js::GetObjectCompartment(obj);
         nsCOMPtr<nsIPrincipal> pc = nsJSPrincipals::get(JS_GetCompartmentPrincipals(cpt));
 
         nsAutoString addonId;
         if (NS_SUCCEEDED(pc->GetAddonId(addonId)) && !addonId.IsEmpty()) {
           // We want to nuke all references to the add-on compartment.
-          js::NukeCrossCompartmentWrappers(cx, js::AllCompartments(),
-                                           js::SingleCompartment(cpt),
-                                           win->IsInnerWindow() ? js::DontNukeWindowReferences
-                                                                : js::NukeWindowReferences);
-
-          // Now mark the compartment as nuked and non-scriptable.
-          auto compartmentPrivate = xpc::CompartmentPrivate::Get(cpt);
-          compartmentPrivate->wasNuked = true;
-          compartmentPrivate->scriptability.Block();
+          xpc::NukeAllWrappersForCompartment(cx, cpt,
+                                             win->IsInnerWindow() ? js::DontNukeWindowReferences
+                                                                  : js::NukeWindowReferences);
         } else {
           // We only want to nuke wrappers for the chrome->content case
           js::NukeCrossCompartmentWrappers(cx, BrowserCompartmentMatcher(),
                                            js::SingleCompartment(cpt),
                                            win->IsInnerWindow() ? js::DontNukeWindowReferences
-                                                                : js::NukeWindowReferences);
+                                                                : js::NukeWindowReferences,
+                                           js::NukeIncomingReferences);
         }
       }
     }
 
     return NS_OK;
   }
 
 private:
--- a/js/src/jsfriendapi.h
+++ b/js/src/jsfriendapi.h
@@ -1125,22 +1125,27 @@ GetErrorTypeName(JSContext* cx, int16_t 
 extern JS_FRIEND_API(unsigned)
 GetEnterCompartmentDepth(JSContext* cx);
 #endif
 
 class RegExpGuard;
 extern JS_FRIEND_API(bool)
 RegExpToSharedNonInline(JSContext* cx, JS::HandleObject regexp, RegExpGuard* shared);
 
-/* Implemented in jswrapper.cpp. */
+/* Implemented in CrossCompartmentWrapper.cpp. */
 typedef enum NukeReferencesToWindow {
     NukeWindowReferences,
     DontNukeWindowReferences
 } NukeReferencesToWindow;
 
+typedef enum NukeReferencesFromTarget {
+    NukeAllReferences,
+    NukeIncomingReferences,
+} NukeReferencesFromTarget;
+
 /*
  * These filters are designed to be ephemeral stack classes, and thus don't
  * do any rooting or holding of their members.
  */
 struct CompartmentFilter {
     virtual bool match(JSCompartment* c) const = 0;
 };
 
@@ -1173,17 +1178,18 @@ struct CompartmentsWithPrincipals : publ
         return JS_GetCompartmentPrincipals(c) == principals;
     }
 };
 
 extern JS_FRIEND_API(bool)
 NukeCrossCompartmentWrappers(JSContext* cx,
                              const CompartmentFilter& sourceFilter,
                              const CompartmentFilter& targetFilter,
-                             NukeReferencesToWindow nukeReferencesToWindow);
+                             NukeReferencesToWindow nukeReferencesToWindow,
+                             NukeReferencesFromTarget nukeReferencesFromTarget);
 
 /* Specify information about DOMProxy proxies in the DOM, for use by ICs. */
 
 /*
  * The DOMProxyShadowsCheck function will be called to check if the property for
  * id should be gotten from the prototype, or if there is an own property that
  * shadows it.
  * * If ShadowsViaDirectExpando is returned, then the slot at
--- a/js/src/proxy/CrossCompartmentWrapper.cpp
+++ b/js/src/proxy/CrossCompartmentWrapper.cpp
@@ -508,48 +508,53 @@ js::NukeCrossCompartmentWrapper(JSContex
  * point to the window object on page navigation (inner window destruction)
  * and only do that on tab close (outer window destruction).  Thus the
  * option of how to handle the global object.
  */
 JS_FRIEND_API(bool)
 js::NukeCrossCompartmentWrappers(JSContext* cx,
                                  const CompartmentFilter& sourceFilter,
                                  const CompartmentFilter& targetFilter,
-                                 js::NukeReferencesToWindow nukeReferencesToWindow)
+                                 js::NukeReferencesToWindow nukeReferencesToWindow,
+                                 js::NukeReferencesFromTarget nukeReferencesFromTarget)
 {
     CHECK_REQUEST(cx);
     JSRuntime* rt = cx->runtime();
 
     EvictAllNurseries(rt);
 
-    // Iterate through scopes looking for system cross compartment wrappers
-    // that point to an object that shares a global with obj.
-
     for (CompartmentsIter c(rt, SkipAtoms); !c.done(); c.next()) {
         if (!sourceFilter.match(c))
             continue;
 
+        // If the compartment matches both the source and target filter, we may
+        // want to cut both incoming and outgoing wrappers.
+        bool nukeAll = (nukeReferencesFromTarget == NukeAllReferences &&
+                        targetFilter.match(c));
+
         // Iterate the wrappers looking for anything interesting.
         for (JSCompartment::WrapperEnum e(c); !e.empty(); e.popFront()) {
             // Some cross-compartment wrappers are for strings.  We're not
             // interested in those.
             const CrossCompartmentKey& k = e.front().key();
             if (!k.is<JSObject*>())
                 continue;
 
             AutoWrapperRooter wobj(cx, WrapperValue(e));
             JSObject* wrapped = UncheckedUnwrap(wobj);
 
+            // We only skip nuking window references that point to a target
+            // compartment, not the ones that belong to it.
             if (nukeReferencesToWindow == DontNukeWindowReferences &&
-                IsWindowProxy(wrapped))
+                MOZ_LIKELY(!nukeAll) && IsWindowProxy(wrapped))
             {
                 continue;
             }
 
-            if (targetFilter.match(wrapped->compartment())) {
+            if (MOZ_UNLIKELY(nukeAll) || targetFilter.match(wrapped->compartment())) {
                 // We found a wrapper to nuke.
                 e.removeFront();
                 NukeCrossCompartmentWrapper(cx, wobj);
             }
         }
     }
 
     return true;
--- a/js/xpconnect/src/XPCComponents.cpp
+++ b/js/xpconnect/src/XPCComponents.cpp
@@ -3015,24 +3015,18 @@ NS_IMETHODIMP
 nsXPCComponents_Utils::NukeSandbox(HandleValue obj, JSContext* cx)
 {
     PROFILER_LABEL_FUNC(js::ProfileEntry::Category::JS);
     NS_ENSURE_TRUE(obj.isObject(), NS_ERROR_INVALID_ARG);
     JSObject* wrapper = &obj.toObject();
     NS_ENSURE_TRUE(IsWrapper(wrapper), NS_ERROR_INVALID_ARG);
     RootedObject sb(cx, UncheckedUnwrap(wrapper));
     NS_ENSURE_TRUE(IsSandbox(sb), NS_ERROR_INVALID_ARG);
-    NukeCrossCompartmentWrappers(cx, AllCompartments(),
-                                 SingleCompartment(GetObjectCompartment(sb)),
-                                 NukeWindowReferences);
-
-    // Now mark the compartment as nuked and non-scriptable.
-    auto compartmentPrivate = xpc::CompartmentPrivate::Get(sb);
-    compartmentPrivate->wasNuked = true;
-    compartmentPrivate->scriptability.Block();
+
+    xpc::NukeAllWrappersForCompartment(cx, GetObjectCompartment(sb));
 
     return NS_OK;
 }
 
 NS_IMETHODIMP
 nsXPCComponents_Utils::BlockScriptForGlobal(HandleValue globalArg,
                                             JSContext* cx)
 {
--- a/js/xpconnect/src/XPCJSContext.cpp
+++ b/js/xpconnect/src/XPCJSContext.cpp
@@ -575,16 +575,47 @@ AddonWindowOrNull(JSObject* aObj)
 
 nsGlobalWindow*
 CurrentWindowOrNull(JSContext* cx)
 {
     JSObject* glob = JS::CurrentGlobalOrNull(cx);
     return glob ? WindowOrNull(glob) : nullptr;
 }
 
+// Nukes all wrappers into or out of the given compartment, and prevents new
+// wrappers from being created. Additionally marks the compartment as
+// unscriptable after wrappers have been nuked.
+//
+// Note: This should *only* be called for browser or extension compartments.
+// Wrappers between web compartments must never be cut in web-observable
+// ways.
+void
+NukeAllWrappersForCompartment(JSContext* cx, JSCompartment* compartment,
+                              js::NukeReferencesToWindow nukeReferencesToWindow)
+{
+    // First, nuke all wrappers into or out of the target compartment. Once
+    // the compartment is marked as nuked, WrapperFactory will refuse to
+    // create new live wrappers for it, in either direction. This means that
+    // we need to be sure that we don't have any existing cross-compartment
+    // wrappers which may be replaced with dead wrappers during unrelated
+    // wrapper recomputation *before* we set that bit.
+    js::NukeCrossCompartmentWrappers(cx, js::AllCompartments(),
+                                     js::SingleCompartment(compartment),
+                                     nukeReferencesToWindow,
+                                     js::NukeAllReferences);
+
+    // At this point, we should cross-compartment wrappers for the nuked
+    // compartment. Set the wasNuked bit so WrapperFactory will return a
+    // DeadObjectProxy when asked to create a new wrapper for it, and mark as
+    // unscriptable.
+    auto compartmentPrivate = xpc::CompartmentPrivate::Get(compartment);
+    compartmentPrivate->wasNuked = true;
+    compartmentPrivate->scriptability.Block();
+}
+
 } // namespace xpc
 
 static void
 CompartmentDestroyedCallback(JSFreeOp* fop, JSCompartment* compartment)
 {
     // NB - This callback may be called in JS_DestroyContext, which happens
     // after the XPCJSContext has been torn down.
 
--- a/js/xpconnect/src/xpcpublic.h
+++ b/js/xpconnect/src/xpcpublic.h
@@ -395,16 +395,19 @@ bool StringToJsval(JSContext* cx, mozill
         rval.setNull();
         return true;
     }
     return NonVoidStringToJsval(cx, str, rval);
 }
 
 nsIPrincipal* GetCompartmentPrincipal(JSCompartment* compartment);
 
+void NukeAllWrappersForCompartment(JSContext* cx, JSCompartment* compartment,
+                                   js::NukeReferencesToWindow nukeReferencesToWindow = js::NukeWindowReferences);
+
 void SetLocationForGlobal(JSObject* global, const nsACString& location);
 void SetLocationForGlobal(JSObject* global, nsIURI* locationURI);
 
 // ReportJSRuntimeExplicitTreeStats will expect this in the |extra| member
 // of JS::ZoneStats.
 class ZoneStatsExtras {
 public:
     ZoneStatsExtras() {}
--- a/js/xpconnect/tests/mochitest/mochitest.ini
+++ b/js/xpconnect/tests/mochitest/mochitest.ini
@@ -96,12 +96,13 @@ support-files =
 [test_bug1158558.html]
 [test_crossOriginObjects.html]
 [test_crosscompartment_weakmap.html]
 [test_frameWrapping.html]
 # The JS test component we use below is only available in debug builds.
 [test_getWebIDLCaller.html]
 skip-if = (debug == false || os == "android")
 [test_nac.xhtml]
+[test_nukeContentWindow.html]
 [test_sameOriginPolicy.html]
 [test_sandbox_fetch.html]
   support-files =
     ../../../../dom/tests/mochitest/fetch/test_fetch_basic.js
new file mode 100644
--- /dev/null
+++ b/js/xpconnect/tests/mochitest/test_nukeContentWindow.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1322273
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for Bug 1322273</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1322273">Mozilla Bug 1322273</a>
+
+<iframe id="subframe"></iframe>
+
+<script type="application/javascript">
+"use strict";
+
+add_task(function* () {
+  let frame = $('subframe');
+  frame.src = "data:text/html,";
+  yield new Promise(resolve => frame.addEventListener("load", resolve, {once: true}));
+
+  let win = frame.contentWindow;
+
+  win.eval("obj = {}");
+  win.obj.foo = {bar: "baz"};
+
+  let obj = win.obj;
+
+  let system = SpecialPowers.Services.scriptSecurityManager.getSystemPrincipal()
+  let sandbox = SpecialPowers.Cu.Sandbox(system);
+
+  sandbox.obj = obj;
+
+  let isWrapperDead = SpecialPowers.Cu.evalInSandbox(`(${
+      function isWrapperDead() {
+        return Components.utils.isDeadWrapper(obj);
+      }
+    })`,
+    sandbox);
+
+  is(isWrapperDead(), false, "Sandbox wrapper for content window should not be dead");
+  is(obj.foo.bar, "baz", "Content wrappers into and out of content window should be alive");
+
+  // Remove the frame, which should nuke the content window.
+  info("Remove the content frame");
+  frame.remove();
+
+  // Give the nuke wrappers task a chance to run.
+  yield new Promise(SimpleTest.executeSoon);
+
+  is(isWrapperDead(), true, "Sandbox wrapper for content window should be dead");
+  is(obj.foo.bar, "baz", "Content wrappers into and out of content window should be alive");
+});
+</script>
+
+</body>
+</html>