Bug 1322273: Cut wrappers that point out of a browser compartment when nuking it. r?bholley draft
authorKris Maglione <maglione.k@gmail.com>
Thu, 23 Feb 2017 11:39:13 -0800
changeset 488797 69777629c4162cd276b4ba387209e2a1fad3dc2d
parent 487794 89120b07453e8df375aa38d9dcc12f271a9b0394
child 488798 46a2f4c2a23a56aaf7bbbc8f312ccbf374b5d60a
push id46645
push usermaglione.k@gmail.com
push dateThu, 23 Feb 2017 19:41:52 +0000
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/tests/mochitest/mochitest.ini
js/xpconnect/tests/mochitest/test_nukeContentWindow.html
--- a/dom/base/nsGlobalWindow.cpp
+++ b/dom/base/nsGlobalWindow.cpp
@@ -9408,28 +9408,30 @@ public:
         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);
+                                                                : js::NukeWindowReferences,
+                                           js::NukeAllReferences);
 
           // Now mark the compartment as nuked and non-scriptable.
           auto compartmentPrivate = xpc::CompartmentPrivate::Get(cpt);
           compartmentPrivate->wasNuked = true;
           compartmentPrivate->scriptability.Block();
         } 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
@@ -3017,17 +3017,18 @@ nsXPCComponents_Utils::NukeSandbox(Handl
     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);
+                                 NukeWindowReferences,
+                                 NukeAllReferences);
 
     // Now mark the compartment as nuked and non-scriptable.
     auto compartmentPrivate = xpc::CompartmentPrivate::Get(sb);
     compartmentPrivate->wasNuked = true;
     compartmentPrivate->scriptability.Block();
 
     return NS_OK;
 }
--- 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>