Bug 1431255 - Part V, Set the reflectors of the UA Widget DOM to UA Widget Scope draft
authorTimothy Guan-tin Chien <timdream@gmail.com>
Fri, 29 Jun 2018 13:39:46 -0700
changeset 829410 331b637785390c0b82582685bc635aae1e3a60fb
parent 829409 82f0506e69de1d1d3dcc1f20c10d1f372c012b77
child 829411 f6add5c8ca965242271523e7188f0e4e7cf15704
push id118779
push usertimdream@gmail.com
push dateWed, 15 Aug 2018 23:50:47 +0000
bugs1431255
milestone63.0a1
Bug 1431255 - Part V, Set the reflectors of the UA Widget DOM to UA Widget Scope The DOM elements within the UA Widget Shadow DOM should have its reflectors in the UA Widget Scope. This is done by calling nsINode::IsInUAWidget() which would check its containing shadow and its UA Widget bit. To prevent JS access of the DOM element before it is in the UA Widget Shadom DOM tree, various DOM methods are set to inaccessible to UA Widget script. It would need to use the two special methods in ShadowRoot instead to insert the DOM directly into the shadow tree. MozReview-Commit-ID: Jz9iCaVIoij
dom/base/ShadowRoot.cpp
dom/base/ShadowRoot.h
dom/base/nsIDocument.h
dom/base/nsINode.cpp
dom/base/nsINode.h
dom/bindings/BindingDeclarations.h
dom/bindings/BindingUtils.h
dom/html/HTMLMediaElement.cpp
dom/webidl/Document.webidl
dom/webidl/EventTarget.webidl
dom/webidl/HTMLVideoElement.webidl
dom/webidl/Node.webidl
dom/webidl/ShadowRoot.webidl
js/xpconnect/src/Sandbox.cpp
js/xpconnect/src/XPCComponents.cpp
js/xpconnect/src/XPCJSRuntime.cpp
js/xpconnect/src/XPCWrappedNativeScope.cpp
js/xpconnect/src/nsXPConnect.cpp
js/xpconnect/src/xpcprivate.h
js/xpconnect/src/xpcpublic.h
layout/generic/nsVideoFrame.cpp
toolkit/content/tests/widgets/mochitest.ini
toolkit/content/tests/widgets/test_ua_widget.html
toolkit/content/widgets/videocontrols.js
--- a/dom/base/ShadowRoot.cpp
+++ b/dom/base/ShadowRoot.cpp
@@ -59,16 +59,17 @@ NS_IMPL_RELEASE_INHERITED(ShadowRoot, Do
 
 ShadowRoot::ShadowRoot(Element* aElement, ShadowRootMode aMode,
                        already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
   : DocumentFragment(aNodeInfo)
   , DocumentOrShadowRoot(*this)
   , mMode(aMode)
   , mServoStyles(Servo_AuthorStyles_Create())
   , mIsComposedDocParticipant(false)
+  , mIsUAWidget(false)
 {
   SetHost(aElement);
 
   // Nodes in a shadow tree should never store a value
   // in the subtree root pointer, nodes in the shadow tree
   // track the subtree root using GetContainingShadow().
   ClearSubtreeRootPointer();
 
@@ -114,16 +115,22 @@ ShadowRoot::SetIsComposedDocParticipant(
   nsIDocument* doc = OwnerDoc();
   if (IsComposedDocParticipant()) {
     doc->AddComposedDocShadowRoot(*this);
   } else {
     doc->RemoveComposedDocShadowRoot(*this);
   }
 }
 
+void
+ShadowRoot::SetIsUAWidget(bool aIsUAWidget)
+{
+  mIsUAWidget = aIsUAWidget;
+}
+
 JSObject*
 ShadowRoot::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
 {
   return mozilla::dom::ShadowRoot_Binding::Wrap(aCx, this, aGivenProto);
 }
 
 void
 ShadowRoot::CloneInternalDataFrom(ShadowRoot* aOther)
@@ -544,16 +551,61 @@ ShadowRoot::GetInnerHTML(nsAString& aInn
 }
 
 void
 ShadowRoot::SetInnerHTML(const nsAString& aInnerHTML, ErrorResult& aError)
 {
   SetInnerHTMLInternal(aInnerHTML, aError);
 }
 
+nsINode*
+ShadowRoot::ImportNodeAndAppendChildAt(nsINode& aParentNode,
+                                       nsINode& aNode,
+                                       bool aDeep,
+                                       mozilla::ErrorResult& rv)
+{
+  MOZ_ASSERT(mIsUAWidget);
+  MOZ_ASSERT(OwnerDoc());
+
+  if (!aParentNode.IsInUAWidget()) {
+    rv.Throw(NS_ERROR_INVALID_ARG);
+    return nullptr;
+  }
+
+  RefPtr<nsINode> node = OwnerDoc()->ImportNode(aNode, aDeep, rv);
+  if (rv.Failed()) {
+    return nullptr;
+  }
+
+  return aParentNode.AppendChild(*node, rv);
+}
+
+nsINode*
+ShadowRoot::CreateElementAndAppendChildAt(nsINode& aParentNode,
+                                          const nsAString& aTagName,
+                                          mozilla::ErrorResult& rv) {
+  MOZ_ASSERT(mIsUAWidget);
+  MOZ_ASSERT(OwnerDoc());
+
+  if (!aParentNode.IsInUAWidget()) {
+    rv.Throw(NS_ERROR_INVALID_ARG);
+    return nullptr;
+  }
+
+  // This option is not exposed to UA Widgets
+  ElementCreationOptionsOrString options;
+
+  RefPtr<nsINode> node = OwnerDoc()->CreateElement(aTagName, options, rv);
+  if (rv.Failed()) {
+    return nullptr;
+  }
+
+  return aParentNode.AppendChild(*node, rv);
+}
+
 void
 ShadowRoot::AttributeChanged(Element* aElement,
                              int32_t aNameSpaceID,
                              nsAtom* aAttribute,
                              int32_t aModType,
                              const nsAttrValue* aOldValue)
 {
   if (aNameSpaceID != kNameSpaceID_None || aAttribute != nsGkAtoms::slot) {
--- a/dom/base/ShadowRoot.h
+++ b/dom/base/ShadowRoot.h
@@ -2,16 +2,17 @@
 /* vim: set ts=8 sts=2 et sw=2 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/. */
 
 #ifndef mozilla_dom_shadowroot_h__
 #define mozilla_dom_shadowroot_h__
 
+#include "mozilla/dom/DocumentBinding.h"
 #include "mozilla/dom/DocumentFragment.h"
 #include "mozilla/dom/DocumentOrShadowRoot.h"
 #include "mozilla/dom/NameSpaceConstants.h"
 #include "mozilla/dom/ShadowRootBinding.h"
 #include "mozilla/ServoBindings.h"
 #include "nsCOMPtr.h"
 #include "nsCycleCollectionParticipant.h"
 #include "nsIdentifierMapEntry.h"
@@ -166,23 +167,43 @@ public:
 
   // WebIDL methods.
   using mozilla::dom::DocumentOrShadowRoot::GetElementById;
 
   Element* GetActiveElement();
   void GetInnerHTML(nsAString& aInnerHTML);
   void SetInnerHTML(const nsAString& aInnerHTML, ErrorResult& aError);
 
+  /**
+   * These methods allow UA Widget to insert DOM elements into the Shadow ROM
+   * without putting their DOM reflectors to content scope first.
+   * The inserted DOM will have their reflectors in the UA Widget scope.
+   */
+  nsINode* ImportNodeAndAppendChildAt(nsINode& aParentNode,
+                                      nsINode& aNode,
+                                      bool aDeep, mozilla::ErrorResult& rv);
+
+  nsINode* CreateElementAndAppendChildAt(nsINode& aParentNode,
+                                         const nsAString& aTagName,
+                                         mozilla::ErrorResult& rv);
+
   bool IsComposedDocParticipant() const
   {
     return mIsComposedDocParticipant;
   }
 
   void SetIsComposedDocParticipant(bool aIsComposedDocParticipant);
 
+  bool IsUAWidget() const
+  {
+    return mIsUAWidget;
+  }
+
+  void SetIsUAWidget(bool aIsUAWidget);
+
   void GetEventTargetParent(EventChainPreVisitor& aVisitor) override;
 
 protected:
   // FIXME(emilio): This will need to become more fine-grained.
   void ApplicableRulesChanged();
 
   virtual ~ShadowRoot();
 
@@ -199,15 +220,17 @@ protected:
   nsClassHashtable<nsStringHashKey, SlotArray> mSlotMap;
 
   // Flag to indicate whether the descendants of this shadow root are part of the
   // composed document. Ideally, we would use a node flag on nodes to
   // mark whether it is in the composed document, but we have run out of flags
   // so instead we track it here.
   bool mIsComposedDocParticipant;
 
+  bool mIsUAWidget;
+
   nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
 };
 
 } // namespace dom
 } // namespace mozilla
 
 #endif // mozilla_dom_shadowroot_h__
--- a/dom/base/nsIDocument.h
+++ b/dom/base/nsIDocument.h
@@ -4659,23 +4659,33 @@ nsINode::OwnerDocAsNode() const
 // no really good way to include it.
 template<typename T>
 inline bool ShouldUseXBLScope(const T* aNode)
 {
   // TODO(emilio): Is the <svg:use> shadow tree check needed now?
   return aNode->IsInAnonymousSubtree() && !aNode->IsInSVGUseShadowTree();
 }
 
+template<typename T>
+inline bool ShouldUseUAWidgetScope(const T* aNode)
+{
+  return aNode->IsInUAWidget();
+}
+
 inline mozilla::dom::ParentObject
 nsINode::GetParentObject() const
 {
   mozilla::dom::ParentObject p(OwnerDoc());
-    // Note that mUseXBLScope is a no-op for chrome, and other places where we
-    // don't use XBL scopes.
-  p.mUseXBLScope = ShouldUseXBLScope(this);
+    // Note that mReflectionScope is a no-op for chrome, and other places
+    // where we don't check this value.
+  if (ShouldUseXBLScope(this)) {
+    p.mReflectionScope = mozilla::dom::ReflectionScope::XBL;
+  } else if (ShouldUseUAWidgetScope(this)) {
+    p.mReflectionScope = mozilla::dom::ReflectionScope::UAWidget;
+  }
   return p;
 }
 
 inline nsIDocument*
 nsINode::AsDocument()
 {
   MOZ_ASSERT(IsDocument());
   return static_cast<nsIDocument*>(this);
--- a/dom/base/nsINode.cpp
+++ b/dom/base/nsINode.cpp
@@ -541,16 +541,26 @@ operator<<(std::ostream& aStream, const 
 
 SVGUseElement*
 nsINode::DoGetContainingSVGUseShadowHost() const
 {
   MOZ_ASSERT(IsInShadowTree());
   return SVGUseElement::FromNodeOrNull(AsContent()->GetContainingShadowHost());
 }
 
+bool
+nsINode::IsInUAWidget() const
+{
+  if (!IsInShadowTree()) {
+    return false;
+  }
+  ShadowRoot* shadowRoot = AsContent()->GetContainingShadow();
+  return shadowRoot && shadowRoot->IsUAWidget();
+}
+
 void
 nsINode::GetNodeValueInternal(nsAString& aNodeValue)
 {
   SetDOMStringToNull(aNodeValue);
 }
 
 nsINode*
 nsINode::RemoveChild(nsINode& aOldChild, ErrorResult& aError)
--- a/dom/base/nsINode.h
+++ b/dom/base/nsINode.h
@@ -1236,16 +1236,18 @@ public:
   mozilla::dom::SVGUseElement* GetContainingSVGUseShadowHost() const
   {
     if (!IsInShadowTree()) {
       return nullptr;
     }
     return DoGetContainingSVGUseShadowHost();
   }
 
+  bool IsInUAWidget() const;
+
   // True for native anonymous content and for XBL content if the binding
   // has chromeOnlyContent="true".
   bool ChromeOnlyAccess() const
   {
     return HasFlag(NODE_IS_IN_NATIVE_ANONYMOUS_SUBTREE | NODE_CHROME_ONLY_ACCESS);
   }
 
   bool IsInShadowTree() const
--- a/dom/bindings/BindingDeclarations.h
+++ b/dom/bindings/BindingDeclarations.h
@@ -516,42 +516,48 @@ GetWrapperCache(void* p)
 // GetWrappeCache(void*) and GetWrapperCache(const ParentObject&).
 template <template <typename> class SmartPtr, typename T>
 inline nsWrapperCache*
 GetWrapperCache(const SmartPtr<T>& aObject)
 {
   return GetWrapperCache(aObject.get());
 }
 
+enum class ReflectionScope {
+  Content,
+  XBL,
+  UAWidget
+};
+
 struct MOZ_STACK_CLASS ParentObject {
   template<class T>
   MOZ_IMPLICIT ParentObject(T* aObject) :
     mObject(aObject),
     mWrapperCache(GetWrapperCache(aObject)),
-    mUseXBLScope(false)
+    mReflectionScope(ReflectionScope::Content)
   {}
 
   template<class T, template<typename> class SmartPtr>
   MOZ_IMPLICIT ParentObject(const SmartPtr<T>& aObject) :
     mObject(aObject.get()),
     mWrapperCache(GetWrapperCache(aObject.get())),
-    mUseXBLScope(false)
+    mReflectionScope(ReflectionScope::Content)
   {}
 
   ParentObject(nsISupports* aObject, nsWrapperCache* aCache) :
     mObject(aObject),
     mWrapperCache(aCache),
-    mUseXBLScope(false)
+    mReflectionScope(ReflectionScope::Content)
   {}
 
   // We don't want to make this an nsCOMPtr because of performance reasons, but
   // it's safe because ParentObject is a stack class.
   nsISupports* const MOZ_NON_OWNING_REF mObject;
   nsWrapperCache* const mWrapperCache;
-  bool mUseXBLScope;
+  ReflectionScope mReflectionScope;
 };
 
 namespace binding_detail {
 
 // Class for simple sequence arguments, only used internally by codegen.
 template<typename T>
 class AutoSequence : public AutoTArray<T, 16>
 {
--- a/dom/bindings/BindingUtils.h
+++ b/dom/bindings/BindingUtils.h
@@ -1399,26 +1399,26 @@ GetParentPointer(T* aObject)
 
 inline nsISupports*
 GetParentPointer(const ParentObject& aObject)
 {
   return aObject.mObject;
 }
 
 template <typename T>
-inline bool
-GetUseXBLScope(T* aParentObject)
+inline mozilla::dom::ReflectionScope
+GetReflectionScope(T* aParentObject)
 {
-  return false;
+  return mozilla::dom::ReflectionScope::Content;
 }
 
-inline bool
-GetUseXBLScope(const ParentObject& aParentObject)
+inline mozilla::dom::ReflectionScope
+GetReflectionScope(const ParentObject& aParentObject)
 {
-  return aParentObject.mUseXBLScope;
+  return aParentObject.mReflectionScope;
 }
 
 template<class T>
 inline void
 ClearWrapper(T* p, nsWrapperCache* cache, JSObject* obj)
 {
   JS::AutoAssertGCCallback inCallback;
 
@@ -1662,56 +1662,76 @@ struct WrapNativeHelper<T, false>
     return obj;
   }
 };
 
 // Finding the associated global for an object.
 template<typename T>
 static inline JSObject*
 FindAssociatedGlobal(JSContext* cx, T* p, nsWrapperCache* cache,
-                     bool useXBLScope = false)
+                     mozilla::dom::ReflectionScope scope = mozilla::dom::ReflectionScope::Content)
 {
   if (!p) {
     return JS::CurrentGlobalOrNull(cx);
   }
 
   JSObject* obj = WrapNativeHelper<T>::Wrap(cx, p, cache);
   if (!obj) {
     return nullptr;
   }
   MOZ_ASSERT(JS::ObjectIsNotGray(obj));
 
   // The object is never a CCW but it may not be in the current compartment of
   // the JSContext.
   obj = JS::GetNonCCWObjectGlobal(obj);
 
-  if (!useXBLScope) {
-    return obj;
+  switch (scope) {
+    case mozilla::dom::ReflectionScope::XBL: {
+      // If scope is set to XBLScope, it means that the canonical reflector for this
+      // native object should live in the content XBL scope. Note that we never put
+      // anonymous content inside an add-on scope.
+      if (xpc::IsInContentXBLScope(obj)) {
+        return obj;
+      }
+      JS::Rooted<JSObject*> rootedObj(cx, obj);
+      JSObject* xblScope = xpc::GetXBLScope(cx, rootedObj);
+      MOZ_ASSERT_IF(xblScope, JS_IsGlobalObject(xblScope));
+      MOZ_ASSERT(JS::ObjectIsNotGray(xblScope));
+      return xblScope;
+    }
+
+    case mozilla::dom::ReflectionScope::UAWidget: {
+      // If scope is set to UAWidgetScope, it means that the canonical reflector
+      // for this native object should live in the UA widget scope.
+      if (xpc::IsInUAWidgetScope(obj)) {
+        return obj;
+      }
+      JS::Rooted<JSObject*> rootedObj(cx, obj);
+      JSObject* uaWidgetScope = xpc::GetUAWidgetScope(cx, rootedObj);
+      MOZ_ASSERT_IF(uaWidgetScope, JS_IsGlobalObject(uaWidgetScope));
+      MOZ_ASSERT(JS::ObjectIsNotGray(uaWidgetScope));
+      return uaWidgetScope;
+    }
+
+    case ReflectionScope::Content:
+      return obj;
   }
 
-  // If useXBLScope is true, it means that the canonical reflector for this
-  // native object should live in the content XBL scope. Note that we never put
-  // anonymous content inside an add-on scope.
-  if (xpc::IsInContentXBLScope(obj)) {
-    return obj;
-  }
-  JS::Rooted<JSObject*> rootedObj(cx, obj);
-  JSObject* xblScope = xpc::GetXBLScope(cx, rootedObj);
-  MOZ_ASSERT_IF(xblScope, JS_IsGlobalObject(xblScope));
-  MOZ_ASSERT(JS::ObjectIsNotGray(xblScope));
-  return xblScope;
+  MOZ_CRASH("Unknown ReflectionScope variant");
+
+  return nullptr;
 }
 
 // Finding of the associated global for an object, when we don't want to
 // explicitly pass in things like the nsWrapperCache for it.
 template<typename T>
 static inline JSObject*
 FindAssociatedGlobal(JSContext* cx, const T& p)
 {
-  return FindAssociatedGlobal(cx, GetParentPointer(p), GetWrapperCache(p), GetUseXBLScope(p));
+  return FindAssociatedGlobal(cx, GetParentPointer(p), GetWrapperCache(p), GetReflectionScope(p));
 }
 
 // Specialization for the case of nsIGlobalObject, since in that case
 // we can just get the JSObject* directly.
 template<>
 inline JSObject*
 FindAssociatedGlobal(JSContext* cx, nsIGlobalObject* const& p)
 {
--- a/dom/html/HTMLMediaElement.cpp
+++ b/dom/html/HTMLMediaElement.cpp
@@ -4936,21 +4936,24 @@ HTMLMediaElement::AssertReadyStateIsNoth
   }
 #endif
 }
 
 void
 HTMLMediaElement::AttachAndSetUAShadowRoot()
 {
   if (GetShadowRoot()) {
+    MOZ_ASSERT(GetShadowRoot()->IsUAWidget());
     return;
   }
 
   // Add a closed shadow root to host video controls
-  AttachShadowWithoutNameChecks(ShadowRootMode::Closed);
+  RefPtr<ShadowRoot> shadowRoot =
+    AttachShadowWithoutNameChecks(ShadowRootMode::Closed);
+  shadowRoot->SetIsUAWidget(true);
 }
 
 nsresult
 HTMLMediaElement::InitializeDecoderAsClone(ChannelMediaDecoder* aOriginal)
 {
   NS_ASSERTION(mLoadingSrc, "mLoadingSrc must already be set");
   NS_ASSERTION(mDecoder == nullptr, "Shouldn't have a decoder");
   AssertReadyStateIsNothing();
--- a/dom/webidl/Document.webidl
+++ b/dom/webidl/Document.webidl
@@ -58,32 +58,35 @@ interface Document : Node {
   HTMLCollection getElementsByTagName(DOMString localName);
   [Pure, Throws]
   HTMLCollection getElementsByTagNameNS(DOMString? namespace, DOMString localName);
   [Pure]
   HTMLCollection getElementsByClassName(DOMString classNames);
   [Pure]
   Element? getElementById(DOMString elementId);
 
-  [CEReactions, NewObject, Throws]
+  // These DOM methods cannot be accessed by UA Widget scripts
+  // because the DOM element reflectors will be in the content scope,
+  // instead of the desired UA Widget scope.
+  [CEReactions, NewObject, Throws, Func="IsNotUAWidget"]
   Element createElement(DOMString localName, optional (ElementCreationOptions or DOMString) options);
-  [CEReactions, NewObject, Throws]
+  [CEReactions, NewObject, Throws, Func="IsNotUAWidget"]
   Element createElementNS(DOMString? namespace, DOMString qualifiedName, optional (ElementCreationOptions or DOMString) options);
   [NewObject]
   DocumentFragment createDocumentFragment();
-  [NewObject]
+  [NewObject, Func="IsNotUAWidget"]
   Text createTextNode(DOMString data);
-  [NewObject]
+  [NewObject, Func="IsNotUAWidget"]
   Comment createComment(DOMString data);
   [NewObject, Throws]
   ProcessingInstruction createProcessingInstruction(DOMString target, DOMString data);
 
-  [CEReactions, Throws]
+  [CEReactions, Throws, Func="IsNotUAWidget"]
   Node importNode(Node node, optional boolean deep = false);
-  [CEReactions, Throws]
+  [CEReactions, Throws, Func="IsNotUAWidget"]
   Node adoptNode(Node node);
 
   [NewObject, Throws, NeedsCallerType]
   Event createEvent(DOMString interface);
 
   [NewObject, Throws]
   Range createRange();
 
@@ -169,17 +172,17 @@ partial interface Document {
 
                 [Pref="dom.select_events.enabled"]
                 attribute EventHandler onselectionchange;
 
   /**
    * True if this document is synthetic : stand alone image, video, audio file,
    * etc.
    */
-  [Func="IsChromeOrXBL"] readonly attribute boolean mozSyntheticDocument;
+  [Func="IsChromeOrXBLOrUAWidget"] readonly attribute boolean mozSyntheticDocument;
   [Throws, Func="IsChromeOrXBL"]
   BoxObject? getBoxObjectFor(Element? element);
   /**
    * Returns the script element whose script is currently being processed.
    *
    * @see <https://developer.mozilla.org/en/DOM/document.currentScript>
    */
   [Pure]
--- a/dom/webidl/EventTarget.webidl
+++ b/dom/webidl/EventTarget.webidl
@@ -9,17 +9,17 @@
  * Copyright © 2012 W3C® (MIT, ERCIM, Keio), All Rights Reserved. W3C
  * liability, trademark and document use rules apply.
  */
 
 
 dictionary EventListenerOptions {
   boolean capture = false;
   /* Setting to true make the listener be added to the system group. */
-  [Func="ThreadSafeIsChromeOrXBL"]
+  [Func="ThreadSafeIsChromeOrXBLOrUAWidget"]
   boolean mozSystemGroup = false;
 };
 
 dictionary AddEventListenerOptions : EventListenerOptions {
   boolean passive;
   boolean once = false;
   [ChromeOnly]
   boolean wantUntrusted;
--- a/dom/webidl/HTMLVideoElement.webidl
+++ b/dom/webidl/HTMLVideoElement.webidl
@@ -43,20 +43,20 @@ partial interface HTMLVideoElement {
   // Time which the last painted video frame was late by, in seconds.
   readonly attribute double mozFrameDelay;
 
   // True if the video has an audio track available.
   readonly attribute boolean mozHasAudio;
 
   // Attributes for builtin video controls to lock screen orientation.
   // True if video controls should lock orientation when fullscreen.
-  [Pref="media.videocontrols.lock-video-orientation", Func="IsChromeOrXBL"]
+  [Pref="media.videocontrols.lock-video-orientation", Func="IsChromeOrXBLOrUAWidget"]
     readonly attribute boolean mozOrientationLockEnabled;
   // True if screen orientation is locked by video controls.
-  [Pref="media.videocontrols.lock-video-orientation", Func="IsChromeOrXBL"]
+  [Pref="media.videocontrols.lock-video-orientation", Func="IsChromeOrXBLOrUAWidget"]
     attribute boolean mozIsOrientationLocked;
 };
 
 // https://dvcs.w3.org/hg/html-media/raw-file/default/media-source/media-source.html#idl-def-HTMLVideoElement
 partial interface HTMLVideoElement {
   [Func="mozilla::dom::MediaSource::Enabled", NewObject]
   VideoPlaybackQuality getVideoPlaybackQuality();
 };
--- a/dom/webidl/Node.webidl
+++ b/dom/webidl/Node.webidl
@@ -57,28 +57,31 @@ interface Node : EventTarget {
   [Pure]
   readonly attribute Node? nextSibling;
 
   [CEReactions, SetterThrows, Pure]
            attribute DOMString? nodeValue;
   [CEReactions, SetterThrows, GetterCanOOM,
    SetterNeedsSubjectPrincipal=NonSystem, Pure]
            attribute DOMString? textContent;
-  [CEReactions, Throws]
+  // These DOM methods cannot be accessed by UA Widget scripts
+  // because the DOM element reflectors will be in the content scope,
+  // instead of the desired UA Widget scope.
+  [CEReactions, Throws, Func="IsNotUAWidget"]
   Node insertBefore(Node node, Node? child);
-  [CEReactions, Throws]
+  [CEReactions, Throws, Func="IsNotUAWidget"]
   Node appendChild(Node node);
-  [CEReactions, Throws]
+  [CEReactions, Throws, Func="IsNotUAWidget"]
   Node replaceChild(Node node, Node child);
   [CEReactions, Throws]
   Node removeChild(Node child);
   [CEReactions]
   void normalize();
 
-  [CEReactions, Throws]
+  [CEReactions, Throws, Func="IsNotUAWidget"]
   Node cloneNode(optional boolean deep = false);
   [Pure]
   boolean isSameNode(Node? node);
   [Pure]
   boolean isEqualNode(Node? node);
 
   const unsigned short DOCUMENT_POSITION_DISCONNECTED = 0x01;
   const unsigned short DOCUMENT_POSITION_PRECEDING = 0x02;
--- a/dom/webidl/ShadowRoot.webidl
+++ b/dom/webidl/ShadowRoot.webidl
@@ -26,11 +26,28 @@ interface ShadowRoot : DocumentFragment
 
   // [deprecated] Shadow DOM v0
   Element? getElementById(DOMString elementId);
   HTMLCollection getElementsByTagName(DOMString localName);
   HTMLCollection getElementsByTagNameNS(DOMString? namespace, DOMString localName);
   HTMLCollection getElementsByClassName(DOMString classNames);
   [CEReactions, SetterThrows, TreatNullAs=EmptyString]
   attribute DOMString innerHTML;
+
+  // When JS invokes importNode or createElement, the binding code needs to
+  // create a reflector, and so invoking those methods directly on the content
+  // document would cause the reflector to be created in the content scope,
+  // at which point it would be difficult to move into the UA Widget scope.
+  // As such, these methods allow UA widget code to simultaneously create nodes
+  // and associate them with the UA widget tree, so that the reflectors get
+  // created in the right scope.
+  [CEReactions, Throws, Func="IsChromeOrXBLOrUAWidget"]
+  Node importNodeAndAppendChildAt(Node parentNode, Node node, optional boolean deep = false);
+
+  [CEReactions, Throws, Func="IsChromeOrXBLOrUAWidget"]
+  Node createElementAndAppendChildAt(Node parentNode, DOMString localName);
+
+  // For triggering UA Widget scope in tests.
+  [ChromeOnly]
+  void setIsUAWidget(boolean isUAWidget);
 };
 
 ShadowRoot implements DocumentOrShadowRoot;
--- a/js/xpconnect/src/Sandbox.cpp
+++ b/js/xpconnect/src/Sandbox.cpp
@@ -1107,16 +1107,17 @@ xpc::CreateSandboxObject(JSContext* cx, 
                                                      principal, realmOptions));
     if (!sandbox)
         return NS_ERROR_FAILURE;
 
     CompartmentPrivate* priv = CompartmentPrivate::Get(sandbox);
     priv->allowWaivers = options.allowWaivers;
     priv->isWebExtensionContentScript = options.isWebExtensionContentScript;
     priv->isContentXBLCompartment = options.isContentXBLScope;
+    priv->isUAWidgetCompartment = options.isUAWidgetScope;
     priv->isSandboxCompartment = true;
 
     // Set up the wantXrays flag, which indicates whether xrays are desired even
     // for same-origin access.
     //
     // This flag has historically been ignored for chrome sandboxes due to
     // quirks in the wrapping implementation that have now been removed. Indeed,
     // same-origin Xrays for chrome->chrome access seems a bit superfluous.
--- a/js/xpconnect/src/XPCComponents.cpp
+++ b/js/xpconnect/src/XPCComponents.cpp
@@ -2178,18 +2178,17 @@ nsXPCComponents_Utils::EvalInSandbox(con
 
 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.
+    JSObject* scope = xpc::GetUAWidgetScope(cx, principal);
 
     rval.set(JS::ObjectValue(*scope));
 
     return NS_OK;
 }
 
 NS_IMETHODIMP
 nsXPCComponents_Utils::GetSandboxMetadata(HandleValue sandboxVal,
--- a/js/xpconnect/src/XPCJSRuntime.cpp
+++ b/js/xpconnect/src/XPCJSRuntime.cpp
@@ -179,17 +179,19 @@ public:
 namespace xpc {
 
 CompartmentPrivate::CompartmentPrivate(JS::Compartment* c)
     : wantXrays(false)
     , allowWaivers(true)
     , isWebExtensionContentScript(false)
     , allowCPOWs(false)
     , isContentXBLCompartment(false)
+    , isUAWidgetCompartment(false)
     , isSandboxCompartment(false)
+    , isAddonCompartment(false)
     , universalXPConnectEnabled(false)
     , forcePermissiveCOWs(false)
     , wasNuked(false)
     , mWrappedJSMap(JSObject2WrappedJSMap::newMap(XPC_JS_MAP_LENGTH))
 {
     MOZ_COUNT_CTOR(xpc::CompartmentPrivate);
     mozilla::PodArrayZero(wrapperDenialWarnings);
 }
@@ -464,16 +466,36 @@ IsContentXBLScope(JS::Realm* realm)
 
 bool
 IsInContentXBLScope(JSObject* obj)
 {
     return IsContentXBLCompartment(js::GetObjectCompartment(obj));
 }
 
 bool
+IsUAWidgetCompartment(JS::Compartment* compartment)
+{
+    // We always eagerly create compartment privates for UA Widget compartments.
+    CompartmentPrivate* priv = CompartmentPrivate::Get(compartment);
+    return priv && priv->isUAWidgetCompartment;
+}
+
+bool
+IsUAWidgetScope(JS::Realm* realm)
+{
+    return IsUAWidgetCompartment(JS::GetCompartmentForRealm(realm));
+}
+
+bool
+IsInUAWidgetScope(JSObject* obj)
+{
+    return IsUAWidgetCompartment(js::GetObjectCompartment(obj));
+}
+
+bool
 IsInSandboxCompartment(JSObject* obj)
 {
     JS::Compartment* comp = js::GetObjectCompartment(obj);
 
     // We always eagerly create compartment privates for sandbox compartments.
     CompartmentPrivate* priv = CompartmentPrivate::Get(comp);
     return priv && priv->isSandboxCompartment;
 }
@@ -2823,17 +2845,16 @@ 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();
@@ -3151,16 +3172,17 @@ XPCJSRuntime::GetUAWidgetScope(JSContext
     if (Principal2JSObjectMap::Ptr p = mUAWidgetScopeMap.lookup(key)) {
         return p->value();
     }
 
     SandboxOptions options;
     options.sandboxName.AssignLiteral("UA Widget Scope");
     options.wantXrays = false;
     options.wantComponents = false;
+    options.isUAWidgetScope = true;
 
     // 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());
@@ -3168,16 +3190,18 @@ XPCJSRuntime::GetUAWidgetScope(JSContext
     // 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_ASSERT(xpc::IsInUAWidgetScope(js::UncheckedUnwrap(scope)));
+
     MOZ_ALWAYS_TRUE(mUAWidgetScopeMap.putNew(key, scope));
 
     return scope;
 }
 
 void
 XPCJSRuntime::InitSingletonScopes()
 {
--- a/js/xpconnect/src/XPCWrappedNativeScope.cpp
+++ b/js/xpconnect/src/XPCWrappedNativeScope.cpp
@@ -297,16 +297,42 @@ GetXBLScope(JSContext* cx, JSObject* con
     RootedObject scope(cx, nativeScope->EnsureContentXBLScope(cx));
     NS_ENSURE_TRUE(scope, nullptr); // See bug 858642.
 
     scope = js::UncheckedUnwrap(scope);
     JS::ExposeObjectToActiveJS(scope);
     return scope;
 }
 
+JSObject*
+GetUAWidgetScope(JSContext* cx, JSObject* contentScopeArg)
+{
+    JS::RootedObject contentScope(cx, contentScopeArg);
+    JSAutoRealm ar(cx, contentScope);
+    nsIPrincipal* principal =
+        nsJSPrincipals::get(JS_GetCompartmentPrincipals(js::GetObjectCompartment(contentScope)));
+
+    if (nsContentUtils::IsSystemPrincipal(principal)) {
+        return JS::GetNonCCWObjectGlobal(contentScope);
+    }
+
+    return GetUAWidgetScope(cx, principal);
+}
+
+JSObject*
+GetUAWidgetScope(JSContext* cx, nsIPrincipal* principal)
+{
+    RootedObject scope(cx, XPCJSRuntime::Get()->GetUAWidgetScope(cx, principal));
+    NS_ENSURE_TRUE(scope, nullptr); // See bug 858642.
+
+    scope = js::UncheckedUnwrap(scope);
+    JS::ExposeObjectToActiveJS(scope);
+    return scope;
+}
+
 bool
 AllowContentXBLScope(JS::Realm* realm)
 {
     XPCWrappedNativeScope* scope = RealmPrivate::Get(realm)->scope;
     return scope && scope->AllowContentXBLScope();
 }
 
 bool
--- a/js/xpconnect/src/nsXPConnect.cpp
+++ b/js/xpconnect/src/nsXPConnect.cpp
@@ -1175,23 +1175,50 @@ IsChromeOrXBL(JSContext* cx, JSObject* /
     // compat and not security for remote XUL, we just always claim to be XBL.
     //
     // Note that, for performance, we don't check AllowXULXBLForPrincipal here,
     // and instead rely on the fact that AllowContentXBLScope() only returns false in
     // remote XUL situations.
     return AccessCheck::isChrome(c) || IsContentXBLCompartment(c) || !AllowContentXBLScope(realm);
 }
 
+bool
+IsNotUAWidget(JSContext* cx, JSObject* /* unused */)
+{
+    MOZ_ASSERT(NS_IsMainThread());
+    JS::Realm* realm = JS::GetCurrentRealmOrNull(cx);
+    MOZ_ASSERT(realm);
+    JS::Compartment* c = JS::GetCompartmentForRealm(realm);
+
+    return !IsUAWidgetCompartment(c);
+}
+
+bool
+IsChromeOrXBLOrUAWidget(JSContext* cx, JSObject* /* unused */)
+{
+    if (IsChromeOrXBL(cx, nullptr)) {
+      return true;
+    }
+
+    MOZ_ASSERT(NS_IsMainThread());
+    JS::Realm* realm = JS::GetCurrentRealmOrNull(cx);
+    MOZ_ASSERT(realm);
+    JS::Compartment* c = JS::GetCompartmentForRealm(realm);
+
+    return IsUAWidgetCompartment(c);
+}
+
+
 extern bool IsCurrentThreadRunningChromeWorker();
 
 bool
-ThreadSafeIsChromeOrXBL(JSContext* cx, JSObject* obj)
+ThreadSafeIsChromeOrXBLOrUAWidget(JSContext* cx, JSObject* obj)
 {
     if (NS_IsMainThread()) {
-        return IsChromeOrXBL(cx, obj);
+        return IsChromeOrXBLOrUAWidget(cx, obj);
     }
     return IsCurrentThreadRunningChromeWorker();
 }
 
 } // namespace dom
 } // namespace mozilla
 
 void
--- a/js/xpconnect/src/xpcprivate.h
+++ b/js/xpconnect/src/xpcprivate.h
@@ -2661,16 +2661,17 @@ public:
         , allowWaivers(true)
         , wantComponents(true)
         , wantExportHelpers(false)
         , isWebExtensionContentScript(false)
         , proto(cx)
         , sameZoneAs(cx)
         , freshZone(false)
         , isContentXBLScope(false)
+        , isUAWidgetScope(false)
         , invisibleToDebugger(false)
         , discardSource(false)
         , metadata(cx)
         , userContextId(0)
         , originAttributes(cx)
     { }
 
     virtual bool Parse() override;
@@ -2680,16 +2681,17 @@ public:
     bool wantComponents;
     bool wantExportHelpers;
     bool isWebExtensionContentScript;
     JS::RootedObject proto;
     nsCString sandboxName;
     JS::RootedObject sameZoneAs;
     bool freshZone;
     bool isContentXBLScope;
+    bool isUAWidgetScope;
     bool invisibleToDebugger;
     bool discardSource;
     GlobalProperties globalProperties;
     JS::RootedValue metadata;
     uint32_t userContextId;
     JS::RootedObject originAttributes;
 
 protected:
@@ -2930,19 +2932,27 @@ public:
     // to opt into CPOWs. It's necessary for the implementation of
     // RemoteAddonsParent.jsm.
     bool allowCPOWs;
 
     // True if this compartment is a content XBL compartment. Every global in
     // such a compartment is a content XBL scope.
     bool isContentXBLCompartment;
 
+    // True if this compartment is a UA widget compartment.
+    bool isUAWidgetCompartment;
+
     // True if this is a sandbox compartment. See xpc::CreateSandboxObject.
     bool isSandboxCompartment;
 
+    // True if EnsureAddonCompartment has been called for this compartment.
+    // Note that this is false for extensions that ship with the browser, like
+    // browser/extensions/activity-stream.
+    bool isAddonCompartment;
+
     // This is only ever set during mochitest runs when enablePrivilege is called.
     // It's intended as a temporary stopgap measure until we can finish ripping out
     // enablePrivilege. Once set, this value is never unset (i.e., it doesn't follow
     // the old scoping rules of enablePrivilege).
     //
     // Using it in production is inherently unsafe.
     bool universalXPConnectEnabled;
 
--- a/js/xpconnect/src/xpcpublic.h
+++ b/js/xpconnect/src/xpcpublic.h
@@ -81,16 +81,20 @@ TransplantObject(JSContext* cx, JS::Hand
 
 JSObject*
 TransplantObjectRetainingXrayExpandos(JSContext* cx, JS::HandleObject origobj, JS::HandleObject target);
 
 bool IsContentXBLCompartment(JS::Compartment* compartment);
 bool IsContentXBLScope(JS::Realm* realm);
 bool IsInContentXBLScope(JSObject* obj);
 
+bool IsUAWidgetCompartment(JS::Compartment* compartment);
+bool IsUAWidgetScope(JS::Realm* realm);
+bool IsInUAWidgetScope(JSObject* obj);
+
 bool IsInSandboxCompartment(JSObject* obj);
 
 // Return a raw XBL scope object corresponding to contentScope, which must
 // be an object whose global is a DOM window.
 //
 // The return value is not wrapped into cx->compartment, so be sure to enter
 // its compartment before doing anything meaningful.
 //
@@ -99,16 +103,22 @@ bool IsInSandboxCompartment(JSObject* ob
 // exist.
 //
 // This function asserts if |contentScope| is itself in an XBL scope to catch
 // sloppy consumers. Conversely, GetXBLScopeOrGlobal will handle objects that
 // are in XBL scope (by just returning the global).
 JSObject*
 GetXBLScope(JSContext* cx, JSObject* contentScope);
 
+JSObject*
+GetUAWidgetScope(JSContext* cx, nsIPrincipal* principal);
+
+JSObject*
+GetUAWidgetScope(JSContext* cx, JSObject* contentScope);
+
 inline JSObject*
 GetXBLScopeOrGlobal(JSContext* cx, JSObject* obj)
 {
     MOZ_ASSERT(!js::IsCrossCompartmentWrapper(obj));
     if (IsInContentXBLScope(obj))
         return JS::GetNonCCWObjectGlobal(obj);
     return GetXBLScope(cx, obj);
 }
@@ -718,16 +728,29 @@ namespace dom {
 
 /**
  * A test for whether WebIDL methods that should only be visible to
  * chrome or XBL scopes should be exposed.
  */
 bool IsChromeOrXBL(JSContext* cx, JSObject* /* unused */);
 
 /**
- * Same as IsChromeOrXBL but can be used in worker threads as well.
+ * This is used to prevent UA widget code from directly creating and adopting
+ * nodes via the content document, since they should use the special
+ * create-and-insert apis instead.
  */
-bool ThreadSafeIsChromeOrXBL(JSContext* cx, JSObject* obj);
+bool IsNotUAWidget(JSContext* cx, JSObject* /* unused */);
+
+/**
+ * A test for whether WebIDL methods that should only be visible to
+ * chrome, XBL scopes, or UA Widget scopes.
+ */
+bool IsChromeOrXBLOrUAWidget(JSContext* cx, JSObject* /* unused */);
+
+/**
+ * Same as IsChromeOrXBLOrUAWidget but can be used in worker threads as well.
+ */
+bool ThreadSafeIsChromeOrXBLOrUAWidget(JSContext* cx, JSObject* obj);
 
 } // namespace dom
 } // namespace mozilla
 
 #endif
--- a/layout/generic/nsVideoFrame.cpp
+++ b/layout/generic/nsVideoFrame.cpp
@@ -175,16 +175,17 @@ nsVideoFrame::AppendAnonymousContentTo(n
 nsIContent*
 nsVideoFrame::GetVideoControls()
 {
   if (mVideoControls) {
     return mVideoControls;
   }
   if (mContent->GetShadowRoot()) {
     // The video controls <div> is the only child of the UA Widget Shadow Root.
+    MOZ_ASSERT(mContent->GetShadowRoot()->IsUAWidget());
     return mContent->GetShadowRoot()->GetFirstChild();
   }
   return nullptr;
 }
 
 void
 nsVideoFrame::DestroyFrom(nsIFrame* aDestructRoot, PostDestroyData& aPostDestroyData)
 {
--- a/toolkit/content/tests/widgets/mochitest.ini
+++ b/toolkit/content/tests/widgets/mochitest.ini
@@ -20,16 +20,17 @@ support-files =
   videocontrols_direction-2d.html
   videocontrols_direction-2e.html
   videocontrols_direction_test.js
   videomask.css
 
 [test_audiocontrols_dimensions.html]
 [test_mousecapture_area.html]
 skip-if = (verify && debug)
+[test_ua_widget.html]
 [test_videocontrols.html]
 tags = fullscreen
 skip-if = toolkit == 'android' || (verify && debug && (os == 'linux')) #TIMED_OUT
 [test_videocontrols_keyhandler.html]
 skip-if = toolkit == 'android'
 [test_videocontrols_vtt.html]
 [test_videocontrols_iframe_fullscreen.html]
 [test_videocontrols_size.html]
new file mode 100644
--- /dev/null
+++ b/toolkit/content/tests/widgets/test_ua_widget.html
@@ -0,0 +1,102 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>UA Widget test</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/AddTask.js"></script>
+  <script type="text/javascript" src="head.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+
+<div id="content">
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+const content = document.getElementById("content");
+
+const div = content.appendChild(document.createElement("div"));
+div.attachShadow({ mode: "open"});
+SpecialPowers.wrap(div.shadowRoot).setIsUAWidget(true);
+
+const sandbox = SpecialPowers.Cu.getUAWidgetScope(SpecialPowers.wrap(div).nodePrincipal);
+
+SpecialPowers.setWrapped(sandbox, "info", SpecialPowers.wrap(info));
+SpecialPowers.setWrapped(sandbox, "is", SpecialPowers.wrap(is));
+SpecialPowers.setWrapped(sandbox, "ok", SpecialPowers.wrap(ok));
+
+const sandboxScript = function(shadowRoot) {
+  info("UA Widget scope tests");
+  is(typeof window, "undefined", "The sandbox has no window");
+  is(typeof document, "undefined", "The sandbox has no document");
+
+  let element = shadowRoot.host;
+  let doc = element.ownerDocument;
+  let win = doc.defaultView;
+
+  ok(shadowRoot instanceof win.ShadowRoot, "shadowRoot is a ShadowRoot");
+  ok(element instanceof win.HTMLDivElement, "Element is a <div>");
+
+  is("createElement" in doc, false, "No document.createElement");
+  is("createElementNS" in doc, false, "No document.createElementNS");
+  is("createTextNode" in doc, false, "No document.createTextNode");
+  is("createComment" in doc, false, "No document.createComment");
+  is("importNode" in doc, false, "No document.importNode");
+  is("adoptNode" in doc, false, "No document.adoptNode");
+
+  is("insertBefore" in element, false, "No element.insertBefore");
+  is("appendChild" in element, false, "No element.appendChild");
+  is("replaceChild" in element, false, "No element.replaceChild");
+  is("cloneNode" in element, false, "No element.cloneNode");
+
+  ok("importNodeAndAppendChildAt" in shadowRoot, "shadowRoot.importNodeAndAppendChildAt");
+  ok("createElementAndAppendChildAt" in shadowRoot, "shadowRoot.createElementAndAppendChildAt");
+
+  info("UA Widget special methods tests");
+
+  const span = shadowRoot.createElementAndAppendChildAt(shadowRoot, "span");
+  span.textContent = "Hello from <span>!";
+
+  is(shadowRoot.lastChild, span, "<span> inserted");
+
+  const parser = new win.DOMParser();
+  let parserDoc = parser.parseFromString(
+    `<div xmlns="http://www.w3.org/1999/xhtml">Hello from DOMParser!</div>`, "application/xml");
+  shadowRoot.importNodeAndAppendChildAt(shadowRoot, parserDoc.documentElement, true);
+
+  ok(shadowRoot.lastChild instanceof win.HTMLDivElement, "<div> inserted");
+  is(shadowRoot.lastChild.textContent, "Hello from DOMParser!", "Deep import node worked");
+
+  info("UA Widget reflectors tests");
+
+  win.wrappedJSObject.spanElementFromUAWidget = span;
+  win.wrappedJSObject.divElementFromUAWidget = shadowRoot.lastChild;
+};
+SpecialPowers.Cu.evalInSandbox("this.script = " + sandboxScript.toSource(), sandbox);
+sandbox.script(div.shadowRoot);
+
+ok(window.spanElementFromUAWidget instanceof HTMLSpanElement, "<span> exposed");
+ok(window.divElementFromUAWidget instanceof HTMLDivElement, "<div> exposed");
+
+try {
+  window.spanElementFromUAWidget.textContent;
+  ok(false, "Should throw.");
+} catch (err) {
+  ok(/denied/.test(err), "Permission denied to access <span>");
+}
+
+try {
+  window.divElementFromUAWidget.textContent;
+  ok(false, "Should throw.");
+} catch (err) {
+  ok(/denied/.test(err), "Permission denied to access <div>");
+}
+
+</script>
+</pre>
+</body>
+</html>
--- a/toolkit/content/widgets/videocontrols.js
+++ b/toolkit/content/widgets/videocontrols.js
@@ -1646,25 +1646,22 @@ this.VideoControlsImplPageWidget = class
           if (tt.mode === "showing") {
             this.changeTextTrack(tt.index);
           }
           return;
         }
 
         tt.index = this.textTracksCount++;
 
-        const label = tt.label || "";
-        const ttText = this.document.createTextNode(label);
-        const ttBtn = this.document.createElement("button");
+        const ttBtn =
+          this.shadowRoot.createElementAndAppendChildAt(this.textTrackList, "button");
+        ttBtn.textContent = tt.label || "";
 
         ttBtn.classList.add("textTrackItem");
         ttBtn.setAttribute("index", tt.index);
-        ttBtn.appendChild(ttText);
-
-        this.textTrackList.appendChild(ttBtn);
 
         if (tt.mode === "showing" && tt.index) {
           this.changeTextTrack(tt.index);
         }
       },
 
       changeTextTrack(index) {
         for (let tt of this.overlayableTextTracks) {
@@ -1854,16 +1851,17 @@ this.VideoControlsImplPageWidget = class
             this.clickToPlay.hiddenByAdjustment = false;
           }
           this.clickToPlay.style.width = `${clickToPlayScaledSize}px`;
           this.clickToPlay.style.height = `${clickToPlayScaledSize}px`;
         }
       },
 
       init(shadowRoot) {
+        this.shadowRoot = shadowRoot;
         this.video = shadowRoot.host;
         this.videocontrols = shadowRoot.firstChild;
         this.document = this.videocontrols.ownerDocument;
         this.window = this.document.defaultView;
         this.shadowRoot = shadowRoot;
 
         this.controlsContainer = this.shadowRoot.getElementById("controlsContainer");
         this.statusIcon = this.shadowRoot.getElementById("statusIcon");
@@ -2200,17 +2198,17 @@ this.VideoControlsImplPageWidget = class
                       class="button fullscreenButton"
                       enterfullscreenlabel="&fullscreenButton.enterfullscreenlabel;"
                       exitfullscreenlabel="&fullscreenButton.exitfullscreenlabel;"/>
             </div>
             <div id="textTrackList" class="textTrackList" hidden="true" offlabel="&closedCaption.off;"></div>
           </div>
         </div>
       </div>`, "application/xml");
-    this.shadowRoot.appendChild(this.document.importNode(parserDoc.documentElement, true));
+    this.shadowRoot.importNodeAndAppendChildAt(this.shadowRoot, parserDoc.documentElement, true);
   }
 
   destructor() {
     this.Utils.terminate();
     this.TouchUtils.terminate();
     this.Utils.updateOrientationState(false);
     // randomID used to be a <field>, which meant that the XBL machinery
     // undefined the property when the element was unbound. The code in
@@ -2317,16 +2315,17 @@ this.NoControlsImplPageWidget = class {
           return;
         }
 
         this.noControlsOverlay.hidden = true;
         this.video.play();
       },
 
       init(shadowRoot) {
+        this.shadowRoot = shadowRoot;
         this.video = shadowRoot.host;
         this.videocontrols = shadowRoot.firstChild;
         this.document = this.videocontrols.ownerDocument;
         this.window = this.document.defaultView;
         this.shadowRoot = shadowRoot;
 
         this.randomID = Math.random();
         this.videocontrols.randomID = this.randomID;
@@ -2384,11 +2383,11 @@ this.NoControlsImplPageWidget = class {
         <div id="controlsContainer" class="controlsContainer" role="none" hidden="true">
           <div class="controlsOverlay stackItem">
             <div class="controlsSpacerStack">
               <div id="clickToPlay" class="clickToPlay"></div>
             </div>
           </div>
         </div>
       </div>`, "application/xml");
-    this.shadowRoot.appendChild(this.document.importNode(parserDoc.documentElement, true));
+    this.shadowRoot.importNodeAndAppendChildAt(this.shadowRoot, parserDoc.documentElement, true);
   }
 };