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
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;