commit f81e4cc78e3e5610d9780384ea0d769dd41ed9de
parent 45c0ce6bb70ce393cf410cb51a182c594c29d357
Author: Tom Ritter <tom@mozilla.com>
Date: Tue, 9 Dec 2025 16:35:01 +0000
Bug 1873716: Add utilities for identifying canvas fingerprinters in an automated fashion (crawling) r=timhuang
SKIP_BMO_CHECK
Differential Revision: https://phabricator.services.mozilla.com/D273666
Diffstat:
7 files changed, 160 insertions(+), 47 deletions(-)
diff --git a/dom/canvas/CanvasRenderingContext2D.cpp b/dom/canvas/CanvasRenderingContext2D.cpp
@@ -2279,12 +2279,18 @@ UniquePtr<uint8_t[]> CanvasRenderingContext2D::GetImageBuffer(
mBufferProvider->ReturnSnapshot(snapshot.forget());
- if (ret && aExtractionBehavior == CanvasUtils::ImageExtraction::Randomize) {
- nsRFPService::RandomizePixels(
- GetCookieJarSettings(), PrincipalOrNull(), ret.get(),
- out_imageSize->width, out_imageSize->height,
- out_imageSize->width * out_imageSize->height * 4,
- SurfaceFormat::A8R8G8B8_UINT32);
+ if (ret) {
+ nsRFPService::PotentiallyDumpImage(
+ PrincipalOrNull(), ret.get(), out_imageSize->width,
+ out_imageSize->height,
+ out_imageSize->width * out_imageSize->height * 4);
+ if (aExtractionBehavior == CanvasUtils::ImageExtraction::Randomize) {
+ nsRFPService::RandomizePixels(
+ GetCookieJarSettings(), PrincipalOrNull(), ret.get(),
+ out_imageSize->width, out_imageSize->height,
+ out_imageSize->width * out_imageSize->height * 4,
+ SurfaceFormat::A8R8G8B8_UINT32);
+ }
}
return ret;
@@ -6612,6 +6618,10 @@ nsresult CanvasRenderingContext2D::GetImageDataArray(
do {
uint8_t* randomData;
+ const IntSize size = readback->GetSize();
+ nsRFPService::PotentiallyDumpImage(PrincipalOrNull(), rawData.mData,
+ size.width, size.height,
+ size.height * size.width * 4);
if (extractionBehavior == CanvasUtils::ImageExtraction::Placeholder) {
// Since we cannot call any GC-able functions (like requesting the RNG
// service) after we call JS_GetUint8ClampedArrayData, we will
@@ -6622,7 +6632,6 @@ nsresult CanvasRenderingContext2D::GetImageDataArray(
// need to calculate random noises if we are going to use the place
// holder.
- const IntSize size = readback->GetSize();
nsRFPService::RandomizePixels(GetCookieJarSettings(), PrincipalOrNull(),
rawData.mData, size.width, size.height,
size.height * size.width * 4,
diff --git a/dom/canvas/ClientWebGLContext.cpp b/dom/canvas/ClientWebGLContext.cpp
@@ -1358,6 +1358,7 @@ UniquePtr<uint8_t[]> ClientWebGLContext::GetImageBuffer(
const auto& premultAlpha = notLost->info.options.premultipliedAlpha;
*out_imageSize = dataSurface->GetSize();
+ nsRFPService::PotentiallyDumpImage(PrincipalOrNull(), dataSurface);
if (aExtractionBehavior == CanvasUtils::ImageExtraction::Randomize) {
return gfxUtils::GetImageBufferWithRandomNoise(
dataSurface, premultAlpha, GetCookieJarSettings(), PrincipalOrNull(),
@@ -1385,6 +1386,7 @@ ClientWebGLContext::GetInputStream(
RefPtr<gfx::DataSourceSurface> dataSurface = snapshot->GetDataSurface();
const auto& premultAlpha = notLost->info.options.premultipliedAlpha;
+ nsRFPService::PotentiallyDumpImage(PrincipalOrNull(), dataSurface);
if (ShouldResistFingerprinting(RFPTarget::CanvasRandomization)) {
return gfxUtils::GetInputStreamWithRandomNoise(
dataSurface, premultAlpha, mimeType, encoderOptions,
diff --git a/dom/canvas/ImageBitmapRenderingContext.cpp b/dom/canvas/ImageBitmapRenderingContext.cpp
@@ -195,12 +195,18 @@ mozilla::UniquePtr<uint8_t[]> ImageBitmapRenderingContext::GetImageBuffer(
UniquePtr<uint8_t[]> ret = gfx::SurfaceToPackedBGRA(data);
- if (ret && ShouldResistFingerprinting(RFPTarget::CanvasRandomization)) {
- nsRFPService::RandomizePixels(
- GetCookieJarSettings(), PrincipalOrNull(), ret.get(),
- data->GetSize().width, data->GetSize().height,
- data->GetSize().width * data->GetSize().height * 4,
- gfx::SurfaceFormat::A8R8G8B8_UINT32);
+ if (ret) {
+ nsRFPService::PotentiallyDumpImage(
+ PrincipalOrNull(), ret.get(), data->GetSize().width,
+ data->GetSize().height,
+ data->GetSize().width * data->GetSize().height * 4);
+ if (ShouldResistFingerprinting(RFPTarget::CanvasRandomization)) {
+ nsRFPService::RandomizePixels(
+ GetCookieJarSettings(), PrincipalOrNull(), ret.get(),
+ data->GetSize().width, data->GetSize().height,
+ data->GetSize().width * data->GetSize().height * 4,
+ gfx::SurfaceFormat::A8R8G8B8_UINT32);
+ }
}
return ret;
}
diff --git a/dom/canvas/OffscreenCanvasDisplayHelper.cpp b/dom/canvas/OffscreenCanvasDisplayHelper.cpp
@@ -591,21 +591,24 @@ UniquePtr<uint8_t[]> OffscreenCanvasDisplayHelper::GetImageBuffer(
return nullptr;
}
- if (aExtractionBehavior == CanvasUtils::ImageExtraction::Randomize) {
- nsIPrincipal* principal = nullptr;
- nsICookieJarSettings* cookieJarSettings = nullptr;
- {
- // This function is never called with mOffscreenCanvas set, so we skip
- // the check for it.
- MutexAutoLock lock(mMutex);
- MOZ_ASSERT(!mOffscreenCanvas);
-
- if (mCanvasElement) {
- principal = mCanvasElement->NodePrincipal();
- cookieJarSettings = mCanvasElement->OwnerDoc()->CookieJarSettings();
- }
- }
+ nsIPrincipal* principal = nullptr;
+ nsICookieJarSettings* cookieJarSettings = nullptr;
+ {
+ // This function is never called with mOffscreenCanvas set, so we skip
+ // the check for it.
+ MutexAutoLock lock(mMutex);
+ MOZ_ASSERT(!mOffscreenCanvas);
+ if (mCanvasElement) {
+ principal = mCanvasElement->NodePrincipal();
+ cookieJarSettings = mCanvasElement->OwnerDoc()->CookieJarSettings();
+ }
+ }
+ nsRFPService::PotentiallyDumpImage(
+ principal, imageBuffer.get(), dataSurface->GetSize().width,
+ dataSurface->GetSize().height,
+ dataSurface->GetSize().width * dataSurface->GetSize().height * 4);
+ if (aExtractionBehavior == CanvasUtils::ImageExtraction::Randomize) {
nsRFPService::RandomizePixels(
cookieJarSettings, principal, imageBuffer.get(),
dataSurface->GetSize().width, dataSurface->GetSize().height,
diff --git a/dom/webgpu/CanvasContext.cpp b/dom/webgpu/CanvasContext.cpp
@@ -337,6 +337,7 @@ mozilla::UniquePtr<uint8_t[]> CanvasContext::GetImageBuffer(
RefPtr<gfx::DataSourceSurface> dataSurface = snapshot->GetDataSurface();
*out_imageSize = dataSurface->GetSize();
+ nsRFPService::PotentiallyDumpImage(PrincipalOrNull(), dataSurface);
if (ShouldResistFingerprinting(RFPTarget::CanvasRandomization)) {
gfxUtils::GetImageBufferWithRandomNoise(dataSurface,
/* aIsAlphaPremultiplied */ true,
@@ -360,6 +361,7 @@ NS_IMETHODIMP CanvasContext::GetInputStream(
RefPtr<gfx::DataSourceSurface> dataSurface = snapshot->GetDataSurface();
+ nsRFPService::PotentiallyDumpImage(PrincipalOrNull(), dataSurface);
if (ShouldResistFingerprinting(RFPTarget::CanvasRandomization)) {
return gfxUtils::GetInputStreamWithRandomNoise(
dataSurface, /* aIsAlphaPremultiplied */ true, aMimeType,
diff --git a/toolkit/components/resistfingerprinting/nsRFPService.cpp b/toolkit/components/resistfingerprinting/nsRFPService.cpp
@@ -45,6 +45,7 @@
#include "mozilla/dom/BrowsingContext.h"
#include "mozilla/dom/CanvasRenderingContextHelper.h"
#include "mozilla/dom/CanvasRenderingContext2D.h"
+#include "mozilla/dom/CanvasUtils.h"
#include "mozilla/dom/CanonicalBrowsingContext.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/Element.h"
@@ -52,9 +53,9 @@
#include "mozilla/dom/WindowGlobalParent.h"
#include "mozilla/dom/MediaDeviceInfoBinding.h"
#include "mozilla/dom/quota/QuotaManager.h"
+#include "mozilla/gfx/2D.h"
#include "mozilla/intl/LocaleService.h"
#include "mozilla/XorShift128PlusRNG.h"
-#include "mozilla/dom/CanvasUtils.h"
#include "nsAboutProtocolUtils.h"
#include "nsBaseHashtable.h"
@@ -1730,16 +1731,66 @@ nsresult nsRFPService::GenerateCanvasKeyFromImageData(
}
// static
-nsresult nsRFPService::RandomizePixels(nsICookieJarSettings* aCookieJarSettings,
- nsIPrincipal* aPrincipal, uint8_t* aData,
- uint32_t aWidth, uint32_t aHeight,
- uint32_t aSize,
- gfx::SurfaceFormat aSurfaceFormat) {
-#ifdef __clang__
-# pragma clang diagnostic push
-# pragma clang diagnostic ignored "-Wunreachable-code"
-#endif
- if (false) {
+void nsRFPService::PotentiallyDumpImage(nsIPrincipal* aPrincipal,
+ gfx::DataSourceSurface* aSurface) {
+ // Only dump from the content process to avoid unintended file writes from
+ // privileged or background processes.
+ if (XRE_GetProcessType() != GeckoProcessType_Content) {
+ return;
+ }
+ if (MOZ_LOG_TEST(gFingerprinterDetection, LogLevel::Debug)) {
+ int32_t format = 0;
+ UniquePtr<uint8_t[]> imageBuffer =
+ gfxUtils::GetImageBuffer(aSurface, true, &format);
+ nsRFPService::PotentiallyDumpImage(
+ aPrincipal, imageBuffer.get(), aSurface->GetSize().width,
+ aSurface->GetSize().height,
+ aSurface->GetSize().width * aSurface->GetSize().height * 4);
+ }
+}
+
+void nsRFPService::PotentiallyDumpImage(nsIPrincipal* aPrincipal,
+ uint8_t* aData, uint32_t aWidth,
+ uint32_t aHeight, uint32_t aSize) {
+ // Only dump from the content process to avoid unintended file writes from
+ // privileged or background processes.
+ if (XRE_GetProcessType() != GeckoProcessType_Content) {
+ return;
+ }
+ nsAutoCString safeSite;
+
+ if (MOZ_LOG_TEST(gFingerprinterDetection, LogLevel::Debug)) {
+ nsAutoCString site;
+ if (aPrincipal) {
+ nsCOMPtr<nsIURI> uri = aPrincipal->GetURI();
+ if (uri) {
+ site.Assign(uri->GetSpecOrDefault());
+ }
+ }
+ if (site.IsEmpty()) {
+ site.AssignLiteral("unknown");
+ }
+
+ safeSite.SetCapacity(site.Length());
+ // Build a sanitized site string from the principal's URI for filename.
+ // Allow alnum, '.', '_', '-', replace others with '_'.
+ for (uint32_t i = 0; i < site.Length() && safeSite.Length() < 80; ++i) {
+ char c = site[i];
+ if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
+ (c >= '0' && c <= '9') || c == '.' || c == '_' || c == '-') {
+ safeSite.Append(c);
+ } else {
+ safeSite.Append('_');
+ }
+ }
+
+ MOZ_LOG(gFingerprinterDetection, LogLevel::Debug,
+ ("Would dump a canvas image from %s width: %i height: %i bytes: %i",
+ site.get(), aWidth, aHeight, aSize));
+ }
+
+ if (MOZ_LOG_TEST(gFingerprinterDetection, LogLevel::Verbose)) {
+ // Requires sandbox level 0
// For debugging purposes you can dump the image with this code
// then convert it with the image-magick command
// convert -size WxH -depth 8 rgba:$i $i.png
@@ -1747,17 +1798,47 @@ nsresult nsRFPService::RandomizePixels(nsICookieJarSettings* aCookieJarSettings,
// up...
static int calls = 0;
char filename[256];
- SprintfLiteral(filename, "rendered_image_%dx%d_%d_pre", aWidth, aHeight,
- calls);
- FILE* outputFile = fopen(filename, "wb"); // "wb" for binary write mode
- fwrite(aData, 1, aSize, outputFile);
- fclose(outputFile);
- calls++;
- }
-#ifdef __clang__
-# pragma clang diagnostic pop
+ SprintfLiteral(filename, "rendered_image_%s__%dx%d_%d", safeSite.get(),
+ aWidth, aHeight, calls);
+
+ const char* logEnv = PR_GetEnv("MOZ_LOG_FILE");
+#ifdef XP_WIN
+ const char sep = '\\';
+#else
+ const char sep = '/';
#endif
+ const char* dirEnd = nullptr;
+ if (logEnv) {
+ for (const char* it = logEnv; *it; ++it) {
+ if (*it == sep) {
+ dirEnd = it;
+ }
+ }
+ }
+
+ char outPath[512];
+ if (dirEnd) {
+ int dirLen = int(dirEnd - logEnv + 1);
+ SprintfLiteral(outPath, "%.*s%s", dirLen, logEnv, filename);
+ } else {
+ SprintfLiteral(outPath, "%s", filename);
+ }
+
+ FILE* outputFile = fopen(outPath, "wb");
+ if (outputFile) {
+ fwrite(aData, 1, aSize, outputFile);
+ fclose(outputFile);
+ calls++;
+ }
+ }
+}
+// static
+nsresult nsRFPService::RandomizePixels(nsICookieJarSettings* aCookieJarSettings,
+ nsIPrincipal* aPrincipal, uint8_t* aData,
+ uint32_t aWidth, uint32_t aHeight,
+ uint32_t aSize,
+ gfx::SurfaceFormat aSurfaceFormat) {
constexpr uint8_t bytesPerChannel = 1;
constexpr uint8_t bytesPerPixel = 4 * bytesPerChannel;
diff --git a/toolkit/components/resistfingerprinting/nsRFPService.h b/toolkit/components/resistfingerprinting/nsRFPService.h
@@ -23,6 +23,7 @@
#include "nsISupports.h"
#include "nsIRFPService.h"
#include "nsStringFwd.h"
+#include <queue>
// Defines regarding spoofed values of Navigator object. These spoofed values
// are returned when 'privacy.resistFingerprinting' is true.
@@ -79,6 +80,9 @@ namespace dom {
class Document;
enum class CanvasContextType : uint8_t;
} // namespace dom
+namespace gfx {
+class DataSourceSurface;
+} // namespace gfx
enum KeyboardLang { EN = 0x01 };
@@ -510,6 +514,12 @@ class nsRFPService final : public nsIObserver, public nsIRFPService {
nsIURI* aFirstPartyURI, nsIPrincipal* aPrincipal,
bool aForeignByAncestorContext);
+ static void PotentiallyDumpImage(nsIPrincipal* aPrincipal,
+ gfx::DataSourceSurface* aSurface);
+ static void PotentiallyDumpImage(nsIPrincipal* aPrincipal, uint8_t* aData,
+ uint32_t aWidth, uint32_t aHeight,
+ uint32_t aSize);
+
// This function is plumbed to RandomizeElements function.
static nsresult RandomizePixels(nsICookieJarSettings* aCookieJarSettings,
nsIPrincipal* aPrincipal, uint8_t* aData,