tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 0ca707cca577868fc2e8a20384300937dad9581d
parent 98b91beb72b63447b9ca1d9aab3fc6862b18ddd0
Author: Tom Ritter <tom@mozilla.com>
Date:   Mon,  8 Dec 2025 16:31:03 +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:
Mdom/canvas/CanvasRenderingContext2D.cpp | 23++++++++++++++++-------
Mdom/canvas/ClientWebGLContext.cpp | 2++
Mdom/canvas/ImageBitmapRenderingContext.cpp | 18++++++++++++------
Mdom/canvas/OffscreenCanvasDisplayHelper.cpp | 31+++++++++++++++++--------------
Mdom/webgpu/CanvasContext.cpp | 2++
Mtoolkit/components/resistfingerprinting/nsRFPService.cpp | 121++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Mtoolkit/components/resistfingerprinting/nsRFPService.h | 10++++++++++
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,