Bug 1431255 - Part III, Create per-origin sandboxes from XPCJSRuntime and load UA widgets scripts r=bholley,jaws,sfink
authorTimothy Guan-tin Chien <timdream@gmail.com>
Wed, 27 Jun 2018 11:34:07 -0700
changeset 486753 a0200438265ba27626915031f3dd2f91f95969b6
parent 486752 cfecf5444baf6d15aca77461d9fc3daac59458e6
child 486754 4ddca5eb06c25aeeeae1f9776b81fe971a4f75cd
push id9719
push userffxbld-merge
push dateFri, 24 Aug 2018 17:49:46 +0000
treeherdermozilla-beta@719ec98fba77 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbholley, jaws, sfink
bugs1431255
milestone63.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 1431255 - Part III, Create per-origin sandboxes from XPCJSRuntime and load UA widgets scripts r=bholley,jaws,sfink This patch creates the basic structure on how the widget scripts can be loaded and be pointed to the Shadow Root, from the UAWidgetsChild.jsm. The UAWidgetsClass class asks for a sandbox from Cu.getUAWidgetScope(), which calls into XPCJSRuntime::GetUAWidgetScope(). It creates and keeps the sandboxes, in a GCHashMap keyed to the origin, so we could reuse it if needed. MozReview-Commit-ID: J6W4PDQWMcN
browser/actors/UAWidgetsChild.jsm
browser/actors/moz.build
browser/components/nsBrowserGlue.js
caps/BasePrincipal.h
js/xpconnect/idl/xpccomponents.idl
js/xpconnect/src/XPCComponents.cpp
js/xpconnect/src/XPCJSRuntime.cpp
js/xpconnect/src/xpcprivate.h
new file mode 100644
--- /dev/null
+++ b/browser/actors/UAWidgetsChild.jsm
@@ -0,0 +1,87 @@
+/* vim: set ts=2 sw=2 sts=2 et tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+var EXPORTED_SYMBOLS = ["UAWidgetsChild"];
+
+ChromeUtils.import("resource://gre/modules/ActorChild.jsm");
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+class UAWidgetsChild extends ActorChild {
+  constructor(mm) {
+    super(mm);
+
+    this.widgets = new WeakMap();
+  }
+
+  handleEvent(aEvent) {
+    switch (aEvent.type) {
+      case "UAWidgetBindToTree":
+      case "UAWidgetAttributeChanged":
+        this.setupOrNotifyWidget(aEvent.target);
+        break;
+      case "UAWidgetUnbindFromTree":
+        this.teardownWidget(aEvent.target);
+        break;
+    }
+  }
+
+  setupOrNotifyWidget(aElement) {
+    let widget = this.widgets.get(aElement);
+    if (!widget) {
+      this.setupWidget(aElement);
+      return;
+    }
+    if (typeof widget.wrappedJSObject.onattributechange == "function") {
+      widget.wrappedJSObject.onattributechange();
+    }
+  }
+
+  setupWidget(aElement) {
+    let uri;
+    let widgetName;
+    switch (aElement.localName) {
+      case "video":
+      case "audio":
+        uri = "chrome://global/content/elements/videocontrols.js";
+        widgetName = "VideoControlsPageWidget";
+        break;
+      case "input":
+        // TODO (datetimebox)
+        break;
+      case "applet":
+      case "embed":
+      case "object":
+        // TODO (pluginProblems)
+        break;
+    }
+
+    if (!uri || !widgetName) {
+      return;
+    }
+
+    let shadowRoot = aElement.openOrClosedShadowRoot;
+    let sandbox = aElement.nodePrincipal.isSystemPrincipal ?
+      Object.create(null) : Cu.getUAWidgetScope(aElement.nodePrincipal);
+
+    if (!sandbox[widgetName]) {
+      Services.scriptloader.loadSubScript(uri, sandbox, "UTF-8");
+    }
+
+    let widget = new sandbox[widgetName](shadowRoot);
+    this.widgets.set(aElement, widget);
+  }
+
+  teardownWidget(aElement) {
+    let widget = this.widgets.get(aElement);
+    if (!widget) {
+      return;
+    }
+    if (typeof widget.wrappedJSObject.destructor == "function") {
+      widget.wrappedJSObject.destructor();
+    }
+    this.widgets.delete(aElement);
+  }
+}
--- a/browser/actors/moz.build
+++ b/browser/actors/moz.build
@@ -32,11 +32,12 @@ FINAL_TARGET_FILES.actors += [
     'DOMFullscreenChild.jsm',
     'LightWeightThemeInstallChild.jsm',
     'NetErrorChild.jsm',
     'OfflineAppsChild.jsm',
     'PageInfoChild.jsm',
     'PageMetadataChild.jsm',
     'PageStyleChild.jsm',
     'PluginChild.jsm',
+    'UAWidgetsChild.jsm',
     'URIFixupChild.jsm',
     'WebRTCChild.jsm',
 ]
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -225,16 +225,28 @@ let ACTORS = {
       module: "resource://normandy-content/ShieldFrameChild.jsm",
       events: {
         "ShieldPageEvent": {wantUntrusted: true},
       },
       matches: ["about:studies"],
     },
   },
 
+  UAWidgets: {
+    child: {
+      module: "resource:///actors/UAWidgetsChild.jsm",
+      group: "browsers",
+      events: {
+        "UAWidgetBindToTree": {},
+        "UAWidgetAttributeChanged": {},
+        "UAWidgetUnbindFromTree": {}
+      },
+    }
+  },
+
   UITour: {
     child: {
       module: "resource:///modules/UITourChild.jsm",
       events: {
         "mozUITour": {wantUntrusted: true},
       },
       permissions: ["uitour"],
     },
--- a/caps/BasePrincipal.h
+++ b/caps/BasePrincipal.h
@@ -158,16 +158,18 @@ public:
     }
     // Extension principals always override the CSP non-extension principals.
     // This is primarily for the sake of their stylesheets, which are usually
     // loaded from channels and cannot have expanded principals.
     return (AddonPolicy() &&
             !BasePrincipal::Cast(aDocumentPrincipal)->AddonPolicy());
   }
 
+  uint32_t GetOriginNoSuffixHash() const { return mOriginNoSuffix->hash(); }
+
 protected:
   virtual ~BasePrincipal();
 
   // Note that this does not check OriginAttributes. Callers that depend on
   // those must call Subsumes instead.
   virtual bool SubsumesInternal(nsIPrincipal* aOther, DocumentDomainConsideration aConsider) = 0;
 
   // Internal, side-effect-free check to determine whether the concrete
--- a/js/xpconnect/idl/xpccomponents.idl
+++ b/js/xpconnect/idl/xpccomponents.idl
@@ -14,16 +14,17 @@ interface nsIClassInfo;
 interface nsIComponentManager;
 interface nsICycleCollectorListener;
 interface nsIFile;
 interface nsIURI;
 interface nsIJSCID;
 interface nsIJSIID;
 interface nsIPrincipal;
 interface nsIStackFrame;
+webidl Element;
 
 /**
 * interface of Components.interfacesByID
 * (interesting stuff only reflected into JavaScript)
 */
 [scriptable, uuid(f235ef76-9919-478b-aa0f-282d994ddf76)]
 interface nsIXPCComponents_InterfacesByID : nsISupports
 {
@@ -171,16 +172,23 @@ interface nsIXPCComponents_Utils : nsISu
      */
     [implicit_jscontext,optional_argc]
     jsval evalInSandbox(in AString source, in jsval sandbox,
                         [optional] in jsval version,
                         [optional] in AUTF8String filename,
                         [optional] in long lineNo);
 
     /*
+     * Get the sandbox for running JS-implemented UA widgets (video controls etc.),
+     * hosted inside UA-created Shadow DOM.
+     */
+    [implicit_jscontext]
+    jsval getUAWidgetScope(in nsIPrincipal principal);
+
+    /*
      * getSandboxMetadata is designed to be called from JavaScript only.
      *
      * getSandboxMetadata retrieves the metadata associated with
      * a sandbox object. It will return undefined if there
      * is no metadata attached to the sandbox.
      *
      * var s = C.u.Sandbox(..., { metadata: "metadata" });
      * var metadata = C.u.getSandboxMetadata(s);
--- a/js/xpconnect/src/XPCComponents.cpp
+++ b/js/xpconnect/src/XPCComponents.cpp
@@ -2172,16 +2172,31 @@ nsXPCComponents_Utils::EvalInSandbox(con
             lineNo = frame->GetLineNumber(cx);
         }
     }
 
     return xpc::EvalInSandbox(cx, sandbox, source, filename, lineNo, retval);
 }
 
 NS_IMETHODIMP
+nsXPCComponents_Utils::GetUAWidgetScope(nsIPrincipal* principal,
+                                        JSContext* cx,
+                                        MutableHandleValue rval)
+{
+    rval.set(UndefinedValue());
+
+    JSObject* scope = XPCJSRuntime::Get()->GetUAWidgetScope(cx, principal);
+    NS_ENSURE_TRUE(scope, NS_ERROR_OUT_OF_MEMORY); // See bug 858642.
+
+    rval.set(JS::ObjectValue(*scope));
+
+    return NS_OK;
+}
+
+NS_IMETHODIMP
 nsXPCComponents_Utils::GetSandboxMetadata(HandleValue sandboxVal,
                                           JSContext* cx, MutableHandleValue rval)
 {
     if (!sandboxVal.isObject())
         return NS_ERROR_INVALID_ARG;
 
     RootedObject sandbox(cx, &sandboxVal.toObject());
     sandbox = js::CheckedUnwrap(sandbox);
--- a/js/xpconnect/src/XPCJSRuntime.cpp
+++ b/js/xpconnect/src/XPCJSRuntime.cpp
@@ -893,16 +893,17 @@ XPCJSRuntime::FinalizeCallback(JSFreeOp*
 XPCJSRuntime::WeakPointerZonesCallback(JSContext* cx, void* data)
 {
     // Called before each sweeping slice -- after processing any final marking
     // triggered by barriers -- to clear out any references to things that are
     // about to be finalized and update any pointers to moved GC things.
     XPCJSRuntime* self = static_cast<XPCJSRuntime*>(data);
 
     self->mWrappedJSMap->UpdateWeakPointersAfterGC();
+    self->mUAWidgetScopeMap.sweep();
 
     XPCWrappedNativeScope::UpdateWeakPointersInAllScopesAfterGC();
 }
 
 /* static */ void
 XPCJSRuntime::WeakPointerCompartmentCallback(JSContext* cx, JS::Compartment* comp, void* data)
 {
     // Called immediately after the ZoneGroup weak pointer callback, but only
@@ -2822,16 +2823,17 @@ XPCJSRuntime::XPCJSRuntime(JSContext* aC
    mDyingWrappedNativeProtoMap(XPCWrappedNativeProtoMap::newMap(XPC_DYING_NATIVE_PROTO_MAP_LENGTH)),
    mGCIsRunning(false),
    mNativesToReleaseArray(),
    mDoingFinalization(false),
    mVariantRoots(nullptr),
    mWrappedJSRoots(nullptr),
    mAsyncSnowWhiteFreer(new AsyncFreeSnowWhite())
 {
+    MOZ_ALWAYS_TRUE(mUAWidgetScopeMap.init());
     MOZ_COUNT_CTOR_INHERITED(XPCJSRuntime, CycleCollectedJSRuntime);
 }
 
 /* static */
 XPCJSRuntime*
 XPCJSRuntime::Get()
 {
     return nsXPConnect::GetRuntimeInstance();
@@ -3134,16 +3136,53 @@ XPCJSRuntime::RemoveGCCallback(xpcGCCall
 {
     MOZ_ASSERT(cb, "null callback");
     bool found = extraGCCallbacks.RemoveElement(cb);
     if (!found) {
         NS_ERROR("Removing a callback which was never added.");
     }
 }
 
+JSObject*
+XPCJSRuntime::GetUAWidgetScope(JSContext* cx, nsIPrincipal* principal)
+{
+    MOZ_ASSERT(!nsContentUtils::IsSystemPrincipal(principal),
+        "Running UA Widget in chrome");
+
+    RefPtr<BasePrincipal> key = BasePrincipal::Cast(principal);
+    if (Principal2JSObjectMap::Ptr p = mUAWidgetScopeMap.lookup(key)) {
+        return p->value();
+    }
+
+    SandboxOptions options;
+    options.sandboxName.AssignLiteral("UA Widget Scope");
+    options.wantXrays = false;
+    options.wantComponents = false;
+
+    // Use an ExpandedPrincipal to create asymmetric security.
+    MOZ_ASSERT(!nsContentUtils::IsExpandedPrincipal(principal));
+    nsTArray<nsCOMPtr<nsIPrincipal>> principalAsArray(1);
+    principalAsArray.AppendElement(principal);
+    RefPtr<ExpandedPrincipal> ep =
+        ExpandedPrincipal::Create(principalAsArray,
+                                  principal->OriginAttributesRef());
+
+    // Create the sandbox.
+    RootedValue v(cx);
+    nsresult rv = CreateSandboxObject(cx, &v,
+                                      static_cast<nsIExpandedPrincipal*>(ep),
+                                      options);
+    NS_ENSURE_SUCCESS(rv, nullptr);
+    JSObject* scope = &v.toObject();
+
+    MOZ_ALWAYS_TRUE(mUAWidgetScopeMap.putNew(key, scope));
+
+    return scope;
+}
+
 void
 XPCJSRuntime::InitSingletonScopes()
 {
     // This all happens very early, so we don't bother with cx pushing.
     JSContext* cx = XPCJSContext::Get()->Context();
     JSAutoRequest ar(cx);
     RootedValue v(cx);
     nsresult rv;
--- a/js/xpconnect/src/xpcprivate.h
+++ b/js/xpconnect/src/xpcprivate.h
@@ -87,16 +87,18 @@
 #include "mozilla/dom/ScriptSettings.h"
 
 #include <math.h>
 #include <stdint.h>
 #include <stdlib.h>
 #include <string.h>
 
 #include "xpcpublic.h"
+#include "js/HashTable.h"
+#include "js/GCHashTable.h"
 #include "js/TracingAPI.h"
 #include "js/WeakMapPtr.h"
 #include "PLDHashTable.h"
 #include "nscore.h"
 #include "nsXPCOM.h"
 #include "nsAutoPtr.h"
 #include "nsCycleCollectionParticipant.h"
 #include "nsDebug.h"
@@ -567,16 +569,18 @@ public:
 
     bool GCIsRunning() const {return mGCIsRunning;}
 
     ~XPCJSRuntime();
 
     void AddGCCallback(xpcGCCallback cb);
     void RemoveGCCallback(xpcGCCallback cb);
 
+    JSObject* GetUAWidgetScope(JSContext* cx, nsIPrincipal* principal);
+
     size_t SizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf);
 
     JSObject* UnprivilegedJunkScope() { return mUnprivilegedJunkScope; }
     JSObject* LoaderGlobal();
 
     void InitSingletonScopes();
     void DeleteSingletonScopes();
 
@@ -590,21 +594,45 @@ private:
     void Shutdown(JSContext* cx) override;
 
     void ReleaseIncrementally(nsTArray<nsISupports*>& array);
 
     static const char* const mStrings[XPCJSContext::IDX_TOTAL_COUNT];
     jsid mStrIDs[XPCJSContext::IDX_TOTAL_COUNT];
     JS::Value mStrJSVals[XPCJSContext::IDX_TOTAL_COUNT];
 
+    struct Hasher {
+        typedef RefPtr<mozilla::BasePrincipal> Key;
+        typedef Key Lookup;
+        static uint32_t hash(const Lookup& l) {
+            return l->GetOriginNoSuffixHash();
+        }
+        static bool match(const Key& k, const Lookup& l) {
+            return k->FastEquals(l);
+        }
+    };
+
+    struct SweepPolicy {
+        static bool needsSweep(RefPtr<mozilla::BasePrincipal>* /* unused */, JS::Heap<JSObject*>* value) {
+            return JS::GCPolicy<JS::Heap<JSObject*>>::needsSweep(value);
+        }
+    };
+
+    typedef JS::GCHashMap<RefPtr<mozilla::BasePrincipal>,
+                          JS::Heap<JSObject*>,
+                          Hasher,
+                          js::SystemAllocPolicy,
+                          SweepPolicy> Principal2JSObjectMap;
+
     JSObject2WrappedJSMap*   mWrappedJSMap;
     IID2WrappedJSClassMap*   mWrappedJSClassMap;
     IID2NativeInterfaceMap*  mIID2NativeInterfaceMap;
     ClassInfo2NativeSetMap*  mClassInfo2NativeSetMap;
     NativeSetMap*            mNativeSetMap;
+    Principal2JSObjectMap    mUAWidgetScopeMap;
     XPCWrappedNativeProtoMap* mDyingWrappedNativeProtoMap;
     bool mGCIsRunning;
     nsTArray<nsISupports*> mNativesToReleaseArray;
     bool mDoingFinalization;
     XPCRootSetElem* mVariantRoots;
     XPCRootSetElem* mWrappedJSRoots;
     nsTArray<xpcGCCallback> extraGCCallbacks;
     JS::GCSliceCallback mPrevGCSliceCallback;