Bug 1502799 - Implement origin-clean algorithm for ImageBitmap, r=aosmond a=lizzard FIREFOX_60_7_0esr_BUILD1 FIREFOX_60_7_0esr_RELEASE
authorAndrea Marchesini <amarchesini@mozilla.com>
Tue, 14 May 2019 17:24:07 +0200
changeset 451200 64ee63facd4ff96b3e8590cff559d7e97ac6b061
parent 451199 9d3b9a48a91710e8bf4eb36c5181e391c7c14abe
child 451201 4fb7510f93b650a4f10ada2b1a40aa79d652dd4c
child 451203 2ae9b50be57173c299c10df512590e2feb164977
push id427
push userarchaeopteryx@coole-files.de
push dateTue, 14 May 2019 15:34:25 +0000
treeherdermozilla-esr60@64ee63facd4f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersaosmond, lizzard
bugs1502799, 1484980, 1526218, 1528909, 1540221
milestone60.7.0
Bug 1502799 - Implement origin-clean algorithm for ImageBitmap, r=aosmond a=lizzard Bug 1484980 - Add selective canvas tainting for content scripts r=bzbarsky Bug 1526218 - transferFromImageBitmap() should propage the origin-clean state to the canvas element, r=aosmond Bug 1528909 - cross-origin checks in CanvasRenderingContext2D::DrawImage, r=aosmond Bug 1540221 - Setting fillStyle to a pattern of an unclean canvas makes the canvas origin-unclean, r=aosmond Differential Revision: https://phabricator.services.mozilla.com//D30950
dom/canvas/CanvasRenderingContext2D.cpp
dom/canvas/CanvasRenderingContext2D.h
dom/canvas/CanvasUtils.cpp
dom/canvas/CanvasUtils.h
dom/canvas/ImageBitmap.cpp
dom/canvas/ImageBitmap.h
dom/canvas/ImageBitmapRenderingContext.cpp
dom/canvas/WebGLContext.h
dom/canvas/WebGLTextureUpload.cpp
dom/canvas/test/test_imagebitmap.html
dom/html/HTMLCanvasElement.cpp
dom/html/HTMLCanvasElement.h
layout/base/nsLayoutUtils.cpp
servo/components/style/gecko_string_cache/namespace.rs
servo/components/style_traits/values.rs
testing/web-platform/meta/html/semantics/embedded-content/the-canvas-element/security.pattern.canvas.fillStyle.cross.html.ini
testing/web-platform/meta/html/semantics/embedded-content/the-canvas-element/security.pattern.canvas.fillStyle.redirect.html.ini
testing/web-platform/meta/html/semantics/embedded-content/the-canvas-element/security.pattern.canvas.strokeStyle.cross.html.ini
testing/web-platform/meta/html/semantics/embedded-content/the-canvas-element/security.pattern.canvas.strokeStyle.redirect.html.ini
testing/web-platform/meta/html/semantics/embedded-content/the-canvas-element/security.pattern.image.fillStyle.cross.html.ini
testing/web-platform/meta/html/semantics/embedded-content/the-canvas-element/security.pattern.image.fillStyle.redirect.html.ini
testing/web-platform/meta/html/semantics/embedded-content/the-canvas-element/security.pattern.image.strokeStyle.cross.html.ini
testing/web-platform/meta/html/semantics/embedded-content/the-canvas-element/security.pattern.image.strokeStyle.redirect.html.ini
testing/web-platform/tests/2dcontext/imagebitmap/createImageBitmap-origin.sub.html
testing/web-platform/tests/common/canvas-tests.js
--- a/dom/canvas/CanvasRenderingContext2D.cpp
+++ b/dom/canvas/CanvasRenderingContext2D.cpp
@@ -1075,17 +1075,18 @@ CanvasRenderingContext2D::CanvasRenderin
       mIPC(false),
       mIsSkiaGL(false),
       mHasPendingStableStateCallback(false),
       mDrawObserver(nullptr),
       mIsEntireFrameInvalid(false),
       mPredictManyRedrawCalls(false),
       mIsCapturedFrameInvalid(false),
       mPathTransformWillUpdate(false),
-      mInvalidateCount(0) {
+      mInvalidateCount(0),
+      mWriteOnly(false) {
   if (!sMaxContextsInitialized) {
     sMaxContexts = gfxPrefs::CanvasAzureAcceleratedLimit();
     sMaxContextsInitialized = true;
   }
 
   sNumLivingContexts++;
 
   mShutdownObserver = new CanvasShutdownObserver(this);
@@ -2341,17 +2342,21 @@ void CanvasRenderingContext2D::SetStyleF
   }
 
   if (aValue.IsCanvasGradient()) {
     SetStyleFromGradient(aValue.GetAsCanvasGradient(), aWhichStyle);
     return;
   }
 
   if (aValue.IsCanvasPattern()) {
-    SetStyleFromPattern(aValue.GetAsCanvasPattern(), aWhichStyle);
+    CanvasPattern& pattern = aValue.GetAsCanvasPattern();
+    SetStyleFromPattern(pattern, aWhichStyle);
+    if (pattern.mForceWriteOnly) {
+      SetWriteOnly();
+    }
     return;
   }
 
   MOZ_ASSERT_UNREACHABLE("Invalid union value");
 }
 
 void CanvasRenderingContext2D::SetFillRule(const nsAString& aString) {
   FillRule rule;
@@ -2492,40 +2497,41 @@ already_AddRefed<CanvasPattern> CanvasRe
       aError.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
       return nullptr;
     }
 
     // An ImageBitmap never taints others so we set principalForSecurityCheck to
     // nullptr and set CORSUsed to true for passing the security check in
     // CanvasUtils::DoDrawImageSecurityCheck().
     RefPtr<CanvasPattern> pat =
-        new CanvasPattern(this, srcSurf, repeatMode, nullptr, false, true);
+      new CanvasPattern(this, srcSurf, repeatMode, nullptr, imgBitmap.IsWriteOnly(), true);
 
     return pat.forget();
   }
 
   EnsureTarget();
   if (!IsTargetValid()) {
     aError.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
     return nullptr;
   }
 
   // The canvas spec says that createPattern should use the first frame
   // of animated images
   nsLayoutUtils::SurfaceFromElementResult res =
       nsLayoutUtils::SurfaceFromElement(
           element, nsLayoutUtils::SFE_WANT_FIRST_FRAME_IF_IMAGE, mTarget);
 
-  if (!res.GetSourceSurface()) {
+  RefPtr<SourceSurface> surface = res.GetSourceSurface();
+  if (!surface) {
     return nullptr;
   }
 
   RefPtr<CanvasPattern> pat =
-      new CanvasPattern(this, res.GetSourceSurface(), repeatMode,
-                        res.mPrincipal, res.mIsWriteOnly, res.mCORSUsed);
+      new CanvasPattern(this, surface, repeatMode, res.mPrincipal,
+                        res.mIsWriteOnly, res.mCORSUsed);
   return pat.forget();
 }
 
 //
 // shadows
 //
 void CanvasRenderingContext2D::SetShadowColor(const nsAString& aShadowColor) {
   nscolor color;
@@ -4843,18 +4849,18 @@ CanvasRenderingContext2D::CachedSurfaceF
 
   int32_t corsmode = imgIRequest::CORS_NONE;
   if (NS_SUCCEEDED(imgRequest->GetCORSMode(&corsmode))) {
     res.mCORSUsed = corsmode != imgIRequest::CORS_NONE;
   }
 
   res.mSize = res.mSourceSurface->GetSize();
   res.mPrincipal = principal.forget();
-  res.mIsWriteOnly = false;
   res.mImageRequest = imgRequest.forget();
+  res.mIsWriteOnly = CheckWriteOnlySecurity(res.mCORSUsed, res.mPrincipal);
 
   return res;
 }
 
 // drawImage(in HTMLImageElement image, in float dx, in float dy);
 //   -- render image from 0,0 at dx,dy top-left coords
 // drawImage(in HTMLImageElement image, in float dx, in float dy, in float dw,
 //           in float dh);
@@ -4903,24 +4909,32 @@ void CanvasRenderingContext2D::DrawImage
   if (aImage.IsHTMLCanvasElement()) {
     HTMLCanvasElement* canvas = &aImage.GetAsHTMLCanvasElement();
     element = canvas;
     nsIntSize size = canvas->GetSize();
     if (size.width == 0 || size.height == 0) {
       aError.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
       return;
     }
+
+    if (canvas->IsWriteOnly()) {
+      SetWriteOnly();
+    }
   } else if (aImage.IsImageBitmap()) {
     ImageBitmap& imageBitmap = aImage.GetAsImageBitmap();
     srcSurf = imageBitmap.PrepareForDrawTarget(mTarget);
 
     if (!srcSurf) {
       return;
     }
 
+    if (imageBitmap.IsWriteOnly()) {
+      SetWriteOnly();
+    }
+
     imgSize = gfx::IntSize(imageBitmap.Width(), imageBitmap.Height());
   } else {
     if (aImage.IsHTMLImageElement()) {
       HTMLImageElement* img = &aImage.GetAsHTMLImageElement();
       element = img;
     } else if (aImage.IsSVGImageElement()) {
       SVGImageElement* img = &aImage.GetAsSVGImageElement();
       element = img;
@@ -5416,23 +5430,18 @@ already_AddRefed<ImageData> CanvasRender
   if (!mCanvasElement && !mDocShell) {
     NS_ERROR("No canvas element and no docshell in GetImageData!!!");
     aError.Throw(NS_ERROR_DOM_SECURITY_ERR);
     return nullptr;
   }
 
   // Check only if we have a canvas element; if we were created with a docshell,
   // then it's special internal use.
-  if (mCanvasElement && mCanvasElement->IsWriteOnly() &&
-      // We could ask bindings for the caller type, but they already hand us a
-      // JSContext, and we're at least _somewhat_ perf-sensitive (so may not
-      // want to compute the caller type in the common non-write-only case), so
-      // let's just use what we have.
-      !nsContentUtils::CallerHasPermission(aCx,
-                                           nsGkAtoms::all_urlsPermission)) {
+  if (IsWriteOnly() ||
+      (mCanvasElement && !mCanvasElement->CallerCanRead(aCx))) {
     // XXX ERRMSG we need to report an error to developers here! (bug 329026)
     aError.Throw(NS_ERROR_DOM_SECURITY_ERR);
     return nullptr;
   }
 
   if (!IsFinite(aSx) || !IsFinite(aSy) || !IsFinite(aSw) || !IsFinite(aSh)) {
     aError.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
     return nullptr;
@@ -6024,16 +6033,23 @@ bool CanvasRenderingContext2D::IsContext
   return !mIsCapturedFrameInvalid;
 }
 
 bool CanvasRenderingContext2D::ShouldForceInactiveLayer(
     LayerManager* aManager) {
   return !aManager->CanUseCanvasLayerForSize(GetSize());
 }
 
+void CanvasRenderingContext2D::SetWriteOnly() {
+  mWriteOnly = true;
+  if (mCanvasElement) {
+    mCanvasElement->SetWriteOnly();
+  }
+}
+
 NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(CanvasPath, AddRef)
 NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(CanvasPath, Release)
 
 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(CanvasPath, mParent)
 
 CanvasPath::CanvasPath(nsISupports* aParent) : mParent(aParent) {
   mPathBuilder = gfxPlatform::GetPlatform()
                      ->ScreenReferenceDrawTarget()
--- a/dom/canvas/CanvasRenderingContext2D.h
+++ b/dom/canvas/CanvasRenderingContext2D.h
@@ -38,16 +38,17 @@ namespace gl {
 class SourceSurface;
 }  // namespace gl
 
 namespace dom {
 class
     HTMLImageElementOrSVGImageElementOrHTMLCanvasElementOrHTMLVideoElementOrImageBitmap;
 typedef HTMLImageElementOrSVGImageElementOrHTMLCanvasElementOrHTMLVideoElementOrImageBitmap
     CanvasImageSource;
+class ImageBitmap;
 class ImageData;
 class StringOrCanvasGradientOrCanvasPattern;
 class OwningStringOrCanvasGradientOrCanvasPattern;
 class TextMetrics;
 class CanvasFilterChainObserver;
 class CanvasPath;
 
 extern const mozilla::gfx::Float SIGMA_MAX;
@@ -1086,16 +1087,26 @@ class CanvasRenderingContext2D final : p
 
   FINISH:
     if (aPerDevPixel) *aPerDevPixel = devPixel;
     if (aPerCSSPixel) *aPerCSSPixel = cssPixel;
   }
 
   friend struct CanvasBidiProcessor;
   friend class CanvasDrawObserver;
+  friend class ImageBitmap;
+
+  void SetWriteOnly();
+
+  bool IsWriteOnly() const
+  {
+    return mWriteOnly;
+  }
+
+  bool mWriteOnly;
 };
 
 size_t BindingJSObjectMallocBytes(CanvasRenderingContext2D* aContext);
 
 }  // namespace dom
 }  // namespace mozilla
 
 #endif /* CanvasRenderingContext2D_h */
--- a/dom/canvas/CanvasUtils.cpp
+++ b/dom/canvas/CanvasUtils.cpp
@@ -231,17 +231,19 @@ void DoDrawImageSecurityCheck(dom::HTMLC
                               nsIPrincipal* aPrincipal, bool forceWriteOnly,
                               bool CORSUsed) {
   // Callers should ensure that mCanvasElement is non-null before calling this
   if (!aCanvasElement) {
     NS_WARNING("DoDrawImageSecurityCheck called without canvas element!");
     return;
   }
 
-  if (aCanvasElement->IsWriteOnly()) return;
+  if (aCanvasElement->IsWriteOnly() && !aCanvasElement->mExpandedReader) {
+    return;
+  }
 
   // If we explicitly set WriteOnly just do it and get out
   if (forceWriteOnly) {
     aCanvasElement->SetWriteOnly();
     return;
   }
 
   // No need to do a security check if the image used CORS for the load
@@ -249,16 +251,35 @@ void DoDrawImageSecurityCheck(dom::HTMLC
 
   NS_PRECONDITION(aPrincipal, "Must have a principal here");
 
   if (aCanvasElement->NodePrincipal()->Subsumes(aPrincipal)) {
     // This canvas has access to that image anyway
     return;
   }
 
+    if (BasePrincipal::Cast(aPrincipal)->AddonPolicy()) {
+        // This is a resource from an extension content script principal.
+
+        if (aCanvasElement->mExpandedReader &&
+            aCanvasElement->mExpandedReader->Subsumes(aPrincipal)) {
+            // This canvas already allows reading from this principal.
+            return;
+        }
+
+        if (!aCanvasElement->mExpandedReader) {
+            // Allow future reads from this same princial only.
+            aCanvasElement->SetWriteOnly(aPrincipal);
+            return;
+        }
+
+        // If we got here, this must be the *second* extension tainting
+        // the canvas.  Fall through to mark it WriteOnly for everyone.
+    }
+
   aCanvasElement->SetWriteOnly();
 }
 
 bool CoerceDouble(const JS::Value& v, double* d) {
   if (v.isDouble()) {
     *d = v.toDouble();
   } else if (v.isInt32()) {
     *d = double(v.toInt32());
@@ -270,10 +291,30 @@ bool CoerceDouble(const JS::Value& v, do
   return true;
 }
 
 bool HasDrawWindowPrivilege(JSContext* aCx, JSObject* /* unused */) {
   return nsContentUtils::CallerHasPermission(aCx,
                                              nsGkAtoms::all_urlsPermission);
 }
 
+bool CheckWriteOnlySecurity(bool aCORSUsed, nsIPrincipal* aPrincipal) {
+  if (!aPrincipal) {
+    return true;
+  }
+
+  if (!aCORSUsed) {
+    nsIGlobalObject* incumbentSettingsObject = dom::GetIncumbentGlobal();
+    if (NS_WARN_IF(!incumbentSettingsObject)) {
+      return true;
+    }
+
+    nsIPrincipal* principal = incumbentSettingsObject->PrincipalOrNull();
+    if (NS_WARN_IF(!principal) || !(principal->Subsumes(aPrincipal))) {
+      return true;
+    }
+  }
+
+  return false;
+}
+
 }  // namespace CanvasUtils
 }  // namespace mozilla
--- a/dom/canvas/CanvasUtils.h
+++ b/dom/canvas/CanvasUtils.h
@@ -6,16 +6,17 @@
 #ifndef _CANVASUTILS_H_
 #define _CANVASUTILS_H_
 
 #include "CanvasRenderingContextHelper.h"
 #include "mozilla/CheckedInt.h"
 #include "mozilla/dom/ToJSValue.h"
 #include "jsapi.h"
 #include "mozilla/FloatingPoint.h"
+#include "nsLayoutUtils.h"
 
 class nsIPrincipal;
 
 namespace mozilla {
 
 namespace dom {
 class HTMLCanvasElement;
 }  // namespace dom
@@ -167,12 +168,16 @@ void DashArrayToJSVal(nsTArray<T>& dashe
     return;
   }
   JS::Rooted<JS::Value> val(cx);
   if (!mozilla::dom::ToJSValue(cx, dashes, retval)) {
     rv.Throw(NS_ERROR_OUT_OF_MEMORY);
   }
 }
 
+// returns true if write-only mode must used for this principal based on
+// the incumbent global.
+bool CheckWriteOnlySecurity(bool aCORSUsed, nsIPrincipal* aPrincipal);
+
 }  // namespace CanvasUtils
 }  // namespace mozilla
 
 #endif /* _CANVASUTILS_H_ */
--- a/dom/canvas/ImageBitmap.cpp
+++ b/dom/canvas/ImageBitmap.cpp
@@ -402,67 +402,37 @@ class CreateImageFromRawDataInMainThread
   uint8_t* mBuffer;
   uint32_t mBufferLength;
   uint32_t mStride;
   gfx::SurfaceFormat mFormat;
   gfx::IntSize mSize;
   const Maybe<IntRect>& mCropRect;
 };
 
-static bool CheckSecurityForHTMLElements(bool aIsWriteOnly, bool aCORSUsed,
-                                         nsIPrincipal* aPrincipal) {
-  if (aIsWriteOnly || !aPrincipal) {
-    return false;
-  }
-
-  if (!aCORSUsed) {
-    nsIGlobalObject* incumbentSettingsObject = GetIncumbentGlobal();
-    if (NS_WARN_IF(!incumbentSettingsObject)) {
-      return false;
-    }
-
-    nsIPrincipal* principal = incumbentSettingsObject->PrincipalOrNull();
-    if (NS_WARN_IF(!principal) || !(principal->Subsumes(aPrincipal))) {
-      return false;
-    }
-  }
-
-  return true;
-}
-
-static bool CheckSecurityForHTMLElements(
-    const nsLayoutUtils::SurfaceFromElementResult& aRes) {
-  return CheckSecurityForHTMLElements(aRes.mIsWriteOnly, aRes.mCORSUsed,
-                                      aRes.mPrincipal);
-}
-
 /*
  * A wrapper to the nsLayoutUtils::SurfaceFromElement() function followed by the
  * security checking.
  */
-template <class HTMLElementType>
+template <class ElementType>
 static already_AddRefed<SourceSurface> GetSurfaceFromElement(
-    nsIGlobalObject* aGlobal, HTMLElementType& aElement, ErrorResult& aRv) {
+    nsIGlobalObject* aGlobal, ElementType& aElement, bool* aWriteOnly,
+    ErrorResult& aRv) {
   nsLayoutUtils::SurfaceFromElementResult res =
       nsLayoutUtils::SurfaceFromElement(
           &aElement, nsLayoutUtils::SFE_WANT_FIRST_FRAME_IF_IMAGE);
 
-  // check origin-clean
-  if (!CheckSecurityForHTMLElements(res)) {
-    aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
-    return nullptr;
-  }
-
   RefPtr<SourceSurface> surface = res.GetSourceSurface();
 
   if (NS_WARN_IF(!surface)) {
     aRv.Throw(NS_ERROR_NOT_AVAILABLE);
     return nullptr;
   }
 
+  *aWriteOnly = res.mIsWriteOnly;
+
   return surface.forget();
 }
 
 /*
  * The specification doesn't allow to create an ImegeBitmap from a vector image.
  * This function is used to check if the given HTMLImageElement contains a
  * raster image.
  */
@@ -480,25 +450,26 @@ static bool HasRasterImage(HTMLImageElem
       return true;
     }
   }
 
   return false;
 }
 
 ImageBitmap::ImageBitmap(nsIGlobalObject* aGlobal, layers::Image* aData,
-                         gfxAlphaType aAlphaType)
+                         bool aWriteOnly, gfxAlphaType aAlphaType)
     : mParent(aGlobal),
       mData(aData),
       mSurface(nullptr),
       mDataWrapper(new ImageUtils(mData)),
       mPictureRect(0, 0, aData->GetSize().width, aData->GetSize().height),
       mAlphaType(aAlphaType),
       mIsCroppingAreaOutSideOfSourceImage(false),
-      mAllocatedImageData(false) {
+      mAllocatedImageData(false),
+      mWriteOnly(aWriteOnly) {
   MOZ_ASSERT(aData, "aData is null in ImageBitmap constructor.");
 
   mShutdownObserver = new ImageBitmapShutdownObserver(this);
   mShutdownObserver->RegisterObserver();
 }
 
 ImageBitmap::~ImageBitmap() {
   if (mShutdownObserver) {
@@ -746,60 +717,59 @@ UniquePtr<ImageBitmapCloneData> ImageBit
   UniquePtr<ImageBitmapCloneData> result(new ImageBitmapCloneData());
   result->mPictureRect = mPictureRect;
   result->mAlphaType = mAlphaType;
   result->mIsCroppingAreaOutSideOfSourceImage =
       mIsCroppingAreaOutSideOfSourceImage;
   RefPtr<SourceSurface> surface = mData->GetAsSourceSurface();
   result->mSurface = surface->GetDataSurface();
   MOZ_ASSERT(result->mSurface);
+  result->mWriteOnly = mWriteOnly;
 
   return Move(result);
 }
 
 /* static */ already_AddRefed<ImageBitmap> ImageBitmap::CreateFromCloneData(
     nsIGlobalObject* aGlobal, ImageBitmapCloneData* aData) {
   RefPtr<layers::Image> data = CreateImageFromSurface(aData->mSurface);
 
-  RefPtr<ImageBitmap> ret = new ImageBitmap(aGlobal, data, aData->mAlphaType);
+  RefPtr<ImageBitmap> ret =
+    new ImageBitmap(aGlobal, data, aData->mWriteOnly, aData->mAlphaType);
 
   ret->mAllocatedImageData = true;
 
   ret->mIsCroppingAreaOutSideOfSourceImage =
       aData->mIsCroppingAreaOutSideOfSourceImage;
 
   ErrorResult rv;
   ret->SetPictureRect(aData->mPictureRect, rv);
   return ret.forget();
 }
 
 /* static */ already_AddRefed<ImageBitmap>
 ImageBitmap::CreateFromOffscreenCanvas(nsIGlobalObject* aGlobal,
                                        OffscreenCanvas& aOffscreenCanvas,
                                        ErrorResult& aRv) {
-  // Check origin-clean.
-  if (aOffscreenCanvas.IsWriteOnly()) {
-    aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
-    return nullptr;
-  }
+  // Check write-only mode.
+  bool writeOnly = aOffscreenCanvas.IsWriteOnly();
 
   nsLayoutUtils::SurfaceFromElementResult res =
       nsLayoutUtils::SurfaceFromOffscreenCanvas(
           &aOffscreenCanvas, nsLayoutUtils::SFE_WANT_FIRST_FRAME_IF_IMAGE);
 
   RefPtr<SourceSurface> surface = res.GetSourceSurface();
 
   if (NS_WARN_IF(!surface)) {
     aRv.Throw(NS_ERROR_NOT_AVAILABLE);
     return nullptr;
   }
 
   RefPtr<layers::Image> data = CreateImageFromSurface(surface);
 
-  RefPtr<ImageBitmap> ret = new ImageBitmap(aGlobal, data);
+  RefPtr<ImageBitmap> ret = new ImageBitmap(aGlobal, data, writeOnly);
 
   ret->mAllocatedImageData = true;
 
   return ret.forget();
 }
 
 /* static */ already_AddRefed<ImageBitmap> ImageBitmap::CreateInternal(
     nsIGlobalObject* aGlobal, HTMLImageElement& aImageEl,
@@ -811,33 +781,36 @@ ImageBitmap::CreateFromOffscreenCanvas(n
   }
 
   // Check if the image element is a bitmap (e.g. it's a vector graphic) or not.
   if (!HasRasterImage(aImageEl)) {
     aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
     return nullptr;
   }
 
+  bool writeOnly = true;
+
   // Get the SourceSurface out from the image element and then do security
   // checking.
-  RefPtr<SourceSurface> surface = GetSurfaceFromElement(aGlobal, aImageEl, aRv);
+  RefPtr<SourceSurface> surface = GetSurfaceFromElement(aGlobal, aImageEl,
+                                                        &writeOnly, aRv);
 
   if (NS_WARN_IF(aRv.Failed())) {
     return nullptr;
   }
 
   // Create ImageBitmap.
   RefPtr<layers::Image> data = CreateImageFromSurface(surface);
 
   if (NS_WARN_IF(!data)) {
     aRv.Throw(NS_ERROR_NOT_AVAILABLE);
     return nullptr;
   }
 
-  RefPtr<ImageBitmap> ret = new ImageBitmap(aGlobal, data);
+  RefPtr<ImageBitmap> ret = new ImageBitmap(aGlobal, data, writeOnly);
 
   // Set the picture rectangle.
   if (ret && aCropRect.isSome()) {
     ret->SetPictureRect(aCropRect.ref(), aRv);
   }
 
   // Set mIsCroppingAreaOutSideOfSourceImage.
   ret->SetIsCroppingAreaOutSideOfSourceImage(surface->GetSize(), aCropRect);
@@ -863,28 +836,25 @@ ImageBitmap::CreateFromOffscreenCanvas(n
   if (aVideoEl.ReadyState() <= HAVE_METADATA) {
     aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
     return nullptr;
   }
 
   // Check security.
   nsCOMPtr<nsIPrincipal> principal = aVideoEl.GetCurrentVideoPrincipal();
   bool CORSUsed = aVideoEl.GetCORSMode() != CORS_NONE;
-  if (!CheckSecurityForHTMLElements(false, CORSUsed, principal)) {
-    aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
-    return nullptr;
-  }
+  bool writeOnly = CheckWriteOnlySecurity(CORSUsed, principal);
 
   // Create ImageBitmap.
   RefPtr<layers::Image> data = aVideoEl.GetCurrentImage();
   if (!data) {
     aRv.Throw(NS_ERROR_NOT_AVAILABLE);
     return nullptr;
   }
-  RefPtr<ImageBitmap> ret = new ImageBitmap(aGlobal, data);
+  RefPtr<ImageBitmap> ret = new ImageBitmap(aGlobal, data, writeOnly);
 
   // Set the picture rectangle.
   if (ret && aCropRect.isSome()) {
     ret->SetPictureRect(aCropRect.ref(), aRv);
   }
 
   // Set mIsCroppingAreaOutSideOfSourceImage.
   ret->SetIsCroppingAreaOutSideOfSourceImage(data->GetSize(), aCropRect);
@@ -895,23 +865,28 @@ ImageBitmap::CreateFromOffscreenCanvas(n
 /* static */ already_AddRefed<ImageBitmap> ImageBitmap::CreateInternal(
     nsIGlobalObject* aGlobal, HTMLCanvasElement& aCanvasEl,
     const Maybe<IntRect>& aCropRect, ErrorResult& aRv) {
   if (aCanvasEl.Width() == 0 || aCanvasEl.Height() == 0) {
     aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
     return nullptr;
   }
 
-  RefPtr<SourceSurface> surface =
-      GetSurfaceFromElement(aGlobal, aCanvasEl, aRv);
+  bool writeOnly = true;
+  RefPtr<SourceSurface> surface = GetSurfaceFromElement(aGlobal, aCanvasEl,
+                                                        &writeOnly, aRv);
 
   if (NS_WARN_IF(aRv.Failed())) {
     return nullptr;
   }
 
+  if (!writeOnly) {
+    writeOnly = aCanvasEl.IsWriteOnly();
+  }
+
   // Crop the source surface if needed.
   RefPtr<SourceSurface> croppedSurface;
   IntRect cropRect = aCropRect.valueOr(IntRect());
 
   // If the HTMLCanvasElement's rendering context is WebGL, then the snapshot
   // we got from the HTMLCanvasElement is a DataSourceSurface which is a copy
   // of the rendering context. We handle cropping in this case.
   bool needToReportMemoryAllocation = false;
@@ -934,17 +909,17 @@ ImageBitmap::CreateFromOffscreenCanvas(n
   // Create an Image from the SourceSurface.
   RefPtr<layers::Image> data = CreateImageFromSurface(croppedSurface);
 
   if (NS_WARN_IF(!data)) {
     aRv.Throw(NS_ERROR_NOT_AVAILABLE);
     return nullptr;
   }
 
-  RefPtr<ImageBitmap> ret = new ImageBitmap(aGlobal, data);
+  RefPtr<ImageBitmap> ret = new ImageBitmap(aGlobal, data, writeOnly);
 
   if (needToReportMemoryAllocation) {
     ret->mAllocatedImageData = true;
   }
 
   // Set the picture rectangle.
   if (ret && aCropRect.isSome()) {
     ret->SetPictureRect(cropRect, aRv);
@@ -996,37 +971,36 @@ ImageBitmap::CreateFromOffscreenCanvas(n
   }
 
   if (NS_WARN_IF(!data)) {
     aRv.Throw(NS_ERROR_NOT_AVAILABLE);
     return nullptr;
   }
 
   // Create an ImageBimtap.
-  RefPtr<ImageBitmap> ret = new ImageBitmap(aGlobal, data, alphaType);
+  RefPtr<ImageBitmap> ret =
+    new ImageBitmap(aGlobal, data, false /* write-only */, alphaType);
 
   ret->mAllocatedImageData = true;
 
   // The cropping information has been handled in the CreateImageFromRawData()
   // function.
 
   // Set mIsCroppingAreaOutSideOfSourceImage.
   ret->SetIsCroppingAreaOutSideOfSourceImage(imageSize, aCropRect);
 
   return ret.forget();
 }
 
 /* static */ already_AddRefed<ImageBitmap> ImageBitmap::CreateInternal(
     nsIGlobalObject* aGlobal, CanvasRenderingContext2D& aCanvasCtx,
     const Maybe<IntRect>& aCropRect, ErrorResult& aRv) {
-  // Check origin-clean.
-  if (aCanvasCtx.GetCanvas()->IsWriteOnly()) {
-    aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
-    return nullptr;
-  }
+  // Check write-only mode.
+  bool writeOnly =
+      aCanvasCtx.GetCanvas()->IsWriteOnly() || aCanvasCtx.IsWriteOnly();
 
   RefPtr<SourceSurface> surface = aCanvasCtx.GetSurfaceSnapshot();
 
   if (NS_WARN_IF(!surface)) {
     aRv.Throw(NS_ERROR_NOT_AVAILABLE);
     return nullptr;
   }
 
@@ -1038,17 +1012,18 @@ ImageBitmap::CreateFromOffscreenCanvas(n
 
   RefPtr<layers::Image> data = CreateImageFromSurface(surface);
 
   if (NS_WARN_IF(!data)) {
     aRv.Throw(NS_ERROR_NOT_AVAILABLE);
     return nullptr;
   }
 
-  RefPtr<ImageBitmap> ret = new ImageBitmap(aGlobal, data);
+  RefPtr<ImageBitmap> ret =
+    new ImageBitmap(aGlobal, data, writeOnly);
 
   ret->mAllocatedImageData = true;
 
   // Set the picture rectangle.
   if (ret && aCropRect.isSome()) {
     ret->SetPictureRect(aCropRect.ref(), aRv);
   }
 
@@ -1063,17 +1038,18 @@ ImageBitmap::CreateFromOffscreenCanvas(n
     const Maybe<IntRect>& aCropRect, ErrorResult& aRv) {
   if (!aImageBitmap.mData) {
     aRv.Throw(NS_ERROR_NOT_AVAILABLE);
     return nullptr;
   }
 
   RefPtr<layers::Image> data = aImageBitmap.mData;
   RefPtr<ImageBitmap> ret =
-      new ImageBitmap(aGlobal, data, aImageBitmap.mAlphaType);
+      new ImageBitmap(aGlobal, data, aImageBitmap.mWriteOnly,
+                      aImageBitmap.mAlphaType);
 
   // Set the picture rectangle.
   if (ret && aCropRect.isSome()) {
     ret->SetPictureRect(aCropRect.ref(), aRv);
   }
 
   // Set mIsCroppingAreaOutSideOfSourceImage.
   if (aImageBitmap.mIsCroppingAreaOutSideOfSourceImage == true) {
@@ -1379,24 +1355,29 @@ static void AsyncCreateImageBitmapFromBl
   // aParent might be null.
 
   uint32_t picRectX_;
   uint32_t picRectY_;
   uint32_t picRectWidth_;
   uint32_t picRectHeight_;
   uint32_t alphaType_;
   uint32_t isCroppingAreaOutSideOfSourceImage_;
+  uint32_t writeOnly;
+  uint32_t dummy;
 
   if (!JS_ReadUint32Pair(aReader, &picRectX_, &picRectY_) ||
       !JS_ReadUint32Pair(aReader, &picRectWidth_, &picRectHeight_) ||
       !JS_ReadUint32Pair(aReader, &alphaType_,
-                         &isCroppingAreaOutSideOfSourceImage_)) {
+                         &isCroppingAreaOutSideOfSourceImage_) ||
+      !JS_ReadUint32Pair(aReader, &writeOnly, &dummy)) {
     return nullptr;
   }
 
+  MOZ_ASSERT(dummy == 0);
+
   int32_t picRectX = BitwiseCast<int32_t>(picRectX_);
   int32_t picRectY = BitwiseCast<int32_t>(picRectY_);
   int32_t picRectWidth = BitwiseCast<int32_t>(picRectWidth_);
   int32_t picRectHeight = BitwiseCast<int32_t>(picRectHeight_);
   const auto alphaType = BitwiseCast<gfxAlphaType>(alphaType_);
 
   // Create a new ImageBitmap.
   MOZ_ASSERT(!aClonedSurfaces.IsEmpty());
@@ -1410,17 +1391,17 @@ static void AsyncCreateImageBitmapFromBl
   JS::Rooted<JS::Value> value(aCx);
   {
 #ifdef FUZZING
     if (aIndex >= aClonedSurfaces.Length()) {
       return nullptr;
     }
 #endif
     RefPtr<layers::Image> img = CreateImageFromSurface(aClonedSurfaces[aIndex]);
-    RefPtr<ImageBitmap> imageBitmap = new ImageBitmap(aParent, img, alphaType);
+    RefPtr<ImageBitmap> imageBitmap = new ImageBitmap(aParent, img, !!writeOnly, alphaType);
 
     imageBitmap->mIsCroppingAreaOutSideOfSourceImage =
         isCroppingAreaOutSideOfSourceImage_;
 
     ErrorResult error;
     imageBitmap->SetPictureRect(
         IntRect(picRectX, picRectY, picRectWidth, picRectHeight), error);
     if (NS_WARN_IF(error.Failed())) {
@@ -1457,17 +1438,18 @@ static void AsyncCreateImageBitmapFromBl
 
   // Indexing the cloned surfaces and send the index to the receiver.
   uint32_t index = aClonedSurfaces.Length();
 
   if (NS_WARN_IF(!JS_WriteUint32Pair(aWriter, SCTAG_DOM_IMAGEBITMAP, index)) ||
       NS_WARN_IF(!JS_WriteUint32Pair(aWriter, picRectX, picRectY)) ||
       NS_WARN_IF(!JS_WriteUint32Pair(aWriter, picRectWidth, picRectHeight)) ||
       NS_WARN_IF(!JS_WriteUint32Pair(aWriter, alphaType,
-                                     isCroppingAreaOutSideOfSourceImage))) {
+                                     isCroppingAreaOutSideOfSourceImage)) ||
+      NS_WARN_IF(!JS_WriteUint32Pair(aWriter, aImageBitmap->mWriteOnly, 0))) {
     return false;
   }
 
   RefPtr<SourceSurface> surface = aImageBitmap->mData->GetAsSourceSurface();
   RefPtr<DataSourceSurface> snapshot = surface->GetDataSurface();
   RefPtr<DataSourceSurface> dstDataSurface;
   {
     // DataSourceSurfaceD2D1::GetStride() will call EnsureMapped implicitly and
@@ -1998,17 +1980,18 @@ class CreateImageFromBufferSourceRawData
   if (NS_WARN_IF(!data)) {
     aRv.Throw(NS_ERROR_NOT_AVAILABLE);
     return promise.forget();
   }
 
   // Create an ImageBimtap.
   // Assume the data from an external buffer is not alpha-premultiplied.
   RefPtr<ImageBitmap> imageBitmap =
-      new ImageBitmap(aGlobal, data, gfxAlphaType::NonPremult);
+      new ImageBitmap(aGlobal, data, false /* write-only */,
+                      gfxAlphaType::NonPremult);
 
   imageBitmap->mAllocatedImageData = true;
 
   // We don't need to call SetPictureRect() here because there is no cropping
   // supported and the ImageBitmap's mPictureRect is the size of the source
   // image in default
 
   // We don't need to set mIsCroppingAreaOutSideOfSourceImage here because there
@@ -2234,17 +2217,18 @@ void CreateImageBitmapFromBlob::DecodeAn
   });
 
   if (NS_WARN_IF(NS_FAILED(aStatus))) {
     mPromise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR);
     return;
   }
 
   // Create ImageBitmap object.
-  RefPtr<ImageBitmap> imageBitmap = new ImageBitmap(mGlobalObject, aImage);
+  RefPtr<ImageBitmap> imageBitmap = new ImageBitmap(mGlobalObject, aImage,
+                                                    false /* write-only */);
 
   // Set mIsCroppingAreaOutSideOfSourceImage.
   imageBitmap->SetIsCroppingAreaOutSideOfSourceImage(mSourceSize,
                                                      mOriginalCropRect);
 
   if (mCropRect.isSome()) {
     ErrorResult rv;
     imageBitmap->SetPictureRect(mCropRect.ref(), rv);
--- a/dom/canvas/ImageBitmap.h
+++ b/dom/canvas/ImageBitmap.h
@@ -58,16 +58,17 @@ class Promise;
 class PostMessageEvent;  // For StructuredClone between windows.
 class ImageBitmapShutdownObserver;
 
 struct ImageBitmapCloneData final {
   RefPtr<gfx::DataSourceSurface> mSurface;
   gfx::IntRect mPictureRect;
   gfxAlphaType mAlphaType;
   bool mIsCroppingAreaOutSideOfSourceImage;
+  bool mWriteOnly;
 };
 
 /*
  * ImageBitmap is an opaque handler to several kinds of image-like objects from
  * HTMLImageElement, HTMLVideoElement, HTMLCanvasElement, ImageData to
  * CanvasRenderingContext2D and Image Blob.
  *
  * An ImageBitmap could be painted to a canvas element.
@@ -155,16 +156,21 @@ class ImageBitmap final : public nsISupp
       JSContext* aCx, ImageBitmapFormat aFormat,
       const ArrayBufferViewOrArrayBuffer& aBuffer, int32_t aOffset,
       ErrorResult& aRv);
 
   size_t GetAllocatedSize() const;
 
   void OnShutdown();
 
+  bool IsWriteOnly() const
+  {
+    return mWriteOnly;
+  }
+
  protected:
   /*
    * The default value of aIsPremultipliedAlpha is TRUE because that the
    * data stored in HTMLImageElement, HTMLVideoElement, HTMLCanvasElement,
    * CanvasRenderingContext2D are alpha-premultiplied in default.
    *
    * Actually, if one HTMLCanvasElement's rendering context is WebGLContext, it
    * is possible to get un-premultipliedAlpha data out. But, we do not do it in
@@ -176,16 +182,17 @@ class ImageBitmap final : public nsISupp
    *    re-decoding if the original decoded data is alpha-premultiplied) and
    * 2) while decoding a blob. But we do not do it in both code path too.
    *
    * ImageData's underlying data is triggered as non-premultipliedAlpha, so set
    * the aIsPremultipliedAlpha to be false in the
    * CreateInternal(from ImageData) method.
    */
   ImageBitmap(nsIGlobalObject* aGlobal, layers::Image* aData,
+              bool aWriteOnly,
               gfxAlphaType aAlphaType = gfxAlphaType::Premult);
 
   virtual ~ImageBitmap();
 
   void SetPictureRect(const gfx::IntRect& aRect, ErrorResult& aRv);
 
   void SetIsCroppingAreaOutSideOfSourceImage(
       const gfx::IntSize& aSourceSize,
@@ -266,14 +273,21 @@ class ImageBitmap final : public nsISupp
    * IndexSizeError.
    */
   bool mIsCroppingAreaOutSideOfSourceImage;
 
   /*
    * Whether this object allocated allocated and owns the image data.
    */
   bool mAllocatedImageData;
+
+  /*
+   * Write-Only flag is set to true if this image has been generated from a
+   * cross-origin source. This is the opposite of what is called 'origin-clean'
+   * in the spec.
+   */
+  bool mWriteOnly;
 };
 
 }  // namespace dom
 }  // namespace mozilla
 
 #endif  // mozilla_dom_ImageBitmap_h
--- a/dom/canvas/ImageBitmapRenderingContext.cpp
+++ b/dom/canvas/ImageBitmapRenderingContext.cpp
@@ -55,16 +55,20 @@ void ImageBitmapRenderingContext::Transf
     ImageBitmap& aImageBitmap) {
   Reset();
   mImage = aImageBitmap.TransferAsImage();
 
   if (!mImage) {
     return;
   }
 
+  if (aImageBitmap.IsWriteOnly() && mCanvasElement) {
+    mCanvasElement->SetWriteOnly();
+  }
+
   Redraw(gfxRect(0, 0, mWidth, mHeight));
 }
 
 NS_IMETHODIMP
 ImageBitmapRenderingContext::SetDimensions(int32_t aWidth, int32_t aHeight) {
   mWidth = aWidth;
   mHeight = aHeight;
   return NS_OK;
--- a/dom/canvas/WebGLContext.h
+++ b/dom/canvas/WebGLContext.h
@@ -236,18 +236,19 @@ struct TexImageSourceAdapter final : pub
                         GLuint ignored2 = 0) {
     mPboOffset = pboOffset;
   }
 
   TexImageSourceAdapter(const WebGLsizeiptr* pboOffset, ErrorResult* ignored) {
     mPboOffset = pboOffset;
   }
 
-  TexImageSourceAdapter(const dom::ImageBitmap* imageBitmap, ErrorResult*) {
+  TexImageSourceAdapter(const dom::ImageBitmap* imageBitmap, ErrorResult* out_error) {
     mImageBitmap = imageBitmap;
+        mOut_error = out_error;
   }
 
   TexImageSourceAdapter(const dom::ImageData* imageData, ErrorResult*) {
     mImageData = imageData;
   }
 
   TexImageSourceAdapter(const dom::Element* domElem,
                         ErrorResult* const out_error) {
--- a/dom/canvas/WebGLTextureUpload.cpp
+++ b/dom/canvas/WebGLTextureUpload.cpp
@@ -7,16 +7,17 @@
 
 #include <algorithm>
 
 #include "CanvasUtils.h"
 #include "gfxPrefs.h"
 #include "GLBlitHelper.h"
 #include "GLContext.h"
 #include "mozilla/gfx/2D.h"
+#include "mozilla/dom/HTMLCanvasElement.h"
 #include "mozilla/dom/HTMLVideoElement.h"
 #include "mozilla/dom/ImageBitmap.h"
 #include "mozilla/dom/ImageData.h"
 #include "mozilla/MathAlgorithms.h"
 #include "mozilla/Scoped.h"
 #include "mozilla/Unused.h"
 #include "ScopedGLHelpers.h"
 #include "TexUnpackBlob.h"
@@ -217,17 +218,22 @@ static UniquePtr<webgl::TexUnpackBytes> 
   const auto ptr = (const uint8_t*)pboOffset;
   return MakeUnique<webgl::TexUnpackBytes>(webgl, target, width, height, depth,
                                            isClientData, ptr, availBufferBytes);
 }
 
 static UniquePtr<webgl::TexUnpackBlob> FromImageBitmap(
     WebGLContext* webgl, const char* funcName, TexImageTarget target,
     uint32_t width, uint32_t height, uint32_t depth,
-    const dom::ImageBitmap& imageBitmap) {
+    const dom::ImageBitmap& imageBitmap, ErrorResult* aRv) {
+    if (imageBitmap.IsWriteOnly()) {
+        aRv->Throw(NS_ERROR_DOM_SECURITY_ERR);
+        return nullptr;
+    }
+
   UniquePtr<dom::ImageBitmapCloneData> cloneData =
       Move(imageBitmap.ToCloneData());
   const RefPtr<gfx::DataSourceSurface> surf = cloneData->mSurface;
 
   if (!width) {
     width = surf->GetSize().width;
   }
 
@@ -289,16 +295,24 @@ static UniquePtr<webgl::TexUnpackBlob> F
   return MakeUnique<webgl::TexUnpackSurface>(webgl, target, width, height,
                                              depth, surf, alphaType);
 }
 
 UniquePtr<webgl::TexUnpackBlob> WebGLContext::FromDomElem(
     const char* funcName, TexImageTarget target, uint32_t width,
     uint32_t height, uint32_t depth, const dom::Element& elem,
     ErrorResult* const out_error) {
+    if (elem.IsHTMLElement(nsGkAtoms::canvas)) {
+        const dom::HTMLCanvasElement* canvas = static_cast<const dom::HTMLCanvasElement*>(&elem);
+        if (canvas->IsWriteOnly()) {
+            out_error->Throw(NS_ERROR_DOM_SECURITY_ERR);
+            return nullptr;
+        }
+    }
+
   // The canvas spec says that drawImage should draw the first frame of
   // animated images. The webgl spec doesn't mention the issue, so we do the
   // same as drawImage.
   uint32_t flags = nsLayoutUtils::SFE_WANT_FIRST_FRAME_IF_IMAGE |
                    nsLayoutUtils::SFE_WANT_IMAGE_SURFACE |
                    nsLayoutUtils::SFE_USE_ELEMENT_SIZE_IF_VECTOR;
 
   if (mPixelStore_ColorspaceConversion == LOCAL_GL_NONE)
@@ -409,17 +423,17 @@ UniquePtr<webgl::TexUnpackBlob> WebGLCon
 
   if (mBoundPixelUnpackBuffer) {
     ErrorInvalidOperation("%s: PIXEL_UNPACK_BUFFER must be null.", funcName);
     return nullptr;
   }
 
   if (src.mImageBitmap) {
     return FromImageBitmap(this, funcName, target, width, height, depth,
-                           *(src.mImageBitmap));
+                           *(src.mImageBitmap), src.mOut_error);
   }
 
   if (src.mImageData) {
     return FromImageData(this, funcName, target, width, height, depth,
                          *(src.mImageData), scopedArr);
   }
 
   if (src.mDomElem) {
--- a/dom/canvas/test/test_imagebitmap.html
+++ b/dom/canvas/test/test_imagebitmap.html
@@ -265,23 +265,32 @@ function testSecurityErrors() {
         reject();
       }
 
       uncleanImage.src = "http://example.com/tests/dom/canvas/test/crossorigin/image.png";
     });
   }
 
   function checkPromiseFailedWithSecurityError(p) {
-    return p.then( function(reason) { ok(false, "Did not get SecurityError with unclean source. ImageBitmap was created successfully."); },
-                   function(reason) { if (reason == "SecurityError: The operation is insecure.") {
-                                        ok(true, reason);
-                                      }
-                                      else {
-                                        ok(false, "Did not get SecurityError with unclean source. Error Message: " + reason);
-                                      }});
+    return p.then(imageBitmap => {
+      ok(!!imageBitmap, "ImageBitmaps are always created");
+      const context = document.createElement("canvas").getContext("2d");
+      context.drawImage(imageBitmap, 0, 0);
+      try {
+        context.getImageData(0, 0, 1, 1);
+        ok(false, "Did not get SecurityError with unclean source. ImageBitmap was created successfully.");
+      } catch (ex) {
+        if (ex == "SecurityError: The operation is insecure.") {
+          ok(true, ex);
+        }
+        else {
+          ok(false, "Did not get SecurityError with unclean source. Error Message: " + ex);
+        }
+      }
+    });
   }
 
   return Promise.all([
     checkPromiseFailedWithSecurityError(getUncleanImagePromise()),
     checkPromiseFailedWithSecurityError(getUncleanVideoPromise()),
     checkPromiseFailedWithSecurityError(getTaintedCanvasPromise()),
     checkPromiseFailedWithSecurityError(getTaintedCanvasRenderingContex2dPromise()),
   ]);
--- a/dom/html/HTMLCanvasElement.cpp
+++ b/dom/html/HTMLCanvasElement.cpp
@@ -581,19 +581,18 @@ bool HTMLCanvasElement::ParseAttribute(i
 
   return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue,
                                               aMaybeScriptedPrincipal, aResult);
 }
 
 void HTMLCanvasElement::ToDataURL(JSContext* aCx, const nsAString& aType,
                                   JS::Handle<JS::Value> aParams,
                                   nsAString& aDataURL, ErrorResult& aRv) {
-  // do a trust check if this is a write-only canvas
-  if (mWriteOnly && !nsContentUtils::CallerHasPermission(
-                        aCx, nsGkAtoms::all_urlsPermission)) {
+  // mWriteOnly check is redundant, but optimizes for the common case.
+  if (mWriteOnly && !CallerCanRead(aCx)) {
     aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
     return;
   }
 
   aRv = ToDataURLImpl(aCx, aType, aParams, aDataURL);
 }
 
 void HTMLCanvasElement::SetMozPrintCallback(PrintCallback* aCallback) {
@@ -756,19 +755,18 @@ nsresult HTMLCanvasElement::ToDataURLImp
   return Base64EncodeInputStream(stream, aDataURL, (uint32_t)count,
                                  aDataURL.Length());
 }
 
 void HTMLCanvasElement::ToBlob(JSContext* aCx, BlobCallback& aCallback,
                                const nsAString& aType,
                                JS::Handle<JS::Value> aParams,
                                ErrorResult& aRv) {
-  // do a trust check if this is a write-only canvas
-  if (mWriteOnly && !nsContentUtils::CallerHasPermission(
-                        aCx, nsGkAtoms::all_urlsPermission)) {
+  // mWriteOnly check is redundant, but optimizes for the common case.
+  if (mWriteOnly && !CallerCanRead(aCx)) {
     aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
     return;
   }
 
   nsCOMPtr<nsIGlobalObject> global = OwnerDoc()->GetScopeObject();
   MOZ_ASSERT(global);
 
   nsIntSize elemSize = GetWidthHeight();
@@ -934,19 +932,48 @@ already_AddRefed<nsISupports> HTMLCanvas
   }
 
   nsCOMPtr<nsISupports> context(mCurrentContext);
   return context.forget();
 }
 
 nsIntSize HTMLCanvasElement::GetSize() { return GetWidthHeight(); }
 
-bool HTMLCanvasElement::IsWriteOnly() { return mWriteOnly; }
+bool HTMLCanvasElement::IsWriteOnly() const { return mWriteOnly; }
+
+void HTMLCanvasElement::SetWriteOnly() { 
+  mExpandedReader = nullptr;
+  mWriteOnly = true;
+}
+
+void
+HTMLCanvasElement::SetWriteOnly(nsIPrincipal* aExpandedReader)
+{
+  mExpandedReader = aExpandedReader;
+  mWriteOnly = true;
+}
 
-void HTMLCanvasElement::SetWriteOnly() { mWriteOnly = true; }
+bool
+HTMLCanvasElement::CallerCanRead(JSContext* aCx)
+{
+  if (!mWriteOnly) {
+    return true;
+  }
+
+  nsIPrincipal* prin = nsContentUtils::SubjectPrincipal(aCx);
+
+  // If mExpandedReader is set, this canvas was tainted only by
+  // mExpandedReader's resources. So allow reading if the subject
+  // principal subsumes mExpandedReader.
+  if (mExpandedReader && prin->Subsumes(mExpandedReader)) {
+    return true;
+  }
+
+  return nsContentUtils::PrincipalHasPermission(prin, nsGkAtoms::all_urlsPermission);
+}
 
 void HTMLCanvasElement::InvalidateCanvasContent(const gfx::Rect* damageRect) {
   // We don't need to flush anything here; if there's no frame or if
   // we plan to reframe we don't need to invalidate it anyway.
   nsIFrame* frame = GetPrimaryFrame();
   if (!frame) return;
 
   ActiveLayerTracker::NotifyContentChange(frame);
--- a/dom/html/HTMLCanvasElement.h
+++ b/dom/html/HTMLCanvasElement.h
@@ -196,24 +196,30 @@ class HTMLCanvasElement final : public n
   /**
    * Get the size in pixels of this canvas element
    */
   nsIntSize GetSize();
 
   /**
    * Determine whether the canvas is write-only.
    */
-  bool IsWriteOnly();
+  bool IsWriteOnly() const;
 
   /**
    * Force the canvas to be write-only.
    */
   void SetWriteOnly();
 
   /**
+   * Force the canvas to be write-only, except for readers from
+   * a specific extension's content script expanded principal.
+   */
+  void SetWriteOnly(nsIPrincipal* aExpandedReader);
+
+  /**
    * Notify that some canvas content has changed and the window may
    * need to be updated. aDamageRect is in canvas coordinates.
    */
   void InvalidateCanvasContent(const mozilla::gfx::Rect* aDamageRect);
   /*
    * Notify that we need to repaint the entire canvas, including updating of
    * the layer tree.
    */
@@ -364,17 +370,24 @@ class HTMLCanvasElement final : public n
   RefPtr<HTMLCanvasElementObserver> mContextObserver;
 
  public:
   // Record whether this canvas should be write-only or not.
   // We set this when script paints an image from a different origin.
   // We also transitively set it when script paints a canvas which
   // is itself write-only.
   bool mWriteOnly;
+ 
+  // When this canvas is (only) tainted by an image from an extension
+  // content script, allow reads from the same extension afterwards.
+  RefPtr<nsIPrincipal> mExpandedReader;
 
+  // Determines if the caller should be able to read the content.
+  bool CallerCanRead(JSContext* aCx);
+  
   bool IsPrintCallbackDone();
 
   void HandlePrintCallback(nsPresContext::nsPresContextType aType);
 
   nsresult DispatchPrintCallback(nsITimerCallback* aCallback);
 
   void ResetPrintCallback();
 
--- a/layout/base/nsLayoutUtils.cpp
+++ b/layout/base/nsLayoutUtils.cpp
@@ -3,16 +3,17 @@
 /* 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/. */
 
 #include "nsLayoutUtils.h"
 
 #include "mozilla/ArrayUtils.h"
 #include "mozilla/BasicEvents.h"
+#include "mozilla/dom/CanvasUtils.h"
 #include "mozilla/ClearOnShutdown.h"
 #include "mozilla/EffectCompositor.h"
 #include "mozilla/EffectSet.h"
 #include "mozilla/EventDispatcher.h"
 #include "mozilla/FloatingPoint.h"
 #include "mozilla/gfx/gfxVars.h"
 #include "mozilla/gfx/PathHelpers.h"
 #include "mozilla/layers/PAPZ.h"
@@ -7301,19 +7302,20 @@ nsLayoutUtils::SurfaceFromElementResult 
   }
 
   int32_t corsmode;
   if (NS_SUCCEEDED(imgRequest->GetCORSMode(&corsmode))) {
     result.mCORSUsed = (corsmode != imgIRequest::CORS_NONE);
   }
 
   result.mPrincipal = principal.forget();
-  // no images, including SVG images, can load content from another domain.
-  result.mIsWriteOnly = false;
   result.mImageRequest = imgRequest.forget();
+  result.mIsWriteOnly =
+      CanvasUtils::CheckWriteOnlySecurity(result.mCORSUsed, result.mPrincipal);
+
   return result;
 }
 
 nsLayoutUtils::SurfaceFromElementResult nsLayoutUtils::SurfaceFromElement(
     HTMLImageElement* aElement, uint32_t aSurfaceFlags,
     RefPtr<DrawTarget>& aTarget) {
   return SurfaceFromElement(static_cast<nsIImageLoadingContent*>(aElement),
                             aSurfaceFlags, aTarget);
@@ -7396,17 +7398,18 @@ nsLayoutUtils::SurfaceFromElementResult 
       result.mSourceSurface = opt;
     }
   }
 
   result.mCORSUsed = aElement->GetCORSMode() != CORS_NONE;
   result.mHasSize = true;
   result.mSize = result.mLayersImage->GetSize();
   result.mPrincipal = principal.forget();
-  result.mIsWriteOnly = false;
+  result.mIsWriteOnly =
+      CanvasUtils::CheckWriteOnlySecurity(result.mCORSUsed, result.mPrincipal);
 
   return result;
 }
 
 nsLayoutUtils::SurfaceFromElementResult nsLayoutUtils::SurfaceFromElement(
     dom::Element* aElement, uint32_t aSurfaceFlags,
     RefPtr<DrawTarget>& aTarget) {
   // If it's a <canvas>, we may be able to just grab its internal surface
--- a/servo/components/style/gecko_string_cache/namespace.rs
+++ b/servo/components/style/gecko_string_cache/namespace.rs
@@ -6,16 +6,17 @@
 
 use gecko_bindings::structs::nsAtom;
 use precomputed_hash::PrecomputedHash;
 use std::borrow::Borrow;
 use std::fmt;
 use std::ops::Deref;
 use string_cache::{Atom, WeakAtom};
 
+/// aaa
 #[macro_export]
 macro_rules! ns {
     () => { $crate::string_cache::Namespace(atom!("")) };
     ($s: tt) => { $crate::string_cache::Namespace(atom!($s)) };
 }
 
 /// A Gecko namespace is just a wrapped atom.
 #[derive(Clone, Debug, Default, Eq, Hash, MallocSizeOf, PartialEq)]
--- a/servo/components/style_traits/values.rs
+++ b/servo/components/style_traits/values.rs
@@ -130,16 +130,17 @@ where
             if !prefix.is_empty() {
                 self.inner.write_str(prefix)?;
             }
         }
         self.inner.write_char(c)
     }
 }
 
+/*
 #[macro_export]
 macro_rules! serialize_function {
     ($dest: expr, $name: ident($( $arg: expr, )+)) => {
         serialize_function!($dest, $name($($arg),+))
     };
     ($dest: expr, $name: ident($first_arg: expr $( , $arg: expr )*)) => {
         {
             $dest.write_str(concat!(stringify!($name), "("))?;
@@ -147,16 +148,17 @@ macro_rules! serialize_function {
             $(
                 $dest.write_str(", ")?;
                 $arg.to_css($dest)?;
             )*
             $dest.write_char(')')
         }
     }
 }
+*/
 
 /// Convenience wrapper to serialise CSS values separated by a given string.
 pub struct SequenceWriter<'a, 'b: 'a, W: 'b> {
     inner: &'a mut CssWriter<'b, W>,
     separator: &'static str,
 }
 
 impl<'a, 'b, W> SequenceWriter<'a, 'b, W>
@@ -399,16 +401,17 @@ impl_to_css_for_predefined_type!(f32);
 impl_to_css_for_predefined_type!(i32);
 impl_to_css_for_predefined_type!(u16);
 impl_to_css_for_predefined_type!(u32);
 impl_to_css_for_predefined_type!(::cssparser::Token<'a>);
 impl_to_css_for_predefined_type!(::cssparser::RGBA);
 impl_to_css_for_predefined_type!(::cssparser::Color);
 impl_to_css_for_predefined_type!(::cssparser::UnicodeRange);
 
+/// Define an enum type with unit variants that each correspond to a CSS keyword.
 #[macro_export]
 macro_rules! define_css_keyword_enum {
     (pub enum $name:ident { $($variant:ident = $css:expr,)+ }) => {
         #[allow(missing_docs)]
         #[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
         #[derive(Clone, Copy, Debug, Eq, Hash, MallocSizeOf, PartialEq)]
         pub enum $name {
             $($variant),+
deleted file mode 100644
--- a/testing/web-platform/meta/html/semantics/embedded-content/the-canvas-element/security.pattern.canvas.fillStyle.cross.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[security.pattern.canvas.fillStyle.cross.html]
-  [Setting fillStyle to a pattern of an unclean canvas makes the canvas origin-unclean]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/html/semantics/embedded-content/the-canvas-element/security.pattern.canvas.fillStyle.redirect.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[security.pattern.canvas.fillStyle.redirect.html]
-  [Setting fillStyle to a pattern of an unclean canvas makes the canvas origin-unclean]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/html/semantics/embedded-content/the-canvas-element/security.pattern.canvas.strokeStyle.cross.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[security.pattern.canvas.strokeStyle.cross.html]
-  [Setting strokeStyle to a pattern of an unclean canvas makes the canvas origin-unclean]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/html/semantics/embedded-content/the-canvas-element/security.pattern.canvas.strokeStyle.redirect.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[security.pattern.canvas.strokeStyle.redirect.html]
-  [Setting strokeStyle to a pattern of an unclean canvas makes the canvas origin-unclean]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/html/semantics/embedded-content/the-canvas-element/security.pattern.image.fillStyle.cross.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[security.pattern.image.fillStyle.cross.html]
-  [Setting fillStyle to a pattern of a different-origin image makes the canvas origin-unclean]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/html/semantics/embedded-content/the-canvas-element/security.pattern.image.fillStyle.redirect.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[security.pattern.image.fillStyle.redirect.html]
-  [Setting fillStyle to a pattern of a different-origin image makes the canvas origin-unclean]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/html/semantics/embedded-content/the-canvas-element/security.pattern.image.strokeStyle.cross.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[security.pattern.image.strokeStyle.cross.html]
-  [Setting strokeStyle to a pattern of a different-origin image makes the canvas origin-unclean]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/html/semantics/embedded-content/the-canvas-element/security.pattern.image.strokeStyle.redirect.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[security.pattern.image.strokeStyle.redirect.html]
-  [Setting strokeStyle to a pattern of a different-origin image makes the canvas origin-unclean]
-    expected: FAIL
-
--- a/testing/web-platform/tests/2dcontext/imagebitmap/createImageBitmap-origin.sub.html
+++ b/testing/web-platform/tests/2dcontext/imagebitmap/createImageBitmap-origin.sub.html
@@ -1,98 +1,27 @@
 <!DOCTYPE html>
 <meta charset=utf-8>
 <title>createImageBitmap: origin-clean flag</title>
 <script src="/resources/testharness.js"></script>
 <script src="/resources/testharnessreport.js"></script>
 <script src="/common/media.js"></script>
 <script src="/common/namespaces.js"></script>
+<script src="/common/canvas-tests.js"></script>
 <div id=log></div>
 <script>
-const crossOriginImageUrl = "http://{{domains[www1]}}:{{ports[http][0]}}/images/red.png";
 
 function assert_origin_unclean(bitmap) {
   const context = document.createElement("canvas").getContext("2d");
   context.drawImage(bitmap, 0, 0);
   assert_throws("SecurityError", () => {
     context.getImageData(0, 0, 1, 1);
   });
 }
 
-function makeImage() {
-  return new Promise((resolve, reject) => {
-    const image = new Image();
-    image.onload = () => resolve(image);
-    image.onerror = reject;
-    image.src = crossOriginImageUrl;
-  });
-}
-
-const arguments = [
-  {
-    name: "cross-origin HTMLImageElement",
-    factory: makeImage,
-  },
-
-  {
-    name: "cross-origin SVGImageElement",
-    factory: () => {
-      return new Promise((resolve, reject) => {
-        const image = document.createElementNS(NAMESPACES.svg, "image");
-        image.onload = () => resolve(image);
-        image.onerror = reject;
-        image.setAttribute("externalResourcesRequired", "true");
-        image.setAttributeNS(NAMESPACES.xlink, 'xlink:href', crossOriginImageUrl);
-        document.body.appendChild(image);
-      });
-    },
-  },
-
-  {
-    name: "cross-origin HTMLVideoElement",
-    factory: () => {
-      return new Promise((resolve, reject) => {
-        const video = document.createElement("video");
-        video.oncanplaythrough = () => resolve(video);
-        video.onerror = reject;
-        video.src = getVideoURI("http://{{domains[www1]}}:{{ports[http][0]}}/media/movie_300");
-      });
-    },
-  },
-
-  {
-    name: "redirected to cross-origin HTMLVideoElement",
-    factory: () => {
-      return new Promise((resolve, reject) => {
-        const video = document.createElement("video");
-        video.oncanplaythrough = () => resolve(video);
-        video.onerror = reject;
-        video.src = "/common/redirect.py?location=" + getVideoURI("http://{{domains[www1]}}:{{ports[http][0]}}/media/movie_300");
-      });
-    },
-  },
-
-  {
-    name: "unclean HTMLCanvasElement",
-    factory: () => {
-      return makeImage().then(image => {
-        const canvas = document.createElement("canvas");
-        const context = canvas.getContext("2d");
-        context.drawImage(image, 0, 0);
-        return canvas;
-      });
-    },
-  },
-
-  {
-    name: "unclean ImageBitmap",
-    factory: () => {
-      return makeImage().then(createImageBitmap);
-    },
-  },
-];
-
-for (let { name, factory } of arguments) {
+forEachCanvasSource("http://{{domains[www1]}}:{{ports[http][0]}}",
+                    "http://{{domains[]}}:{{ports[http][0]}}",
+                    (name, factory) => {
   promise_test(function() {
     return factory().then(createImageBitmap).then(assert_origin_unclean);
   }, name);
-}
+});
 </script>
--- a/testing/web-platform/tests/common/canvas-tests.js
+++ b/testing/web-platform/tests/common/canvas-tests.js
@@ -98,8 +98,99 @@ function addCrossOriginRedirectYellowIma
 {
     var img = new Image();
     img.id = "yellow.png";
     img.className = "resource";
     img.src = get_host_info().HTTP_ORIGIN + "/common/redirect.py?location=" +
         get_host_info().HTTP_REMOTE_ORIGIN + "/images/yellow.png";
     document.body.appendChild(img);
 }
+
+function forEachCanvasSource(crossOriginUrl, sameOriginUrl, callback) {
+  function makeImage() {
+    return new Promise((resolve, reject) => {
+      const image = new Image();
+      image.onload = () => resolve(image);
+      image.onerror = reject;
+      image.src = crossOriginUrl + "/images/red.png";
+    });
+  }
+
+  const arguments = [
+    {
+      name: "cross-origin HTMLImageElement",
+      factory: makeImage,
+    },
+
+    {
+      name: "cross-origin SVGImageElement",
+      factory: () => {
+        return new Promise((resolve, reject) => {
+          const image = document.createElementNS(NAMESPACES.svg, "image");
+          image.onload = () => resolve(image);
+          image.onerror = reject;
+          image.setAttribute("externalResourcesRequired", "true");
+          image.setAttributeNS(NAMESPACES.xlink, 'xlink:href', crossOriginUrl + "/images/red.png");
+          document.body.appendChild(image);
+        });
+      },
+    },
+
+    {
+      name: "cross-origin HTMLVideoElement",
+      factory: () => {
+        return new Promise((resolve, reject) => {
+          const video = document.createElement("video");
+          video.oncanplaythrough = () => resolve(video);
+          video.onerror = reject;
+          video.src = getVideoURI(crossOriginUrl + "/media/movie_300");
+        });
+      },
+    },
+
+    {
+      name: "redirected to cross-origin HTMLVideoElement",
+      factory: () => {
+        return new Promise((resolve, reject) => {
+          const video = document.createElement("video");
+          video.oncanplaythrough = () => resolve(video);
+          video.onerror = reject;
+          video.src = "/common/redirect.py?location=" + getVideoURI(crossOriginUrl + "/media/movie_300");
+        });
+      },
+    },
+
+    {
+      name: "redirected to same-origin HTMLVideoElement",
+      factory: () => {
+        return new Promise((resolve, reject) => {
+          const video = document.createElement("video");
+          video.oncanplaythrough = () => resolve(video);
+          video.onerror = reject;
+          video.src = crossOriginUrl + "/common/redirect.py?location=" + getVideoURI(sameOriginUrl + "/media/movie_300");
+        });
+      },
+    },
+
+    {
+      name: "unclean HTMLCanvasElement",
+      factory: () => {
+        return makeImage().then(image => {
+          const canvas = document.createElement("canvas");
+          const context = canvas.getContext("2d");
+          context.drawImage(image, 0, 0);
+          return canvas;
+        });
+      },
+    },
+
+    {
+      name: "unclean ImageBitmap",
+      factory: () => {
+        return makeImage().then(createImageBitmap);
+      },
+    },
+  ];
+
+  for (let { name, factory } of arguments) {
+    callback(name, factory);
+  }
+}