Bug 1431255 - Part III, Create per-origin sandboxes from XPCJSRuntime and load UA widgets scripts draft
authorTimothy Guan-tin Chien <timdream@gmail.com>
Wed, 27 Jun 2018 11:34:07 -0700
changeset 829408 30f77c9ed6ec3a88cd95602f8b7f31210a42b685
parent 829407 e5c11bffeab4cafaa02743a669e9db5da69ce3ab
child 829409 82f0506e69de1d1d3dcc1f20c10d1f372c012b77
push id118779
push usertimdream@gmail.com
push dateWed, 15 Aug 2018 23:50:47 +0000
bugs1431255
milestone63.0a1
Bug 1431255 - Part III, Create per-origin sandboxes from XPCJSRuntime and load UA widgets scripts 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;