Bug 1621433 - In RFP mode, turn canvas image extraction into a random 'poison pill' for fingerprinters r=tjr,jrmuizel
authorsanketh <sgmenda@uwaterloo.ca>
Thu, 14 May 2020 13:56:55 +0000
changeset 529890 ab2a75db3ebe80fbedb60769df57ee9680d927e8
parent 529889 5b6a16bd94feb25ac39102798942b19650d5ec5a
child 529891 a19376112fb6bb93e19750ba7ab047875e1de002
push id115967
push usercsabou@mozilla.com
push dateThu, 14 May 2020 14:33:24 +0000
treeherderautoland@ab2a75db3ebe [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerstjr, jrmuizel
bugs1621433
milestone78.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1621433 - In RFP mode, turn canvas image extraction into a random 'poison pill' for fingerprinters r=tjr,jrmuizel In RFP mode, canvas image extraction leads to an all-white image, replace that with a random (sample 32 bytes of randomness and fill the buffer with that) 'poison pill'. This helps defeat naive fingerprinters by producing a random image on every try. This feature is toggled using a new, default on, pref `privacy.resistFingerprinting.randomDataOnCanvasExtract`. Updated `browser_canvas_fingerprinting_resistance.js` to test this new feature as well. Updates and replaces D66308. Differential Revision: https://phabricator.services.mozilla.com/D72716
browser/base/content/test/permissions/browser_canvas_fingerprinting_resistance.js
dom/base/ImageEncoder.cpp
dom/canvas/CanvasRenderingContext2D.cpp
dom/canvas/GeneratePlaceholderCanvasData.h
dom/canvas/moz.build
dom/html/HTMLCanvasElement.cpp
modules/libpref/init/StaticPrefList.yaml
--- a/browser/base/content/test/permissions/browser_canvas_fingerprinting_resistance.js
+++ b/browser/base/content/test/permissions/browser_canvas_fingerprinting_resistance.js
@@ -1,13 +1,16 @@
 /**
  * When "privacy.resistFingerprinting" is set to true, user permission is
  * required for canvas data extraction.
  * This tests whether the site permission prompt for canvas data extraction
  * works properly.
+ * When "privacy.resistFingerprinting.randomDataOnCanvasExtract" is true,
+ * canvas data extraction results in random data, and when it is false, canvas
+ * data extraction results in all-white data.
  */
 "use strict";
 
 const kUrl = "https://example.com/";
 const kPrincipal = Services.scriptSecurityManager.createContentPrincipal(
   Services.io.newURI(kUrl),
   {}
 );
@@ -43,65 +46,111 @@ function initTab() {
     }
 
     return canvas;
   };
 
   let placeholder = drawCanvas("white");
   contentWindow.kPlaceholderData = placeholder.toDataURL();
   let canvas = drawCanvas("cyan", "canvas-id-canvas");
+  contentWindow.kPlacedData = canvas.toDataURL();
+  is(
+    canvas.toDataURL(),
+    contentWindow.kPlacedData,
+    "privacy.resistFingerprinting = false, canvas data == placed data"
+  );
   isnot(
     canvas.toDataURL(),
     contentWindow.kPlaceholderData,
     "privacy.resistFingerprinting = false, canvas data != placeholder data"
   );
 }
 
-function enableResistFingerprinting(autoDeclineNoInput) {
+function enableResistFingerprinting(
+  randomDataOnCanvasExtract,
+  autoDeclineNoInput
+) {
   return SpecialPowers.pushPrefEnv({
     set: [
       ["privacy.resistFingerprinting", true],
       [
+        "privacy.resistFingerprinting.randomDataOnCanvasExtract",
+        randomDataOnCanvasExtract,
+      ],
+      [
         "privacy.resistFingerprinting.autoDeclineNoUserInputCanvasPrompts",
         autoDeclineNoInput,
       ],
     ],
   });
 }
 
 function promisePopupShown() {
   return BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown");
 }
 
 function promisePopupHidden() {
   return BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popuphidden");
 }
 
-function extractCanvasData(grantPermission) {
+function extractCanvasData(randomDataOnCanvasExtract, grantPermission) {
   let contentWindow = content.wrappedJSObject;
   let canvas = contentWindow.document.getElementById("canvas-id-canvas");
   let canvasData = canvas.toDataURL();
   if (grantPermission) {
-    isnot(
-      canvasData,
-      contentWindow.kPlaceholderData,
-      "privacy.resistFingerprinting = true, permission granted, canvas data != placeholderdata"
-    );
-  } else if (grantPermission === false) {
     is(
       canvasData,
-      contentWindow.kPlaceholderData,
-      "privacy.resistFingerprinting = true, permission denied, canvas data == placeholderdata"
+      contentWindow.kPlacedData,
+      "privacy.resistFingerprinting = true, permission granted, canvas data == placed data"
     );
+    if (!randomDataOnCanvasExtract) {
+      isnot(
+        canvasData,
+        contentWindow.kPlaceholderData,
+        "privacy.resistFingerprinting = true and randomDataOnCanvasExtract = false, permission granted, canvas data != placeholderdata"
+      );
+    }
+  } else if (grantPermission === false) {
+    isnot(
+      canvasData,
+      contentWindow.kPlacedData,
+      "privacy.resistFingerprinting = true, permission denied, canvas data != placed data"
+    );
+    if (!randomDataOnCanvasExtract) {
+      is(
+        canvasData,
+        contentWindow.kPlaceholderData,
+        "privacy.resistFingerprinting = true and randomDataOnCanvasExtract = false, permission denied, canvas data == placeholderdata"
+      );
+    } else {
+      isnot(
+        canvasData,
+        contentWindow.kPlaceholderData,
+        "privacy.resistFingerprinting = true and randomDataOnCanvasExtract = true, permission denied, canvas data != placeholderdata"
+      );
+    }
   } else {
-    is(
+    isnot(
       canvasData,
-      contentWindow.kPlaceholderData,
-      "privacy.resistFingerprinting = true, requesting permission, canvas data == placeholderdata"
+      contentWindow.kPlacedData,
+      "privacy.resistFingerprinting = true, requesting permission, canvas data != placed data"
     );
+    if (!randomDataOnCanvasExtract) {
+      is(
+        canvasData,
+        contentWindow.kPlaceholderData,
+        "privacy.resistFingerprinting = true and randomDataOnCanvasExtract = false, requesting permission, canvas data == placeholderdata"
+      );
+    } else {
+      isnot(
+        canvasData,
+        contentWindow.kPlaceholderData,
+        "privacy.resistFingerprinting = true and randomDataOnCanvasExtract = true, requesting permission, canvas data != placeholderdata"
+      );
+    }
   }
 }
 
 function triggerCommand(button) {
   let notifications = PopupNotifications.panel.children;
   let notification = notifications[0];
   EventUtils.synthesizeMouseAtCenter(notification[button], {});
 }
@@ -113,68 +162,86 @@ function triggerMainCommand() {
 function triggerSecondaryCommand() {
   triggerCommand("secondaryButton");
 }
 
 function testPermission() {
   return Services.perms.testPermissionFromPrincipal(kPrincipal, kPermission);
 }
 
-async function withNewTabNoInput(grantPermission, browser) {
+async function withNewTabNoInput(
+  randomDataOnCanvasExtract,
+  grantPermission,
+  browser
+) {
   await SpecialPowers.spawn(browser, [], initTab);
-  await enableResistFingerprinting(false);
+  await enableResistFingerprinting(randomDataOnCanvasExtract, false);
   let popupShown = promisePopupShown();
-  await SpecialPowers.spawn(browser, [], extractCanvasData);
+  await SpecialPowers.spawn(
+    browser,
+    [randomDataOnCanvasExtract],
+    extractCanvasData
+  );
   await popupShown;
   let popupHidden = promisePopupHidden();
   if (grantPermission) {
     triggerMainCommand();
     await popupHidden;
     is(testPermission(), Services.perms.ALLOW_ACTION, "permission granted");
   } else {
     triggerSecondaryCommand();
     await popupHidden;
     is(testPermission(), Services.perms.DENY_ACTION, "permission denied");
   }
-  await SpecialPowers.spawn(browser, [grantPermission], extractCanvasData);
+  await SpecialPowers.spawn(
+    browser,
+    [randomDataOnCanvasExtract, grantPermission],
+    extractCanvasData
+  );
   await SpecialPowers.popPrefEnv();
 }
 
-async function doTestNoInput(grantPermission) {
+async function doTestNoInput(randomDataOnCanvasExtract, grantPermission) {
   await BrowserTestUtils.withNewTab(
     kUrl,
-    withNewTabNoInput.bind(null, grantPermission)
+    withNewTabNoInput.bind(null, randomDataOnCanvasExtract, grantPermission)
   );
   Services.perms.removeFromPrincipal(kPrincipal, kPermission);
 }
 
 // With auto-declining disabled (not the default)
 // Tests clicking "Don't Allow" button of the permission prompt.
-add_task(doTestNoInput.bind(null, false));
+add_task(doTestNoInput.bind(null, true, false));
+add_task(doTestNoInput.bind(null, false, false));
 
 // Tests clicking "Allow" button of the permission prompt.
-add_task(doTestNoInput.bind(null, true));
+add_task(doTestNoInput.bind(null, true, true));
+add_task(doTestNoInput.bind(null, false, true));
 
-async function withNewTabAutoBlockNoInput(browser) {
+async function withNewTabAutoBlockNoInput(randomDataOnCanvasExtract, browser) {
   await SpecialPowers.spawn(browser, [], initTab);
-  await enableResistFingerprinting(true);
+  await enableResistFingerprinting(randomDataOnCanvasExtract, true);
 
   let noShowHandler = () => {
     ok(false, "The popup notification should not show in this case.");
   };
   PopupNotifications.panel.addEventListener("popupshown", noShowHandler, {
     once: true,
   });
 
   let promisePopupObserver = TestUtils.topicObserved(
     "PopupNotifications-updateNotShowing"
   );
 
   // Try to extract canvas data without user inputs.
-  await SpecialPowers.spawn(browser, [], extractCanvasData);
+  await SpecialPowers.spawn(
+    browser,
+    [randomDataOnCanvasExtract],
+    extractCanvasData
+  );
 
   await promisePopupObserver;
   info("There should be no popup shown on the panel.");
 
   // Check that the icon of canvas permission is shown.
   let canvasNotification = PopupNotifications.getNotification(
     "canvas-permissions-prompt",
     browser
@@ -185,48 +252,94 @@ async function withNewTabAutoBlockNoInpu
     "true",
     "The canvas permission icon is correctly shown."
   );
   PopupNotifications.panel.removeEventListener("popupshown", noShowHandler);
 
   await SpecialPowers.popPrefEnv();
 }
 
-add_task(async function doTestAutoBlockNoInput() {
-  await BrowserTestUtils.withNewTab(kUrl, withNewTabAutoBlockNoInput);
-});
+async function doTestAutoBlockNoInput(randomDataOnCanvasExtract) {
+  await BrowserTestUtils.withNewTab(
+    kUrl,
+    withNewTabAutoBlockNoInput.bind(null, randomDataOnCanvasExtract)
+  );
+}
 
-function extractCanvasDataUserInput(grantPermission) {
+add_task(doTestAutoBlockNoInput.bind(null, true));
+add_task(doTestAutoBlockNoInput.bind(null, false));
+
+function extractCanvasDataUserInput(
+  randomDataOnCanvasExtract,
+  grantPermission
+) {
   let contentWindow = content.wrappedJSObject;
   let canvas = contentWindow.document.getElementById("canvas-id-canvas");
   let canvasData = canvas.toDataURL();
   if (grantPermission) {
-    isnot(
-      canvasData,
-      contentWindow.kPlaceholderData,
-      "privacy.resistFingerprinting = true, permission granted, canvas data != placeholderdata"
-    );
-  } else if (grantPermission === false) {
     is(
       canvasData,
-      contentWindow.kPlaceholderData,
-      "privacy.resistFingerprinting = true, permission denied, canvas data == placeholderdata"
+      contentWindow.kPlacedData,
+      "privacy.resistFingerprinting = true, permission granted, canvas data == placed data"
     );
+    if (!randomDataOnCanvasExtract) {
+      isnot(
+        canvasData,
+        contentWindow.kPlaceholderData,
+        "privacy.resistFingerprinting = true and randomDataOnCanvasExtract = false, permission granted, canvas data != placeholderdata"
+      );
+    }
+  } else if (grantPermission === false) {
+    isnot(
+      canvasData,
+      contentWindow.kPlacedData,
+      "privacy.resistFingerprinting = true, permission denied, canvas data != placed data"
+    );
+    if (!randomDataOnCanvasExtract) {
+      is(
+        canvasData,
+        contentWindow.kPlaceholderData,
+        "privacy.resistFingerprinting = true and randomDataOnCanvasExtract = false, permission denied, canvas data == placeholderdata"
+      );
+    } else {
+      isnot(
+        canvasData,
+        contentWindow.kPlaceholderData,
+        "privacy.resistFingerprinting = true and randomDataOnCanvasExtract = true, permission denied, canvas data != placeholderdata"
+      );
+    }
   } else {
-    is(
+    isnot(
       canvasData,
-      contentWindow.kPlaceholderData,
-      "privacy.resistFingerprinting = true, requesting permission, canvas data == placeholderdata"
+      contentWindow.kPlacedData,
+      "privacy.resistFingerprinting = true, requesting permission, canvas data != placed data"
     );
+    if (!randomDataOnCanvasExtract) {
+      is(
+        canvasData,
+        contentWindow.kPlaceholderData,
+        "privacy.resistFingerprinting = true and randomDataOnCanvasExtract = false, requesting permission, canvas data == placeholderdata"
+      );
+    } else {
+      isnot(
+        canvasData,
+        contentWindow.kPlaceholderData,
+        "privacy.resistFingerprinting = true and randomDataOnCanvasExtract = true, requesting permission, canvas data != placeholderdata"
+      );
+    }
   }
 }
 
-async function withNewTabInput(grantPermission, browser) {
+async function withNewTabInput(
+  randomDataOnCanvasExtract,
+  grantPermission,
+  browser
+) {
   await SpecialPowers.spawn(browser, [], initTab);
-  await enableResistFingerprinting(true);
+  await enableResistFingerprinting(randomDataOnCanvasExtract, true);
   let popupShown = promisePopupShown();
   await SpecialPowers.spawn(browser, [], function(host) {
     E10SUtils.wrapHandlingUserInput(content, true, function() {
       var button = content.document.getElementById("clickme");
       button.click();
     });
   });
   await popupShown;
@@ -237,28 +350,34 @@ async function withNewTabInput(grantPerm
     is(testPermission(), Services.perms.ALLOW_ACTION, "permission granted");
   } else {
     triggerSecondaryCommand();
     await popupHidden;
     is(testPermission(), Services.perms.DENY_ACTION, "permission denied");
   }
   await SpecialPowers.spawn(
     browser,
-    [grantPermission],
+    [randomDataOnCanvasExtract, grantPermission],
     extractCanvasDataUserInput
   );
   await SpecialPowers.popPrefEnv();
 }
 
-async function doTestInput(grantPermission, autoDeclineNoInput) {
+async function doTestInput(
+  randomDataOnCanvasExtract,
+  grantPermission,
+  autoDeclineNoInput
+) {
   await BrowserTestUtils.withNewTab(
     kUrl,
-    withNewTabInput.bind(null, grantPermission)
+    withNewTabInput.bind(null, randomDataOnCanvasExtract, grantPermission)
   );
   Services.perms.removeFromPrincipal(kPrincipal, kPermission);
 }
 
 // With auto-declining enabled (the default)
 // Tests clicking "Don't Allow" button of the permission prompt.
-add_task(doTestInput.bind(null, false));
+add_task(doTestInput.bind(null, true, false));
+add_task(doTestInput.bind(null, false, false));
 
 // Tests clicking "Allow" button of the permission prompt.
-add_task(doTestInput.bind(null, true));
+add_task(doTestInput.bind(null, true, true));
+add_task(doTestInput.bind(null, false, true));
--- a/dom/base/ImageEncoder.cpp
+++ b/dom/base/ImageEncoder.cpp
@@ -1,16 +1,17 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* 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/. */
 
 #include "ImageEncoder.h"
 #include "mozilla/dom/CanvasRenderingContext2D.h"
+#include "mozilla/dom/GeneratePlaceholderCanvasData.h"
 #include "mozilla/dom/MemoryBlobImpl.h"
 #include "mozilla/dom/WorkerPrivate.h"
 #include "mozilla/gfx/2D.h"
 #include "mozilla/gfx/DataSurfaceHelpers.h"
 #include "mozilla/layers/AsyncCanvasRenderer.h"
 #include "mozilla/RefPtr.h"
 #include "mozilla/SyncRunnable.h"
 #include "mozilla/Unused.h"
@@ -378,18 +379,19 @@ nsresult ImageEncoder::ExtractDataIntern
       return NS_ERROR_INVALID_ARG;
     }
 
     DataSourceSurface::MappedSurface map;
     if (!emptyCanvas->Map(DataSourceSurface::MapType::WRITE, &map)) {
       return NS_ERROR_INVALID_ARG;
     }
     if (aUsePlaceholder) {
-      // If placeholder data was requested, return all-white, opaque image data.
-      memset(map.mData, 0xFF, 4 * aSize.width * aSize.height);
+      auto size = 4 * aSize.width * aSize.height;
+      auto* data = map.mData;
+      GeneratePlaceholderCanvasData(size, data);
     }
     rv = aEncoder->InitFromData(map.mData, aSize.width * aSize.height * 4,
                                 aSize.width, aSize.height, aSize.width * 4,
                                 imgIEncoder::INPUT_FORMAT_HOSTARGB, aOptions);
     emptyCanvas->Unmap();
     if (NS_SUCCEEDED(rv)) {
       imgStream = aEncoder;
     }
--- a/dom/canvas/CanvasRenderingContext2D.cpp
+++ b/dom/canvas/CanvasRenderingContext2D.cpp
@@ -12,16 +12,17 @@
 #include "SVGImageContext.h"
 
 #include "nsContentUtils.h"
 
 #include "mozilla/PresShell.h"
 #include "mozilla/PresShellInlines.h"
 #include "mozilla/dom/Document.h"
 #include "mozilla/dom/HTMLCanvasElement.h"
+#include "mozilla/dom/GeneratePlaceholderCanvasData.h"
 #include "SVGObserverUtils.h"
 #include "nsPresContext.h"
 
 #include "nsIInterfaceRequestorUtils.h"
 #include "nsIFrame.h"
 #include "nsError.h"
 
 #include "nsCSSPseudoElements.h"
@@ -5090,31 +5091,38 @@ nsresult CanvasRenderingContext2D::GetIm
   bool usePlaceholder = false;
   if (mCanvasElement) {
     nsCOMPtr<Document> ownerDoc = mCanvasElement->OwnerDoc();
     usePlaceholder = !CanvasUtils::IsImageExtractionAllowed(ownerDoc, aCx,
                                                             aSubjectPrincipal);
   }
 
   do {
+    uint8_t* randomData;
+    if (usePlaceholder) {
+      // Since we cannot call any GC-able functions (like requesting the RNG
+      // service) after we call JS_GetUint8ClampedArrayData, we will
+      // pre-generate the randomness required for GeneratePlaceholderCanvasData.
+      randomData = TryToGenerateRandomDataForPlaceholderCanvasData();
+    }
+
     JS::AutoCheckCannotGC nogc;
     bool isShared;
     uint8_t* data = JS_GetUint8ClampedArrayData(darray, &isShared, nogc);
     MOZ_ASSERT(!isShared);  // Should not happen, data was created above
 
+    if (usePlaceholder) {
+      FillPlaceholderCanvas(randomData, len.value(), data);
+      break;
+    }
+
     uint32_t srcStride = rawData.mStride;
     uint8_t* src =
         rawData.mData + srcReadRect.y * srcStride + srcReadRect.x * 4;
 
-    // Return all-white, opaque pixel data if no permission.
-    if (usePlaceholder) {
-      memset(data, 0xFF, len.value());
-      break;
-    }
-
     uint8_t* dst = data + dstWriteRect.y * (aWidth * 4) + dstWriteRect.x * 4;
 
     if (mOpaque) {
       SwizzleData(src, srcStride, SurfaceFormat::X8R8G8B8_UINT32, dst,
                   aWidth * 4, SurfaceFormat::R8G8B8A8, dstWriteRect.Size());
     } else {
       UnpremultiplyData(src, srcStride, SurfaceFormat::A8R8G8B8_UINT32, dst,
                         aWidth * 4, SurfaceFormat::R8G8B8A8,
new file mode 100644
--- /dev/null
+++ b/dom/canvas/GeneratePlaceholderCanvasData.h
@@ -0,0 +1,99 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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_GeneratePlaceholderCanvasData_h
+#define mozilla_dom_GeneratePlaceholderCanvasData_h
+
+#include "mozilla/StaticPrefs_privacy.h"
+#include "nsCOMPtr.h"
+#include "nsIRandomGenerator.h"
+#include "nsServiceManagerUtils.h"
+
+#define RANDOM_BYTES_TO_SAMPLE 32
+
+namespace mozilla {
+namespace dom {
+
+/**
+ * When privacy.resistFingerprinting.randomDataOnCanvasExtract is true, tries
+ * to generate random data for placeholder canvas by sampling
+ * RANDOM_BYTES_TO_SAMPLE bytes and then returning it. If this fails or if
+ * privacy.resistFingerprinting.randomDataOnCanvasExtract is false, returns
+ * nullptr.
+ *
+ * @return uint8_t*  output buffer
+ */
+inline uint8_t* TryToGenerateRandomDataForPlaceholderCanvasData() {
+  if (!StaticPrefs::privacy_resistFingerprinting_randomDataOnCanvasExtract()) {
+    return nullptr;
+  }
+  nsresult rv;
+  nsCOMPtr<nsIRandomGenerator> rg =
+      do_GetService("@mozilla.org/security/random-generator;1", &rv);
+  if (NS_FAILED(rv)) {
+    return nullptr;
+  }
+  uint8_t* randomData;
+  rv = rg->GenerateRandomBytes(RANDOM_BYTES_TO_SAMPLE, &randomData);
+  if (NS_FAILED(rv)) {
+    return nullptr;
+  }
+  return randomData;
+}
+
+/**
+ * If randomData not nullptr, repeats those bytes many times to fill buffer. If
+ * randomData is nullptr, returns all-white pixel data.
+ *
+ * @param[in]   randomData  Buffer of RANDOM_BYTES_TO_SAMPLE bytes of random
+ *                          data, or nullptr
+ * @param[in]   size        Size of output buffer
+ * @param[out]  buffer      Output buffer
+ */
+inline void FillPlaceholderCanvas(uint8_t* randomData, uint32_t size,
+                                  uint8_t* buffer) {
+  if (!randomData) {
+    memset(buffer, 0xFF, size);
+    return;
+  }
+  auto remaining_to_fill = size;
+  auto index = 0;
+  while (remaining_to_fill > 0) {
+    auto bytes_to_write = (remaining_to_fill > RANDOM_BYTES_TO_SAMPLE)
+                              ? RANDOM_BYTES_TO_SAMPLE
+                              : remaining_to_fill;
+    memcpy(buffer + (index * RANDOM_BYTES_TO_SAMPLE), randomData,
+           bytes_to_write);
+    remaining_to_fill -= bytes_to_write;
+    index++;
+  }
+  free(randomData);
+}
+
+/**
+ * When privacy.resistFingerprinting.randomDataOnCanvasExtract is true, tries
+ * to generate random canvas data by sampling RANDOM_BYTES_TO_SAMPLE bytes and
+ * then repeating those bytes many times to fill the buffer. If this fails or
+ * if privacy.resistFingerprinting.randomDataOnCanvasExtract is false, returns
+ * all-white, opaque pixel data.
+ *
+ * It is recommended that callers call this function instead of individually
+ * calling TryToGenerateRandomDataForPlaceholderCanvasData and
+ * FillPlaceholderCanvas unless there are requirements, like NoGC, that prevent
+ * them from calling the RNG service inside this function.
+ *
+ * @param[in]   size    Size of output buffer
+ * @param[out]  buffer  Output buffer
+ */
+inline void GeneratePlaceholderCanvasData(uint32_t size, uint8_t* buffer) {
+  uint8_t* randomData = TryToGenerateRandomDataForPlaceholderCanvasData();
+  FillPlaceholderCanvas(randomData, size, buffer);
+}
+
+}  // namespace dom
+}  // namespace mozilla
+
+#endif  // mozilla_dom_GeneratePlaceholderCanvasData_h
--- a/dom/canvas/moz.build
+++ b/dom/canvas/moz.build
@@ -48,16 +48,17 @@ EXPORTS += [
 EXPORTS.mozilla.dom += [
     'BasicRenderingContext2D.h',
     'CanvasGradient.h',
     'CanvasPath.h',
     'CanvasPattern.h',
     'CanvasRenderingContext2D.h',
     'CanvasRenderingContextHelper.h',
     'CanvasUtils.h',
+    'GeneratePlaceholderCanvasData.h',
     'ImageBitmap.h',
     'ImageBitmapRenderingContext.h',
     'ImageBitmapSource.h',
     'ImageData.h',
     'ImageUtils.h',
     'IpdlQueue.h',
     'OffscreenCanvas.h',
     'ProducerConsumerQueue.h',
--- a/dom/html/HTMLCanvasElement.cpp
+++ b/dom/html/HTMLCanvasElement.cpp
@@ -13,16 +13,17 @@
 #include "Layers.h"
 #include "MediaTrackGraph.h"
 #include "mozilla/Assertions.h"
 #include "mozilla/Base64.h"
 #include "mozilla/BasePrincipal.h"
 #include "mozilla/CheckedInt.h"
 #include "mozilla/dom/CanvasCaptureMediaStream.h"
 #include "mozilla/dom/CanvasRenderingContext2D.h"
+#include "mozilla/dom/GeneratePlaceholderCanvasData.h"
 #include "mozilla/dom/Event.h"
 #include "mozilla/dom/File.h"
 #include "mozilla/dom/HTMLCanvasElementBinding.h"
 #include "mozilla/dom/VideoStreamTrack.h"
 #include "mozilla/dom/MouseEvent.h"
 #include "mozilla/dom/OffscreenCanvas.h"
 #include "mozilla/EventDispatcher.h"
 #include "mozilla/gfx/Rect.h"
@@ -95,18 +96,19 @@ class RequestedFrameRefreshObserver : pu
       return nullptr;
     }
 
     MOZ_ASSERT(read.GetStride() == write.GetStride());
     MOZ_ASSERT(data->GetSize() == copy->GetSize());
     MOZ_ASSERT(data->GetFormat() == copy->GetFormat());
 
     if (aReturnPlaceholderData) {
-      // If returning placeholder data, fill the frame copy with white pixels.
-      memset(write.GetData(), 0xFF, write.GetStride() * copy->GetSize().height);
+      auto size = write.GetStride() * copy->GetSize().height;
+      auto* data = write.GetData();
+      GeneratePlaceholderCanvasData(size, data);
     } else {
       memcpy(write.GetData(), read.GetData(),
              write.GetStride() * copy->GetSize().height);
     }
 
     return copy.forget();
   }
 
--- a/modules/libpref/init/StaticPrefList.yaml
+++ b/modules/libpref/init/StaticPrefList.yaml
@@ -8171,16 +8171,23 @@
 # revert this behavior with this obscure pref. We do not intend to support this
 # long term. If you do set it, to work around some broken website, please file
 # a bug with information so we can understand why it is needed.
 - name: privacy.resistFingerprinting.autoDeclineNoUserInputCanvasPrompts
   type: bool
   value: true
   mirror: always
 
+# Whether canvas extraction should result in random data. If false, canvas
+# extraction results in all-white, opaque pixel data.
+- name: privacy.resistFingerprinting.randomDataOnCanvasExtract
+  type: bool
+  value: true
+  mirror: always
+
 # The log level for browser console messages logged in RFPHelper.jsm. Change to
 # 'All' and restart to see the messages.
 - name: privacy.resistFingerprinting.jsmloglevel
   type: String
   value: "Warn"
   mirror: never
 
 # Enable jittering the clock one precision value forward.