diff --git a/README.md b/README.md index 98e8be2..f517187 100644 --- a/README.md +++ b/README.md @@ -447,7 +447,7 @@ Below is a list of patches and features implemented in Camoufox. - Automatically uses the correct system fonts for your User Agent - Bundled with Windows, Mac, and Linux system fonts -- Letter-spacing noise is available (`fonts:spacing_seed`) but off by default, because no real machine produces it +- No glyph-spacing noise: measured text widths are the ones the same font gives on a real machine ### Playwright support diff --git a/build-tester/scripts/generate-presets.py b/build-tester/scripts/generate-presets.py index 9c14947..2aaf072 100644 --- a/build-tester/scripts/generate-presets.py +++ b/build-tester/scripts/generate-presets.py @@ -29,7 +29,6 @@ def convert_preset(ctx): }, 'camouConfig': config, 'profileConfig': { - 'fontSpacingSeed': config.get('fonts:spacing_seed', 0), 'audioSeed': config.get('audio:seed', 0), 'screenWidth': screen.get('width', 1920), 'screenHeight': screen.get('height', 1080), diff --git a/build-tester/scripts/presets.py b/build-tester/scripts/presets.py index 4a39d10..f00ae72 100644 --- a/build-tester/scripts/presets.py +++ b/build-tester/scripts/presets.py @@ -30,7 +30,6 @@ def convert_preset(ctx: dict) -> dict: }, "camouConfig": config, "profileConfig": { - "fontSpacingSeed": config.get("fonts:spacing_seed", 0), "audioSeed": config.get("audio:seed", 0), "screenWidth": screen.get("width", 1920), "screenHeight": screen.get("height", 1080), diff --git a/build-tester/src/lib/checks/index.ts b/build-tester/src/lib/checks/index.ts index 0d61e9f..56705d0 100644 --- a/build-tester/src/lib/checks/index.ts +++ b/build-tester/src/lib/checks/index.ts @@ -7,7 +7,6 @@ export interface PhaseResult { } const SELF_DESTRUCT_FUNCTIONS = [ - "setFontSpacingSeed", "setAudioFingerprintSeed", "setTimezone", "setScreenDimensions", diff --git a/build-tester/src/lib/types.ts b/build-tester/src/lib/types.ts index 99346d3..0564e2a 100644 --- a/build-tester/src/lib/types.ts +++ b/build-tester/src/lib/types.ts @@ -95,7 +95,6 @@ export interface ProfileConfig { webglVendor: string; webglRenderer: string; audioSeed: number; - fontSpacingSeed: number; fontList: string[]; speechVoices?: string[]; } diff --git a/ci/tribal-rules.yml b/ci/tribal-rules.yml index 48288ee..ed8f85f 100644 --- a/ci/tribal-rules.yml +++ b/ci/tribal-rules.yml @@ -159,6 +159,21 @@ rules: # Measurement # ------------------------------------------------------------------------- + - id: no-glyph-spacing-noise + title: Text is shaped exactly as stock Firefox shapes it; there is no spacing seed + check: automated + evidence: + - "#779 (6daae88): measured +1 px per ~100 glyphs and fractional deltas on every measureText; defaulted the seed to 0" + - "issue #741 / ad697a8: the perturbation detached combining marks in Thai, Lao, Arabic, Devanagari and Hebrew" + rationale: >- + The feature added a seeded amount to every glyph advance so that text + widths differed per context. No real machine produces those widths: + the same font on the same OS measures the same everywhere, so a width + that matches no real installation is a fingerprint, not a disguise. It + was defaulted off in #779 and kept as an opt-in, but an opt-in whose + only effect is to become detectable is not a feature, so the manager, + the window setter, the shaper hook and the config key are gone. + - id: canvas-is-not-noised title: The canvas is rendered, not noised, and there is no canvas seed check: automated diff --git a/docs/patch-upgrading-guide.md b/docs/patch-upgrading-guide.md index 33a10ee..c8e86d9 100644 --- a/docs/patch-upgrading-guide.md +++ b/docs/patch-upgrading-guide.md @@ -37,7 +37,7 @@ are listed in [`patches/patch-dependencies.md`](../patches/patch-dependencies.md ### Key Infrastructure Files - **RoverfoxStorageManager.cpp/h**: Thread-safe key-value storage for per-context data -- **Manager Classes**: FontSpacingSeedManager, WebRTCIPManager, etc. +- **Manager Classes**: AudioFingerprintManager, WebRTCIPManager, etc. - **Window.webidl**: Exposes the per-context setters to Playwright --- @@ -302,14 +302,14 @@ Simply apply the patch manually at the correct line number. The code hasn't chan Most spoofing patches carry per-context support. When porting one, expect these pieces: -1. **Manager classes** (e.g., FontSpacingSeedManager, WebRTCIPManager): +1. **Manager classes** (e.g., AudioFingerprintManager, WebRTCIPManager): - Store per-context settings using RoverfoxStorageManager - Provide WebIDL-compatible enable/disable checks - Handle self-destructing functions 2. **Window.webidl functions**: - JavaScript APIs exposed to Playwright - - Examples: `setFontSpacingSeed()`, `setWebRTCIPv4()` + - Examples: `setAudioFingerprintSeed()`, `setWebRTCIPv4()` 3. **nsGlobalWindowInner.cpp implementations**: - Extract userContextId from window/document/docshell diff --git a/docs/per-context-patches.md b/docs/per-context-patches.md index 277016f..e5f267a 100644 --- a/docs/per-context-patches.md +++ b/docs/per-context-patches.md @@ -5,7 +5,7 @@ Camoufox spoofs fingerprints globally via `CAMOU_CONFIG` — every browser conte ### The Patches **Per-context patches (with a `window.setXxx()` API):** -- `anti-font-fingerprinting.patch` — per-context `measureText()` spacing seed; also adds `RoverfoxStorageManager` (the shared per-context store) and puts the userContextId in `WordCacheKey` so the glyph cache never serves one context's result to another +- `anti-font-fingerprinting.patch` — adds `RoverfoxStorageManager` (the shared per-context store) and gives each font group its context's userContextId, which `font-list-spoofing.patch` uses to pick that context's font list - `audio-fingerprint-manager.patch` — per-context audio fingerprint seeding (all 6 AudioBuffer + AnalyserNode methods) - `timezone-spoofing.patch` — true per-realm timezone isolation via SpiderMonkey DateTimeInfo - `screen-spoofing.patch` — per-context screen dimensions and color depth via `ScreenDimensionManager` @@ -25,7 +25,6 @@ output as the GPU and fonts produce it. | Function | Patch | What it controls | |----------|-------|-----------------| -| `window.setFontSpacingSeed(seed)` | `anti-font-fingerprinting.patch` | Canvas `measureText()` letter spacing | | `window.setAudioFingerprintSeed(seed)` | `audio-fingerprint-manager.patch` | Audio buffer/analyser fingerprint hash | | `window.setTimezone(tz)` | `timezone-spoofing.patch` | `Date`, `Intl.DateTimeFormat`, all time APIs | | `window.setScreenDimensions(w, h)` | `screen-spoofing.patch` | `screen.width`, `screen.height` | @@ -64,9 +63,6 @@ const context = await browser.newContext({ await context.addInitScript((values) => { const w = window; - if (typeof w.setFontSpacingSeed === 'function') { - w.setFontSpacingSeed(values.fontSpacingSeed); - } if (typeof w.setAudioFingerprintSeed === 'function') { w.setAudioFingerprintSeed(values.audioFingerprintSeed); } @@ -107,7 +103,6 @@ await context.addInitScript((values) => { w.setSpeechVoices(values.speechVoices); } }, { - fontSpacingSeed: 12345678, audioFingerprintSeed: 87654321, timezone: 'America/New_York', screenWidth: 1920, @@ -232,23 +227,16 @@ The `camoufox.cfg` file sets Firefox preferences at startup (before `prefs.js` i ### 1. anti-font-fingerprinting.patch -**Controls:** Canvas `measureText()` letter spacing — makes text width measurements unique per context. The Python library sets the seed to 0 (off) by default: perturbed widths are something no stock Firefox produces. Pass `fonts:spacing_seed` to opt in. +**Controls:** nothing a page can see by itself. It is the groundwork the other per-context patches build on. -**How it works:** Stores a seed per context, then applies a deterministic spacing transformation in HarfBuzz (the text shaping engine). The seed is propagated through the entire text rendering pipeline: `nsTextFrame` → `gfxFont` → `gfxTextRun` → `gfxHarfBuzzShaper`. +**Provides:** +- `RoverfoxStorageManager`, the shared storage layer used by all other per-context patches. See the [Cross-Process Storage](#cross-process-storage-cross-process-storagepatch) section for how it works across processes. +- The userContextId on each `gfxFontGroup`, read from the document's `BrowsingContext` through a `GetDocument()` hook on `FontVisibilityProvider`. `font-list-spoofing.patch` uses it to apply that context's font list. -The transformation adds ~0.0-0.1 em of extra spacing using a Linear Congruential Generator seeded with the profile's value. Same seed always produces the same spacing. +Text is shaped exactly as stock Firefox shapes it. An earlier glyph-spacing seed was removed because the widths it produced match no real installation (`ci/tribal-rules.yml`: `no-glyph-spacing-noise`). -**Also provides:** `RoverfoxStorageManager` — the shared storage layer used by all other per-context patches. See the [Cross-Process Storage](#cross-process-storage-cross-process-storagepatch) section for how it works across processes. - -**WordCacheKey fix:** Added `mUserContextId` to the `WordCacheKey` struct in `gfxFont.h`. Without this, Firefox's shaped word cache shared results across contexts — context 1's font spacing result would be returned for context 2 (a cache hit based on text content alone). The fix adds `mUserContextId` to both constructors, the hash computation (via `* 0x1000000`), and the `match()` comparison, ensuring each context has its own cache entries. Also adds `GetUserContextId()` virtual method to `gfxShapedText` and `gfxShapedWord` so the context ID propagates through the text run pipeline. - -**API:** -```javascript -window.setFontSpacingSeed(12345678); // uint32 seed -``` - -**New C++ files:** `FontSpacingSeedManager.h/cpp`, `RoverfoxStorageManager.h/cpp` -**Modified Firefox files (22):** `nsGlobalWindowInner.cpp/h`, `CanvasRenderingContext2D.cpp`, `OffscreenCanvas.cpp`, `WorkerPrivate.h`, `Window.webidl`, `moz.build` (dom/base), `gfxHarfBuzzShaper.cpp`, `gfxTextRun.cpp/h`, `gfxFont.cpp/h`, `nsFontMetrics.cpp/h`, `nsLayoutUtils.cpp/h`, `nsPresContext.cpp`, `nsTextFrame.cpp`, `MathMLTextRunFactory.cpp`, `nsTextRunTransformations.cpp`, `nsMathMLChar.cpp`, `FontVisibilityProvider.h` +**New C++ files:** `RoverfoxStorageManager.h/cpp` +**Modified Firefox files:** `moz.build` (dom/base), `nsGlobalWindowInner.cpp`, `OffscreenCanvas.cpp`, `WorkerPrivate.h`, `gfxPlatformFontList.cpp`, `gfxTextRun.cpp/h`, `nsPresContext.cpp`, `FontVisibilityProvider.h` --- @@ -535,7 +523,7 @@ For per-context geolocation, use Playwright's built-in `context.setGeolocation() ## Build Notes -**SOURCES vs UNIFIED_SOURCES:** Most new `.cpp` manager files use `SOURCES` (separate compilation) in `moz.build` to avoid namespace pollution (`mozilla::dom::mozilla::dom::`) that occurs when files including `RoverfoxStorageManager.h` are concatenated in unified builds. Currently in `SOURCES`: `AudioFingerprintManager.cpp`, `WebRTCIPManager.cpp`, `NavigatorManager.cpp`, `WebGLParamsManager.cpp`, `FontListManager.cpp`, `SpeechVoicesManager.cpp`, `ScreenDimensionManager.cpp`. Three files use `UNIFIED_SOURCES` and compile without namespace issues in their alphabetical position: `FontSpacingSeedManager.cpp`, `RoverfoxStorageManager.cpp` (both from `anti-font-fingerprinting.patch`) and `TimezoneManager.cpp` (from `timezone-spoofing.patch`). +**SOURCES vs UNIFIED_SOURCES:** Most new `.cpp` manager files use `SOURCES` (separate compilation) in `moz.build` to avoid namespace pollution (`mozilla::dom::mozilla::dom::`) that occurs when files including `RoverfoxStorageManager.h` are concatenated in unified builds. Currently in `SOURCES`: `AudioFingerprintManager.cpp`, `WebRTCIPManager.cpp`, `NavigatorManager.cpp`, `WebGLParamsManager.cpp`, `FontListManager.cpp`, `SpeechVoicesManager.cpp`, `ScreenDimensionManager.cpp`. Two files use `UNIFIED_SOURCES` and compile without namespace issues in their alphabetical position: `RoverfoxStorageManager.cpp` (from `anti-font-fingerprinting.patch`) and `TimezoneManager.cpp` (from `timezone-spoofing.patch`). **EXPORTS sort conflicts:** Each patch uses a separate `EXPORTS.mozilla.dom += ["Header.h"]` statement near its `SOURCES` block, rather than inserting into the main sorted EXPORTS list. This avoids sort conflicts when multiple patches add headers at similar alphabetical positions. @@ -626,7 +614,6 @@ bundles are shipped in the wheel. | Screen dims, colorDepth | fpgen or preset | Viewport adjusted by -28px for browser chrome | | WebGL vendor/renderer | `sample_webgl()` from `webgl_data.db` | OS-weighted probability sampling. fpgen's own WebGL fields are not mapped in `fpgen.yml` yet, so both paths call `sample_webgl()`. | | Font list | `_generate_random_font_subset()` | One weighted OS-version base in full, plus each addition unit at its measured probability; marker fonts always included. See [FONTS.md](FONTS.md). NOT from presets. | -| Font spacing seed | `0` | Off by default (0 = no-op in C++); pass `fonts:spacing_seed` to opt in | | Audio seed | Derived from the identity (NewBrowser) or `randint(1, 2^32-1)` (NewContext) | Never 0 | | Timezone | From preset, or `timezone` in `CAMOU_CONFIG` | The init script calls `setTimezone()` only for an explicit value; otherwise the C++ side falls back to `CAMOU_CONFIG` (set from geoip at launch) or the browser default. | | Speech voices | `_generate_random_voice_subset()` | Follows the measured model in `voice-manifests.json`: Windows gets the display language's OneCore pack plus its legacy Desktop voices at their measured rate; macOS the compact + Eloquence base plus rare downloads; Linux speech-dispatcher's espeak-ng list. Seeded by the identity. NOT from presets. | diff --git a/native-tests/test_tribal_rules.py b/native-tests/test_tribal_rules.py index 0297d51..3a0a57a 100644 --- a/native-tests/test_tribal_rules.py +++ b/native-tests/test_tribal_rules.py @@ -542,6 +542,21 @@ def test_a_sandbox_held_over_a_page_window_is_nuked_not_just_dropped(): ) +def test_no_glyph_spacing_seed_anywhere(): + """No config key, no setter, no shaper hook.""" + declared = { + entry["property"] + for entry in json.loads((REPO_ROOT / "settings" / "properties.json").read_text()) + } + assert "fonts:spacing_seed" not in declared, explain("no-glyph-spacing-noise") + for source in [*sorted((REPO_ROOT / "patches").rglob("*.patch")), + REPO_ROOT / "pythonlib" / "camoufox" / "fingerprints.py"]: + assert "FontSpacingSeed" not in source.read_text(encoding="utf-8", errors="ignore"), ( + f"{source.relative_to(REPO_ROOT)} still carries the spacing seed" + + explain("no-glyph-spacing-noise") + ) + + def test_no_canvas_seed_is_declared_or_sent(): """Nothing in the browser reads a canvas seed, so neither launcher sends one.""" declared = { diff --git a/patches/anti-font-fingerprinting.patch b/patches/anti-font-fingerprinting.patch index a276c96..d7de58f 100644 --- a/patches/anti-font-fingerprinting.patch +++ b/patches/anti-font-fingerprinting.patch @@ -1,179 +1,3 @@ -diff --git a/dom/base/FontSpacingSeedManager.cpp b/dom/base/FontSpacingSeedManager.cpp -new file mode 100644 -index 0000000000..e07de3e753 ---- /dev/null -+++ b/dom/base/FontSpacingSeedManager.cpp -@@ -0,0 +1,91 @@ -+#include "FontSpacingSeedManager.h" -+#include "nsPrintfCString.h" -+#include "MaskConfig.hpp" -+#include "nsGlobalWindowInner.h" -+#include "xpcpublic.h" -+#include "mozilla/dom/BrowsingContext.h" -+#include "mozilla/dom/Document.h" -+#include "nsDocShell.h" -+#include "nsPIDOMWindow.h" -+ -+namespace mozilla { -+namespace dom { -+ -+/* static */ nsString -+FontSpacingSeedManager::KeyForUserContext(uint32_t userContextId) { -+ nsString key; -+ key.AppendLiteral(u"seed_"); -+ key.AppendInt(userContextId); -+ return key; -+} -+ -+/* static */ nsString -+FontSpacingSeedManager::DisabledKeyForUserContext(uint32_t userContextId) { -+ nsString key; -+ key.AppendLiteral(u"disabled_"); -+ key.AppendInt(userContextId); -+ return key; -+} -+ -+/* static */ void -+FontSpacingSeedManager::SetSeed(uint32_t userContextId, uint32_t seed) { -+ nsString key = KeyForUserContext(userContextId); -+ RoverfoxStorageManager::PutUint(key, seed); -+ -+ // Mark the function as disabled for this context after first use -+ DisableFunction(userContextId); -+} -+ -+/* static */ uint32_t -+FontSpacingSeedManager::GetSeed(uint32_t userContextId) { -+ nsString key = KeyForUserContext(userContextId); -+ uint32_t seed = 0; -+ if (RoverfoxStorageManager::GetUint(key, seed) && seed != 0) { -+ return seed; -+ } -+ // Fallback to CAMOU_CONFIG for Workers in separate processes -+ if (auto val = MaskConfig::GetUint32("fonts:spacing_seed")) { -+ return val.value(); -+ } -+ return 0; -+} -+ -+/* static */ bool -+FontSpacingSeedManager::HasSeed(uint32_t userContextId) { -+ nsString key = KeyForUserContext(userContextId); -+ uint32_t seed = 0; -+ return RoverfoxStorageManager::GetUint(key, seed); -+} -+ -+/* static */ bool -+FontSpacingSeedManager::IsFunctionDisabled(uint32_t userContextId) { -+ nsString key = DisabledKeyForUserContext(userContextId); -+ bool disabled = false; -+ return RoverfoxStorageManager::GetBool(key, disabled) && disabled; -+} -+ -+/* static */ void -+FontSpacingSeedManager::DisableFunction(uint32_t userContextId) { -+ nsString key = DisabledKeyForUserContext(userContextId); -+ RoverfoxStorageManager::PutBool(key, true); -+} -+ -+/* static */ bool -+FontSpacingSeedManager::IsFunctionEnabledForWebIDL(JSContext* aCx, JSObject* aObj) { -+ nsGlobalWindowInner* win = xpc::WindowOrNull(aObj); -+ if (!win) { -+ return false; -+ } -+ -+ uint32_t userContextId = 0; -+ if (BrowsingContext* bc = win->GetBrowsingContext()) { -+ userContextId = bc->OriginAttributesRef().mUserContextId; -+ } -+ -+ bool disabled = false; -+ RoverfoxStorageManager::GetBool(DisabledKeyForUserContext(userContextId), disabled); -+ return !disabled; -+} -+ -+} // namespace dom -+} // namespace mozilla -\ No newline at end of file -diff --git a/dom/base/FontSpacingSeedManager.h b/dom/base/FontSpacingSeedManager.h -new file mode 100644 -index 0000000000..63d43f3b86 ---- /dev/null -+++ b/dom/base/FontSpacingSeedManager.h -@@ -0,0 +1,71 @@ -+#ifndef mozilla_dom_FontSpacingSeedManager_h -+#define mozilla_dom_FontSpacingSeedManager_h -+ -+#include "nsString.h" -+#include "RoverfoxStorageManager.h" -+ -+namespace mozilla { -+namespace dom { -+ -+/** -+ * FontSpacingSeedManager manages font spacing seeds per user context. -+ * This enables privacy-preserving font fingerprinting by allowing deterministic -+ * font spacing modifications that are isolated by user context. -+ */ -+class FontSpacingSeedManager { -+public: -+ /** -+ * Set the font spacing seed for a given user context. -+ * @param userContextId The user context ID (0 for default context) -+ * @param seed The seed value to use for font spacing modifications -+ */ -+ static void SetSeed(uint32_t userContextId, uint32_t seed); -+ -+ /** -+ * Get the font spacing seed for a given user context. -+ * @param userContextId The user context ID -+ * @return The seed value, or 0 if no seed has been set -+ */ -+ static uint32_t GetSeed(uint32_t userContextId); -+ -+ /** -+ * Check if a seed has been set for a given user context. -+ * @param userContextId The user context ID -+ * @return true if a seed has been set, false otherwise -+ */ -+ static bool HasSeed(uint32_t userContextId); -+ -+ /** -+ * Check if the setFontSpacingSeed function has been used and should be disabled for a context. -+ * @param userContextId The user context ID -+ * @return true if the function has been used for this context and should not appear on new windows -+ */ -+ static bool IsFunctionDisabled(uint32_t userContextId); -+ -+ /** -+ * Mark the setFontSpacingSeed function as used/disabled for a context. -+ * @param userContextId The user context ID -+ */ -+ static void DisableFunction(uint32_t userContextId); -+ -+ /** -+ * WebIDL-compatible function to check if setFontSpacingSeed should be enabled. -+ * This extracts the user context from the window and checks if disabled. -+ * @param aCx JavaScript context -+ * @param aObj JavaScript object (window) -+ * @return true if function should be available, false otherwise -+ */ -+ static bool IsFunctionEnabledForWebIDL(JSContext* aCx, JSObject* aObj); -+ -+private: -+ // Helper to convert userContextId to string key -+ static nsString KeyForUserContext(uint32_t userContextId); -+ -+ // Helper to create key for tracking function disabled state -+ static nsString DisabledKeyForUserContext(uint32_t userContextId); -+}; -+ -+} // namespace dom -+} // namespace mozilla -+ -+#endif // mozilla_dom_FontSpacingSeedManager_h -\ No newline at end of file diff --git a/dom/base/RoverfoxStorageManager.cpp b/dom/base/RoverfoxStorageManager.cpp new file mode 100644 index 0000000000..dcb359d25a @@ -422,18 +246,10 @@ index 0000000000..e6d64378e1 + +#endif // mozilla_dom_RoverfoxStorageManager_h diff --git a/dom/base/moz.build b/dom/base/moz.build -index 7e8d942191..d922362ddd 100644 +index 7e8d942191..e44e7259bf 100644 --- a/dom/base/moz.build +++ b/dom/base/moz.build -@@ -191,6 +191,7 @@ EXPORTS.mozilla.dom += [ - "External.h", - "FastFrontRemovableArray.h", - "FilteredNodeIterator.h", -+ "FontSpacingSeedManager.h", - "FormData.h", - "FragmentDirective.h", - "FragmentOrElement.h", -@@ -257,6 +258,7 @@ EXPORTS.mozilla.dom += [ +@@ -257,6 +257,7 @@ EXPORTS.mozilla.dom += [ "RequestCallbackManager.h", "ResizeObserver.h", "ResponsiveImageSelector.h", @@ -441,15 +257,7 @@ index 7e8d942191..d922362ddd 100644 "SameProcessMessageQueue.h", "ScreenLuminance.h", "ScreenOrientation.h", -@@ -372,6 +374,7 @@ UNIFIED_SOURCES += [ - "EventSource.cpp", - "EventSourceEventService.cpp", - "External.cpp", -+ "FontSpacingSeedManager.cpp", - "FormData.cpp", - "FragmentDirective.cpp", - "FragmentOrElement.cpp", -@@ -461,6 +464,7 @@ UNIFIED_SOURCES += [ +@@ -461,6 +462,7 @@ UNIFIED_SOURCES += [ "RemoteOuterWindowProxy.cpp", "ResizeObserver.cpp", "ResponsiveImageSelector.cpp", @@ -458,7 +266,7 @@ index 7e8d942191..d922362ddd 100644 "ScreenLuminance.cpp", "ScreenOrientation.cpp", diff --git a/dom/base/nsGlobalWindowInner.cpp b/dom/base/nsGlobalWindowInner.cpp -index 7cdddd10c1..8c4eb1b6c7 100644 +index 7cdddd10c1..922ddbc377 100644 --- a/dom/base/nsGlobalWindowInner.cpp +++ b/dom/base/nsGlobalWindowInner.cpp @@ -56,6 +56,7 @@ @@ -469,91 +277,15 @@ index 7cdddd10c1..8c4eb1b6c7 100644 #include "mozilla/ExtensionPolicyService.h" #include "mozilla/FloatingPoint.h" #include "mozilla/FlushType.h" -@@ -241,6 +242,9 @@ +@@ -241,6 +242,8 @@ #include "nsICookieService.h" #include "nsID.h" #include "nsIDOMStorageManager.h" -+#include "FontSpacingSeedManager.h" +#include "nsDocShell.h" +#include "mozilla/OriginAttributes.h" #include "nsIDOMXULControlElement.h" #include "nsIDeviceSensors.h" #include "nsIDocShell.h" -@@ -7728,6 +7732,25 @@ IntlUtils* nsGlobalWindowInner::GetIntlUtils(ErrorResult& aError) { - return mIntlUtils; - } - -+void nsGlobalWindowInner::SetFontSpacingSeed(uint32_t seed, ErrorResult& aRv) { -+ uint32_t userContextId = 0; -+ if (BrowsingContext* bc = GetBrowsingContext()) { -+ userContextId = bc->OriginAttributesRef().mUserContextId; -+ } -+ -+ FontSpacingSeedManager::SetSeed(userContextId, seed); -+ -+ // Self-destruct: set to undefined first to bust IC/shape caches on ARM64. -+ if (JSContext* cx = nsContentUtils::GetCurrentJSContext()) { -+ JS::Rooted global(cx, JS::CurrentGlobalOrNull(cx)); -+ if (global) { -+ JS::Rooted undef(cx, JS::UndefinedValue()); -+ JS_SetProperty(cx, global, "setFontSpacingSeed", undef); -+ JS_DeleteProperty(cx, global, "setFontSpacingSeed"); -+ } -+ } -+} -+ - void nsGlobalWindowInner::StoreSharedWorker(SharedWorker* aSharedWorker) { - MOZ_ASSERT(aSharedWorker); - MOZ_ASSERT(!mSharedWorkers.Contains(aSharedWorker)); -diff --git a/dom/base/nsGlobalWindowInner.h b/dom/base/nsGlobalWindowInner.h -index 5292a4d543..5b7cd52b9b 100644 ---- a/dom/base/nsGlobalWindowInner.h -+++ b/dom/base/nsGlobalWindowInner.h -@@ -680,6 +680,9 @@ class nsGlobalWindowInner final : public mozilla::dom::EventTarget, - - mozilla::dom::IntlUtils* GetIntlUtils(mozilla::ErrorResult& aRv); - -+ // Font spacing seed for privacy-preserving font fingerprinting -+ void SetFontSpacingSeed(uint32_t seed, mozilla::ErrorResult& aRv); -+ - void StoreSharedWorker(mozilla::dom::SharedWorker* aSharedWorker); - - void ForgetSharedWorker(mozilla::dom::SharedWorker* aSharedWorker); -diff --git a/dom/canvas/CanvasRenderingContext2D.cpp b/dom/canvas/CanvasRenderingContext2D.cpp -index 45b0de48b5..dd79a43beb 100644 ---- a/dom/canvas/CanvasRenderingContext2D.cpp -+++ b/dom/canvas/CanvasRenderingContext2D.cpp -@@ -58,6 +58,7 @@ - #include "mozilla/dom/CanvasRenderingContext2DBinding.h" - #include "mozilla/dom/DOMMatrix.h" - #include "mozilla/dom/Document.h" -+#include "mozilla/dom/BrowsingContext.h" - #include "mozilla/dom/FontFaceSet.h" - #include "mozilla/dom/FontFaceSetImpl.h" - #include "mozilla/dom/GeneratePlaceholderCanvasData.h" -@@ -4775,10 +4776,21 @@ struct MOZ_STACK_CLASS CanvasBidiProcessor final - } else { - flags &= ~gfx::ShapedTextFlags::TEXT_IS_RTL; - } -+ // Extract user context ID via BrowsingContext (canonical source) -+ uint32_t userContextId = 0; -+ if (mCtx && mCtx->mCanvasElement) { -+ if (Document* doc = mCtx->mCanvasElement->GetOwnerDocument()) { -+ if (nsPIDOMWindowInner* win = doc->GetInnerWindow()) { -+ if (BrowsingContext* bc = win->GetBrowsingContext()) { -+ userContextId = bc->OriginAttributesRef().mUserContextId; -+ } -+ } -+ } -+ } - mTextRun = mFontgrp->MakeTextRun( - aText, aLength, mDrawTarget, mAppUnitsPerDevPixel, flags, - nsTextFrameUtils::Flags::DontSkipDrawingForPendingUserFonts, -- mMissingFonts.get()); -+ mMissingFonts.get(), userContextId); - pfl->Unlock(); - } - diff --git a/dom/canvas/OffscreenCanvas.cpp b/dom/canvas/OffscreenCanvas.cpp index 7234240711..df84b793dc 100644 --- a/dom/canvas/OffscreenCanvas.cpp @@ -574,23 +306,6 @@ index 7234240711..df84b793dc 100644 bool OffscreenCanvas::IsChrome() const { if (NS_IsMainThread()) { nsCOMPtr win = do_QueryInterface(GetRelevantGlobal()); -diff --git a/dom/webidl/Window.webidl b/dom/webidl/Window.webidl -index 77f35801a0..454ef6f4e7 100644 ---- a/dom/webidl/Window.webidl -+++ b/dom/webidl/Window.webidl -@@ -934,6 +934,12 @@ partial interface Window { - readonly attribute VisualViewport visualViewport; - }; - -+// Font spacing seed interface for privacy-preserving font fingerprinting -+partial interface Window { -+ [Throws, Func="mozilla::dom::FontSpacingSeedManager::IsFunctionEnabledForWebIDL"] -+ undefined setFontSpacingSeed(unsigned long seed); -+}; -+ - // Used to assign marks to appear on the scrollbar when - // finding on a page. - partial interface Window { diff --git a/dom/workers/WorkerPrivate.h b/dom/workers/WorkerPrivate.h index 7f56e1c86d..80b050c08b 100644 --- a/dom/workers/WorkerPrivate.h @@ -604,324 +319,6 @@ index 7f56e1c86d..80b050c08b 100644 void MemoryPressure(); void UpdateContextOptions(const JS::ContextOptions& aContextOptions); -diff --git a/gfx/src/nsFontMetrics.cpp b/gfx/src/nsFontMetrics.cpp -index a52e3a56d8..3025f57da5 100644 ---- a/gfx/src/nsFontMetrics.cpp -+++ b/gfx/src/nsFontMetrics.cpp -@@ -22,9 +22,13 @@ - #include "nsStyleConsts.h" // for StyleHyphens::None - #include "mozilla/Assertions.h" // for MOZ_ASSERT - #include "mozilla/UniquePtr.h" // for UniquePtr -+#include "mozilla/dom/Document.h" // for Document -+#include "mozilla/dom/BrowsingContext.h" // for BrowsingContext -+#include "nsPIDOMWindow.h" // for nsPIDOMWindowInner - - class gfxUserFontSet; - using namespace mozilla; -+using namespace mozilla::dom; - - namespace { - -@@ -34,17 +38,39 @@ class AutoTextRun { - - AutoTextRun(const nsFontMetrics* aMetrics, DrawTarget* aDrawTarget, - const char* aString, uint32_t aLength) { -+ // Extract user context ID via BrowsingContext (canonical source) -+ uint32_t userContextId = 0; -+ if (aMetrics->mPresContext) { -+ if (Document* doc = aMetrics->mPresContext->Document()) { -+ if (nsPIDOMWindowInner* win = doc->GetInnerWindow()) { -+ if (BrowsingContext* bc = win->GetBrowsingContext()) { -+ userContextId = bc->OriginAttributesRef().mUserContextId; -+ } -+ } -+ } -+ } - mTextRun = aMetrics->GetThebesFontGroup()->MakeTextRun( - reinterpret_cast(aString), aLength, aDrawTarget, - aMetrics->AppUnitsPerDevPixel(), ComputeFlags(aMetrics), -- nsTextFrameUtils::Flags(), nullptr); -+ nsTextFrameUtils::Flags(), nullptr, userContextId); - } - - AutoTextRun(const nsFontMetrics* aMetrics, DrawTarget* aDrawTarget, - const char16_t* aString, uint32_t aLength) { -+ // Extract user context ID via BrowsingContext (canonical source) -+ uint32_t userContextId = 0; -+ if (aMetrics->mPresContext) { -+ if (Document* doc = aMetrics->mPresContext->Document()) { -+ if (nsPIDOMWindowInner* win = doc->GetInnerWindow()) { -+ if (BrowsingContext* bc = win->GetBrowsingContext()) { -+ userContextId = bc->OriginAttributesRef().mUserContextId; -+ } -+ } -+ } -+ } - mTextRun = aMetrics->GetThebesFontGroup()->MakeTextRun( - aString, aLength, aDrawTarget, aMetrics->AppUnitsPerDevPixel(), -- ComputeFlags(aMetrics), nsTextFrameUtils::Flags(), nullptr); -+ ComputeFlags(aMetrics), nsTextFrameUtils::Flags(), nullptr, userContextId); - } - - gfxTextRun* get() const { return mTextRun.get(); } -diff --git a/gfx/src/nsFontMetrics.h b/gfx/src/nsFontMetrics.h -index df44559b26..d829aa8ff2 100644 ---- a/gfx/src/nsFontMetrics.h -+++ b/gfx/src/nsFontMetrics.h -@@ -299,6 +299,10 @@ class nsFontMetrics final { - bool AllowForceGDIClassic() const { return mAllowForceGDIClassic; } - #endif - -+ // Pointer to the pres context for which this fontMetrics object was -+ // created. -+ nsPresContext* MOZ_NON_OWNING_REF mPresContext; -+ - private: - // Private destructor, to discourage deletion outside of Release(): - ~nsFontMetrics(); -@@ -306,9 +310,6 @@ class nsFontMetrics final { - const nsFont mFont; - RefPtr mFontGroup; - RefPtr const mLanguage; -- // Pointer to the pres context for which this fontMetrics object was -- // created. -- nsPresContext* MOZ_NON_OWNING_REF mPresContext; - const int32_t mP2A; - - // The font orientation (horizontal or vertical) for which these metrics -diff --git a/gfx/thebes/gfxFont.cpp b/gfx/thebes/gfxFont.cpp -index 39a0a013de..ce1c233d7d 100644 ---- a/gfx/thebes/gfxFont.cpp -+++ b/gfx/thebes/gfxFont.cpp -@@ -18,6 +18,7 @@ - - #include "gfxGlyphExtents.h" - #include "gfxPlatform.h" -+#include "mozilla/dom/FontSpacingSeedManager.h" - #include "gfxTextRun.h" - #include "nsGkAtoms.h" - -@@ -3308,9 +3309,10 @@ bool gfxFont::ProcessShapedWordInternal( - const T* aText, uint32_t aLength, uint32_t aHash, Script aRunScript, - nsAtom* aLanguage, bool aVertical, int32_t aAppUnitsPerDevUnit, - gfx::ShapedTextFlags aFlags, RoundingFlags aRounding, -- gfxTextPerfMetrics* aTextPerf GFX_MAYBE_UNUSED, Func aCallback) { -+ gfxTextPerfMetrics* aTextPerf GFX_MAYBE_UNUSED, -+ uint32_t aUserContextId, Func aCallback) { - WordCacheKey key(aText, aLength, aHash, aRunScript, aLanguage, -- aAppUnitsPerDevUnit, aFlags, aRounding); -+ aAppUnitsPerDevUnit, aFlags, aRounding, aUserContextId); - { - // If we have a word cache, attempt to look up the word in it. - AutoReadLock lock(mLock); -@@ -3341,6 +3343,8 @@ bool gfxFont::ProcessShapedWordInternal( - NS_WARNING("failed to create gfxShapedWord - expect missing text"); - return false; - } -+ // Propagate user context ID for HarfBuzz shaping/logging. -+ newShapedWord->SetUserContextId(aUserContextId); - DebugOnly ok = ShapeText(aText, 0, aLength, aRunScript, aLanguage, - aVertical, aRounding, newShapedWord.get()); - NS_WARNING_ASSERTION(ok, "failed to shape word - expect garbled text"); -@@ -3397,7 +3401,8 @@ bool gfxFont::WordCacheKey::HashPolicy::match(const Key& aKey, - if (aKey.mLength != aLookup.mLength || aKey.mFlags != aLookup.mFlags || - aKey.mRounding != aLookup.mRounding || - aKey.mAppUnitsPerDevUnit != aLookup.mAppUnitsPerDevUnit || -- aKey.mScript != aLookup.mScript || aKey.mLanguage != aLookup.mLanguage) { -+ aKey.mScript != aLookup.mScript || aKey.mLanguage != aLookup.mLanguage || -+ aKey.mUserContextId != aLookup.mUserContextId) { - return false; - } - -@@ -3434,7 +3439,7 @@ bool gfxFont::ProcessSingleSpaceShapedWord( - return ProcessShapedWordInternal( - &space, 1, gfxShapedWord::HashMix(0, ' '), Script::LATIN, - /* aLanguage = */ nullptr, aVertical, aAppUnitsPerDevUnit, aFlags, -- aRounding, nullptr, aCallback); -+ aRounding, nullptr, 0 /* pbid */, aCallback); - } - - bool gfxFont::ShapeText(const uint8_t* aText, uint32_t aOffset, -@@ -3788,6 +3793,7 @@ bool gfxFont::SplitAndInitTextRun( - bool processed = ProcessShapedWordInternal( - aString + wordStart, length, hash, aRunScript, aLanguage, vertical, - appUnitsPerDevUnit, wordFlags, rounding, tp, -+ aTextRun->GetUserContextId(), - [&](gfxShapedWord* aShapedWord) { - aTextRun->CopyGlyphDataFrom(aShapedWord, aRunStart + wordStart); - }); -@@ -3813,6 +3819,7 @@ bool gfxFont::SplitAndInitTextRun( - &boundary, 1, gfxShapedWord::HashMix(0, boundary), aRunScript, - aLanguage, vertical, appUnitsPerDevUnit, - flags | gfx::ShapedTextFlags::TEXT_IS_8BIT, rounding, tp, -+ aTextRun->GetUserContextId(), - [&](gfxShapedWord* aShapedWord) { - aTextRun->CopyGlyphDataFrom(aShapedWord, aRunStart + i); - if (boundary == ' ') { -diff --git a/gfx/thebes/gfxFont.h b/gfx/thebes/gfxFont.h -index 25022d4b5b..fe65662b77 100644 ---- a/gfx/thebes/gfxFont.h -+++ b/gfx/thebes/gfxFont.h -@@ -746,6 +746,10 @@ class gfxShapedText { - - virtual ~gfxShapedText() = default; - -+ // Optional accessor overridden by gfxTextRun to expose the private browsing ID. -+ // Default returns 0 for non-textrun shaped text objects. -+ virtual uint32_t GetUserContextId() const { return 0; } -+ - /** - * This class records the information associated with a character in the - * input string. It's optimized for the case where there is one glyph -@@ -1335,6 +1339,11 @@ class gfxShapedWord final : public gfxShapedText { - // allocated via malloc. - void operator delete(void* p) { free(p); } - -+ // User Context ID plumbing for shaping-time access. -+ // HarfBuzz shaper will query this via gfxShapedText::GetUserContextId(). -+ void SetUserContextId(uint32_t aId) { mUserContextId = aId; } -+ uint32_t GetUserContextId() const override { return mUserContextId; } -+ - const CompressedGlyph* GetCharacterGlyphs() const override { - return &mCharGlyphsStorage[0]; - } -@@ -1418,6 +1427,9 @@ class gfxShapedWord final : public gfxShapedText { - // With multithreaded shaping, this may be updated by any thread. - std::atomic mAgeCounter; - -+ // User Context ID carried with the shaped word for shaper access. -+ uint32_t mUserContextId = 0; -+ - // The mCharGlyphsStorage array is actually a variable-size member; - // when the ShapedWord is created, its size will be increased as necessary - // to allow the proper number of glyphs to be stored. -@@ -2155,7 +2167,8 @@ class gfxFont { - int32_t aAppUnitsPerDevUnit, - mozilla::gfx::ShapedTextFlags aFlags, - RoundingFlags aRounding, -- gfxTextPerfMetrics* aTextPerf, Func aCallback); -+ gfxTextPerfMetrics* aTextPerf, -+ uint32_t aUserContextId, Func aCallback); - - // whether a given feature is included in feature settings from both the - // font and the style. aFeatureOn set if resolved feature value is non-zero -@@ -2185,12 +2198,13 @@ class gfxFont { - int32_t mAppUnitsPerDevUnit; - PLDHashNumber mHashKey; - bool mTextIs8Bit; -+ uint32_t mUserContextId; - RoundingFlags mRounding; - - WordCacheKey(const uint8_t* aText, uint32_t aLength, uint32_t aStringHash, - Script aScriptCode, nsAtom* aLanguage, - int32_t aAppUnitsPerDevUnit, ShapedTextFlags aFlags, -- RoundingFlags aRounding) -+ RoundingFlags aRounding, uint32_t aUserContextId = 0) - : mLength(aLength), - mFlags(aFlags), - mScript(aScriptCode), -@@ -2198,8 +2212,9 @@ class gfxFont { - mAppUnitsPerDevUnit(aAppUnitsPerDevUnit), - mHashKey(aStringHash + static_cast(aScriptCode) + - aAppUnitsPerDevUnit * 0x100 + uint16_t(aFlags) * 0x10000 + -- int(aRounding) + (aLanguage ? aLanguage->hash() : 0)), -+ int(aRounding) + (aLanguage ? aLanguage->hash() : 0) + aUserContextId * 0x1000000), - mTextIs8Bit(true), -+ mUserContextId(aUserContextId), - mRounding(aRounding) { - NS_ASSERTION(aFlags & ShapedTextFlags::TEXT_IS_8BIT, - "8-bit flag should have been set"); -@@ -2209,7 +2224,7 @@ class gfxFont { - WordCacheKey(const char16_t* aText, uint32_t aLength, uint32_t aStringHash, - Script aScriptCode, nsAtom* aLanguage, - int32_t aAppUnitsPerDevUnit, ShapedTextFlags aFlags, -- RoundingFlags aRounding) -+ RoundingFlags aRounding, uint32_t aUserContextId = 0) - : mLength(aLength), - mFlags(aFlags), - mScript(aScriptCode), -@@ -2217,8 +2232,9 @@ class gfxFont { - mAppUnitsPerDevUnit(aAppUnitsPerDevUnit), - mHashKey(aStringHash + static_cast(aScriptCode) + - aAppUnitsPerDevUnit * 0x100 + uint16_t(aFlags) * 0x10000 + -- int(aRounding)), -+ int(aRounding) + aUserContextId * 0x1000000), - mTextIs8Bit(false), -+ mUserContextId(aUserContextId), - mRounding(aRounding) { - // We can NOT assert that TEXT_IS_8BIT is false in aFlags here, - // because this might be an 8bit-only word from a 16-bit textrun, -diff --git a/gfx/thebes/gfxHarfBuzzShaper.cpp b/gfx/thebes/gfxHarfBuzzShaper.cpp -index e41408f51c..448fcd58fd 100644 ---- a/gfx/thebes/gfxHarfBuzzShaper.cpp -+++ b/gfx/thebes/gfxHarfBuzzShaper.cpp -@@ -16,6 +16,10 @@ - - #include "harfbuzz/hb.h" - #include "harfbuzz/hb-ot.h" -+#include -+#include -+#include "MaskConfig.hpp" -+#include "mozilla/dom/FontSpacingSeedManager.h" - - #include - -@@ -1465,6 +1469,50 @@ bool gfxHarfBuzzShaper::ShapeText(const char16_t* aText, uint32_t aOffset, - - hb_shape(mHBFont, mBuffer, features.Elements(), features.Length()); - -+ // Resolve seed from manager using user context ID. -+ // seed == 0 means no perturbation (consistent with AudioFingerprintManager). -+ uint32_t pbid = aShapedText ? aShapedText->GetUserContextId() : 0; -+ uint32_t seed = mozilla::dom::FontSpacingSeedManager::GetSeed(pbid); -+ -+ if (seed != 0) { -+ // Generate a random float [0, 0.1] to offset the letter spacing -+ seed = (seed * 1103515245 + 12345) & 0x7fffffff; -+ float randomFloat = (static_cast(seed) / 2147483647.0f) * 0.1f; -+ hb_position_t spacing = FloatToFixed(randomFloat); -+ -+ uint32_t glyphCount; -+ hb_glyph_position_t* glyphPositions = -+ hb_buffer_get_glyph_positions(mBuffer, &glyphCount); -+ -+ // Perturb ADVANCES only. -+ // -+ // x_offset/y_offset are the positioning offsets GPOS uses to place a glyph -+ // relative to the pen -- most importantly to park a combining mark over -+ // its base. They do not contribute to the measured width, so they add -+ // nothing to the metric we are perturbing, but layout never sees them: -+ // gfxTextRun stores advances, while painting honours the offsets. Feeding -+ // a running total into them paints every glyph a further `spacing` past -+ // where layout placed it and detaches every combining mark from its base. -+ // Scripts whose marks carry a zero advance -- Thai, Lao, Arabic, -+ // Devanagari, Hebrew -- collapse into stacked glyphs, while Latin merely -+ // looks slightly loose (daijro/camoufox#741). -+ // -+ // Zero-advance glyphs are skipped for the same reason: a combining mark is -+ // not a separate character, and widening it pushes the following base away -+ // and strands the mark. Every advancing glyph still gets +spacing, so the -+ // measured width -- the actual fingerprinting signal -- is perturbed -+ // exactly as before. -+ for (uint32_t i = 0; i < glyphCount; ++i) { -+ if (aVertical) { -+ if (glyphPositions[i].y_advance != 0) { -+ glyphPositions[i].y_advance -= spacing; -+ } -+ } else if (glyphPositions[i].x_advance != 0) { -+ glyphPositions[i].x_advance += spacing; -+ } -+ } -+ } -+ - if (isRightToLeft) { - hb_buffer_reverse(mBuffer); - } -@@ -1770,3 +1818,4 @@ nsresult gfxHarfBuzzShaper::SetGlyphsFromRun(gfxShapedText* aShapedText, - - return NS_OK; - } -+ diff --git a/gfx/thebes/gfxPlatformFontList.cpp b/gfx/thebes/gfxPlatformFontList.cpp index b3c22e2bb1..6bdcd2a57c 100644 --- a/gfx/thebes/gfxPlatformFontList.cpp @@ -935,7 +332,7 @@ index b3c22e2bb1..6bdcd2a57c 100644 private: diff --git a/gfx/thebes/gfxTextRun.cpp b/gfx/thebes/gfxTextRun.cpp -index fc1293be42..45641bd8b3 100644 +index fc1293be42..cac43a8b1f 100644 --- a/gfx/thebes/gfxTextRun.cpp +++ b/gfx/thebes/gfxTextRun.cpp @@ -24,6 +24,7 @@ @@ -956,39 +353,7 @@ index fc1293be42..45641bd8b3 100644 #ifdef XP_WIN # include "gfxWindowsPlatform.h" -@@ -144,25 +148,27 @@ void* gfxTextRun::AllocateStorageForTextRun(size_t aSize, uint32_t aLength) { - already_AddRefed gfxTextRun::Create( - const gfxTextRunFactory::Parameters* aParams, uint32_t aLength, - gfxFontGroup* aFontGroup, gfx::ShapedTextFlags aFlags, -- nsTextFrameUtils::Flags aFlags2) { -+ nsTextFrameUtils::Flags aFlags2, uint32_t aUserContextId) { - void* storage = AllocateStorageForTextRun(sizeof(gfxTextRun), aLength); - if (!storage) { - return nullptr; - } - -- RefPtr result = -- new (storage) gfxTextRun(aParams, aLength, aFontGroup, aFlags, aFlags2); -+ RefPtr result = new (storage) -+ gfxTextRun(aParams, aLength, aFontGroup, aFlags, aFlags2, aUserContextId); - return result.forget(); - } - - gfxTextRun::gfxTextRun(const gfxTextRunFactory::Parameters* aParams, - uint32_t aLength, gfxFontGroup* aFontGroup, - gfx::ShapedTextFlags aFlags, -- nsTextFrameUtils::Flags aFlags2) -+ nsTextFrameUtils::Flags aFlags2, -+ uint32_t aUserContextId) - : gfxShapedText(aLength, aFlags, aParams->mAppUnitsPerDevUnit), - mUserData(aParams->mUserData), - mFontGroup(aFontGroup), - mFlags2(aFlags2), -+ mUserContextId(aUserContextId), - mShapingState(eShapingState_Normal) { - NS_ASSERTION(mAppUnitsPerDevUnit > 0, "Invalid app unit scale"); - NS_ADDREF(mFontGroup); -@@ -699,7 +705,8 @@ void gfxTextRun::DrawEmphasisMarks( +@@ -699,7 +703,8 @@ void gfxTextRun::DrawEmphasisMarks( gfxContext* aContext, gfxTextRun* aMark, gfxFloat aMarkAdvance, gfx::Point aPt, Range aRange, const PropertyProvider* aProvider, mozilla::gfx::PaletteCache& aPaletteCache) const { @@ -998,7 +363,7 @@ index fc1293be42..45641bd8b3 100644 EmphasisMarkDrawParams params(aContext, aPaletteCache); params.mark = aMark; -@@ -1841,6 +1848,18 @@ gfxFontGroup::gfxFontGroup(FontVisibilityProvider* aFontVisibilityProvider, +@@ -1841,6 +1846,18 @@ gfxFontGroup::gfxFontGroup(FontVisibilityProvider* aFontVisibilityProvider, mFontVariantEmoji(aVariantEmoji) { // We don't use SetUserFontSet() here, as we want to unconditionally call // EnsureFontList() rather than only do UpdateUserFonts() if it changed. @@ -1017,60 +382,7 @@ index fc1293be42..45641bd8b3 100644 } gfxFontGroup::~gfxFontGroup() { -@@ -2453,12 +2472,12 @@ already_AddRefed gfxFontGroup::MakeHyphenTextRun( - RefPtr font = GetFirstValidFont(uint32_t(hyphen)); - if (font->HasCharacter(hyphen)) { - return MakeTextRun(&hyphen, 1, aDrawTarget, aAppUnitsPerDevUnit, aFlags, -- nsTextFrameUtils::Flags(), nullptr); -+ nsTextFrameUtils::Flags(), nullptr, mUserContextId); - } - - static const uint8_t dash = '-'; - return MakeTextRun(&dash, 1, aDrawTarget, aAppUnitsPerDevUnit, aFlags, -- nsTextFrameUtils::Flags(), nullptr); -+ nsTextFrameUtils::Flags(), nullptr, mUserContextId); - } - - gfxFloat gfxFontGroup::GetHyphenWidth( -@@ -2479,7 +2498,7 @@ template - already_AddRefed gfxFontGroup::MakeTextRun( - const T* aString, uint32_t aLength, const Parameters* aParams, - gfx::ShapedTextFlags aFlags, nsTextFrameUtils::Flags aFlags2, -- gfxMissingFontRecorder* aMFR) { -+ gfxMissingFontRecorder* aMFR, uint32_t aUserContextId) { - if (aLength == 0) { - return MakeEmptyTextRun(aParams, aFlags, aFlags2); - } -@@ -2498,8 +2517,13 @@ already_AddRefed gfxFontGroup::MakeTextRun( - return MakeBlankTextRun(aString, aLength, aParams, aFlags, aFlags2); - } - -+ // If caller didn't provide an ID, default to the group's cached value. -+ if (aUserContextId == 0) { -+ aUserContextId = mUserContextId; -+ } -+ - RefPtr textRun = -- gfxTextRun::Create(aParams, aLength, this, aFlags, aFlags2); -+ gfxTextRun::Create(aParams, aLength, this, aFlags, aFlags2, aUserContextId); - if (!textRun) { - return nullptr; - } -@@ -2515,11 +2539,11 @@ already_AddRefed gfxFontGroup::MakeTextRun( - template already_AddRefed gfxFontGroup::MakeTextRun( - const uint8_t* aString, uint32_t aLength, const Parameters* aParams, - gfx::ShapedTextFlags aFlags, nsTextFrameUtils::Flags aFlags2, -- gfxMissingFontRecorder* aMFR); -+ gfxMissingFontRecorder* aMFR, uint32_t aUserContextId); - template already_AddRefed gfxFontGroup::MakeTextRun( - const char16_t* aString, uint32_t aLength, const Parameters* aParams, - gfx::ShapedTextFlags aFlags, nsTextFrameUtils::Flags aFlags2, -- gfxMissingFontRecorder* aMFR); -+ gfxMissingFontRecorder* aMFR, uint32_t aUserContextId); - - // ComputeRanges instantiation (used by - // gfxPlatformFontList::ListFontsUsedForString). -@@ -2583,8 +2607,9 @@ static Script ResolveScriptForLang(const nsAtom* aLanguage, Script aDefault) { +@@ -2583,8 +2600,9 @@ static Script ResolveScriptForLang(const nsAtom* aLanguage, Script aDefault) { static LangScriptCache sCache; static RWLock sLock("LangScriptCache lock"); @@ -1082,7 +394,7 @@ index fc1293be42..45641bd8b3 100644 { // Try to use a cached value without taking an exclusive lock. -@@ -3418,7 +3443,7 @@ already_AddRefed gfxFontGroup::FindFontForChar( +@@ -3418,7 +3436,7 @@ already_AddRefed gfxFontGroup::FindFontForChar( font = FindFallbackFaceForChar(ff, aCh, aNextCh, presentation); if (font) { if (CheckCandidate(font, @@ -1092,136 +404,18 @@ index fc1293be42..45641bd8b3 100644 } } diff --git a/gfx/thebes/gfxTextRun.h b/gfx/thebes/gfxTextRun.h -index 85aecf5fdd..5fdfe522fc 100644 +index 85aecf5fdd..79f4df71a4 100644 --- a/gfx/thebes/gfxTextRun.h +++ b/gfx/thebes/gfxTextRun.h -@@ -485,13 +485,14 @@ class gfxTextRun : public gfxShapedText { - void ClearFlagBits(nsTextFrameUtils::Flags aFlags) { mFlags2 &= ~aFlags; } - const gfxSkipChars& GetSkipChars() const { return mSkipChars; } - gfxFontGroup* GetFontGroup() const { return mFontGroup; } -+ uint32_t GetUserContextId() const override { return mUserContextId; } - - // Call this, don't call "new gfxTextRun" directly. This does custom - // allocation and initialization - static already_AddRefed Create( - const gfxTextRunFactory::Parameters* aParams, uint32_t aLength, - gfxFontGroup* aFontGroup, mozilla::gfx::ShapedTextFlags aFlags, -- nsTextFrameUtils::Flags aFlags2); -+ nsTextFrameUtils::Flags aFlags2, uint32_t aUserContextId = 0); - - // The text is divided into GlyphRuns as necessary. (In the vast majority - // of cases, a gfxTextRun contains just a single GlyphRun.) -@@ -801,7 +802,7 @@ class gfxTextRun : public gfxShapedText { - */ - gfxTextRun(const gfxTextRunFactory::Parameters* aParams, uint32_t aLength, - gfxFontGroup* aFontGroup, mozilla::gfx::ShapedTextFlags aFlags, -- nsTextFrameUtils::Flags aFlags2); -+ nsTextFrameUtils::Flags aFlags2, uint32_t aUserContextId = 0); - - // Whether we need to fetch actual glyph extents from the fonts. - bool NeedsGlyphExtents() const; -@@ -893,6 +894,8 @@ class gfxTextRun : public gfxShapedText { - nsTextFrameUtils::Flags - mFlags2; // additional flags (see also gfxShapedText::mFlags) - -+ uint32_t mUserContextId; // user context ID for font spacing seed -+ - bool mDontSkipDrawing; // true if the text run must not skip drawing, even if - // waiting for a user font download, e.g. because we - // are using it to draw canvas text -@@ -970,7 +973,8 @@ class gfxFontGroup final : public gfxTextRunFactory { - const Parameters* aParams, - mozilla::gfx::ShapedTextFlags aFlags, - nsTextFrameUtils::Flags aFlags2, -- gfxMissingFontRecorder* aMFR); -+ gfxMissingFontRecorder* aMFR, -+ uint32_t aUserContextId = 0); - - /** - * Textrun creation helper for clients that don't want to pass -@@ -982,10 +986,11 @@ class gfxFontGroup final : public gfxTextRunFactory { - int32_t aAppUnitsPerDevUnit, - mozilla::gfx::ShapedTextFlags aFlags, - nsTextFrameUtils::Flags aFlags2, -- gfxMissingFontRecorder* aMFR) { -+ gfxMissingFontRecorder* aMFR, -+ uint32_t aUserContextId = 0) { - gfxTextRunFactory::Parameters params = { - aRefDrawTarget, nullptr, nullptr, nullptr, 0, aAppUnitsPerDevUnit}; -- return MakeTextRun(aString, aLength, ¶ms, aFlags, aFlags2, aMFR); -+ return MakeTextRun(aString, aLength, ¶ms, aFlags, aFlags2, aMFR, aUserContextId); - } - - // Get the (possibly-cached) width of the hyphen character. -@@ -1401,6 +1406,8 @@ class gfxFontGroup final : public gfxTextRunFactory { +@@ -1401,6 +1401,8 @@ class gfxFontGroup final : public gfxTextRunFactory { uint32_t mFontListGeneration = 0; // platform font list generation for this // fontgroup -+ uint32_t mUserContextId = 0; // user context ID for font spacing seed ++ uint32_t mUserContextId = 0; // selects the per-context font list + /** * Textrun creation short-cuts for special cases where we don't need to * call a font shaper to generate glyphs. -diff --git a/layout/base/nsLayoutUtils.cpp b/layout/base/nsLayoutUtils.cpp -index 585561b2a0..1e76b87091 100644 ---- a/layout/base/nsLayoutUtils.cpp -+++ b/layout/base/nsLayoutUtils.cpp -@@ -144,6 +144,7 @@ - #include "nsIFrameInlines.h" - #include "nsIImageLoadingContent.h" - #include "nsIInterfaceRequestorUtils.h" -+#include "mozilla/dom/BrowsingContext.h" - #include "nsIWidget.h" - #include "nsListControlFrame.h" - #include "nsMenuPopupFrame.h" -@@ -1212,6 +1213,31 @@ int32_t nsLayoutUtils::DoCompareTreePosition(const nsIFrame* aFrame1, - nonCommonAncestor ? aCommonAncestor : nullptr); - } - -+/* static */ uint32_t -+nsLayoutUtils::GetUserContextId(nsIFrame* aFrame) -+{ -+ if (!aFrame || !aFrame->GetContent()) { -+ return 0; -+ } -+ -+ mozilla::dom::Document* doc = aFrame->GetContent()->GetComposedDoc(); -+ if (!doc) { -+ return 0; -+ } -+ -+ nsPIDOMWindowInner* win = doc->GetInnerWindow(); -+ if (!win) { -+ return 0; -+ } -+ -+ mozilla::dom::BrowsingContext* bc = win->GetBrowsingContext(); -+ if (!bc) { -+ return 0; -+ } -+ -+ return bc->OriginAttributesRef().mUserContextId; -+} -+ - // static - int32_t nsLayoutUtils::DoCompareTreePosition( - const nsIFrame* aFrame1, const nsIFrame* aFrame2, -diff --git a/layout/base/nsLayoutUtils.h b/layout/base/nsLayoutUtils.h -index b453efb3c7..0bbe0f36f7 100644 ---- a/layout/base/nsLayoutUtils.h -+++ b/layout/base/nsLayoutUtils.h -@@ -439,6 +439,11 @@ class nsLayoutUtils { - */ - static nsIFrame* GetLastSibling(nsIFrame* aFrame); - -+ /** -+ * Get the user context ID from a frame's document context -+ */ -+ static uint32_t GetUserContextId(nsIFrame* aFrame); -+ - /** - * FindSiblingViewFor locates the child of aParentView that aFrame's - * view should be inserted 'above' (i.e., before in sibling view diff --git a/layout/base/nsPresContext.cpp b/layout/base/nsPresContext.cpp index b2394c6106..da755e553c 100644 --- a/layout/base/nsPresContext.cpp @@ -1237,186 +431,6 @@ index b2394c6106..da755e553c 100644 void nsPresContext::GetUserPreferences() { if (!GetPresShell()) { // No presshell means nothing to do here. We'll do this when we -diff --git a/layout/generic/MathMLTextRunFactory.cpp b/layout/generic/MathMLTextRunFactory.cpp -index 22adbaf9bc..e685fd4feb 100644 ---- a/layout/generic/MathMLTextRunFactory.cpp -+++ b/layout/generic/MathMLTextRunFactory.cpp -@@ -9,6 +9,8 @@ - #include "mozilla/ComputedStyleInlines.h" - #include "mozilla/StaticPrefs_mathml.h" - #include "mozilla/intl/UnicodeScriptCodes.h" -+#include "mozilla/dom/BrowsingContext.h" -+#include "nsPIDOMWindow.h" - #include "nsDeviceContext.h" - #include "nsFontMetrics.h" - #include "nsStyleConsts.h" -@@ -635,6 +637,18 @@ void MathMLTextRunFactory::RebuildTextRun( - newFontGroup = fontGroup; - } - -+ // Extract user context ID via BrowsingContext (canonical source) -+ uint32_t userContextId = 0; -+ if (length && styles[0]->mPresContext) { -+ if (mozilla::dom::Document* doc = styles[0]->mPresContext->Document()) { -+ if (nsPIDOMWindowInner* win = doc->GetInnerWindow()) { -+ if (mozilla::dom::BrowsingContext* bc = win->GetBrowsingContext()) { -+ userContextId = bc->OriginAttributesRef().mUserContextId; -+ } -+ } -+ } -+ } -+ - if (mInnerTransformingTextRunFactory) { - transformedChild = mInnerTransformingTextRunFactory->MakeTextRun( - convertedString.BeginReading(), convertedString.Length(), &innerParams, -@@ -644,7 +658,7 @@ void MathMLTextRunFactory::RebuildTextRun( - } else { - cachedChild = newFontGroup->MakeTextRun( - convertedString.BeginReading(), convertedString.Length(), &innerParams, -- flags, nsTextFrameUtils::Flags(), aMFR); -+ flags, nsTextFrameUtils::Flags(), aMFR, userContextId); - child = cachedChild.get(); - } - if (!child) { -diff --git a/layout/generic/nsTextFrame.cpp b/layout/generic/nsTextFrame.cpp -index 6c3615f90c..448aed22e7 100644 ---- a/layout/generic/nsTextFrame.cpp -+++ b/layout/generic/nsTextFrame.cpp -@@ -2344,10 +2344,11 @@ static already_AddRefed GetHyphenTextRun(nsTextFrame* aTextFrame, - return fontGroup->MakeHyphenTextRun(dt, flags, appPerDev); - } - auto* missingFonts = aTextFrame->PresContext()->MissingFontRecorder(); -+ uint32_t userContextId = nsLayoutUtils::GetUserContextId(aTextFrame); - const NS_ConvertUTF8toUTF16 hyphenStr(hyphenateChar.AsString().AsString()); - return fontGroup->MakeTextRun(hyphenStr.BeginReading(), hyphenStr.Length(), - dt, appPerDev, flags, nsTextFrameUtils::Flags(), -- missingFonts); -+ missingFonts, userContextId); - } - - already_AddRefed BuildTextRunsScanner::BuildTextRunForFrames( -@@ -2698,6 +2699,9 @@ already_AddRefed BuildTextRunsScanner::BuildTextRunForFrames( - "We didn't cover all the characters in the text run!"); - } - -+ // Get user context ID for font spacing seed -+ uint32_t userContextId = nsLayoutUtils::GetUserContextId(firstFrame); -+ - RefPtr textRun; - gfxTextRunFactory::Parameters params = { - mDrawTarget, -@@ -2715,7 +2719,7 @@ already_AddRefed BuildTextRunsScanner::BuildTextRunForFrames( - std::move(styles), true); - } else { - textRun = fontGroup->MakeTextRun(text, transformedLength, ¶ms, flags, -- flags2, mMissingFonts); -+ flags2, mMissingFonts, userContextId); - } - } else { - const uint8_t* text = static_cast(textPtr); -@@ -2726,7 +2730,7 @@ already_AddRefed BuildTextRunsScanner::BuildTextRunForFrames( - std::move(styles), true); - } else { - textRun = fontGroup->MakeTextRun(text, transformedLength, ¶ms, flags, -- flags2, mMissingFonts); -+ flags2, mMissingFonts, userContextId); - } - } - if (!textRun) { -@@ -5585,6 +5589,7 @@ static already_AddRefed GenerateTextRunForEmphasisMarks( - - RefPtr dt = CreateReferenceDrawTarget(aFrame); - auto appUnitsPerDevUnit = aFrame->PresContext()->AppUnitsPerDevPixel(); -+ uint32_t userContextId = nsLayoutUtils::GetUserContextId(aFrame); - gfx::ShapedTextFlags flags = - nsLayoutUtils::GetTextRunOrientFlagsForStyle(aComputedStyle); - if (flags == gfx::ShapedTextFlags::TEXT_ORIENT_VERTICAL_MIXED) { -@@ -5593,7 +5598,8 @@ static already_AddRefed GenerateTextRunForEmphasisMarks( - } - return aFontGroup->MakeTextRun(string.get(), string.Length(), dt, - appUnitsPerDevUnit, flags, -- nsTextFrameUtils::Flags(), nullptr); -+ nsTextFrameUtils::Flags(), nullptr, -+ userContextId); - } - - static nsRubyFrame* FindFurthestInlineRubyAncestor(nsTextFrame* aFrame) { -diff --git a/layout/generic/nsTextRunTransformations.cpp b/layout/generic/nsTextRunTransformations.cpp -index f1f0c2ef92..59b59112f1 100644 ---- a/layout/generic/nsTextRunTransformations.cpp -+++ b/layout/generic/nsTextRunTransformations.cpp -@@ -908,6 +908,10 @@ void nsCaseTransformTextRunFactory::RebuildTextRun( - RefPtr cachedChild; - gfxTextRun* child; - -+ // Text transformation contexts don't have direct access to document/frame context, -+ // so we cannot extract private browsing ID. Use 0 (default context). -+ uint32_t privateBrowsingId = 0; -+ - if (mInnerTransformingTextRunFactory) { - transformedChild = mInnerTransformingTextRunFactory->MakeTextRun( - convertedString.BeginReading(), convertedString.Length(), &innerParams, -@@ -917,7 +921,7 @@ void nsCaseTransformTextRunFactory::RebuildTextRun( - } else { - cachedChild = fontGroup->MakeTextRun( - convertedString.BeginReading(), convertedString.Length(), &innerParams, -- flags, nsTextFrameUtils::Flags(), aMFR); -+ flags, nsTextFrameUtils::Flags(), aMFR, privateBrowsingId); - child = cachedChild.get(); - } - if (!child) { -diff --git a/layout/mathml/nsMathMLChar.cpp b/layout/mathml/nsMathMLChar.cpp -index befaf7fed8..2f818fd962 100644 ---- a/layout/mathml/nsMathMLChar.cpp -+++ b/layout/mathml/nsMathMLChar.cpp -@@ -19,6 +19,8 @@ - #include "mozilla/StaticPrefs_mathml.h" - #include "mozilla/UniquePtr.h" - #include "mozilla/dom/Document.h" -+#include "mozilla/dom/BrowsingContext.h" -+#include "nsPIDOMWindow.h" - #include "mozilla/gfx/2D.h" - #include "mozilla/intl/UnicodeScriptCodes.h" - #include "nsCSSRendering.h" -@@ -271,7 +273,7 @@ already_AddRefed nsUnicodeTable::MakeTextRun( - "nsUnicodeTable can only access glyphs by code point"); - return aFontGroup->MakeTextRun(&aGlyph.code, 1, aDrawTarget, - aAppUnitsPerDevPixel, gfx::ShapedTextFlags(), -- nsTextFrameUtils::Flags(), nullptr); -+ nsTextFrameUtils::Flags(), nullptr, 0); // TODO: Extract private browsing ID - } - - // An instance of nsOpenTypeTable is associated with one gfxFontEntry that -@@ -343,7 +345,7 @@ void nsOpenTypeTable::UpdateCache(DrawTarget* aDrawTarget, - if (mCharCache != aChar) { - RefPtr textRun = - aFontGroup->MakeTextRun(&aChar, 1, aDrawTarget, aAppUnitsPerDevPixel, -- mFlags, nsTextFrameUtils::Flags(), nullptr); -+ mFlags, nsTextFrameUtils::Flags(), nullptr, 0); - const gfxTextRun::CompressedGlyph& data = textRun->GetCharacterGlyphs()[0]; - if (data.IsSimpleGlyph()) { - mGlyphID = data.GetSimpleGlyph(); -@@ -1216,10 +1218,19 @@ nsresult nsMathMLChar::StretchInternal( - flags |= gfx::ShapedTextFlags::TEXT_IS_RTL; - } - -+ uint32_t userContextId = 0; -+ if (mozilla::dom::Document* doc = presContext->Document()) { -+ if (nsPIDOMWindowInner* win = doc->GetInnerWindow()) { -+ if (mozilla::dom::BrowsingContext* bc = win->GetBrowsingContext()) { -+ userContextId = bc->OriginAttributesRef().mUserContextId; -+ } -+ } -+ } -+ - mGlyphs[0] = fm->GetThebesFontGroup()->MakeTextRun( - static_cast(mData.get()), len, aDrawTarget, - presContext->AppUnitsPerDevPixel(), flags, nsTextFrameUtils::Flags(), -- presContext->MissingFontRecorder()); -+ presContext->MissingFontRecorder(), userContextId); - aDesiredStretchSize = MeasureTextRun(aDrawTarget, mGlyphs[0].get()); - - bool maxWidth = aStretchFlags.contains(MathMLStretchFlag::MaxWidth); diff --git a/toolkit/components/resistfingerprinting/FontVisibilityProvider.h b/toolkit/components/resistfingerprinting/FontVisibilityProvider.h index 2f71aae0f9..c29d5ad5b5 100644 --- a/toolkit/components/resistfingerprinting/FontVisibilityProvider.h diff --git a/patches/patch-dependencies.md b/patches/patch-dependencies.md index 0fdcf68..cd2570f 100644 --- a/patches/patch-dependencies.md +++ b/patches/patch-dependencies.md @@ -22,7 +22,6 @@ be listed there. | Patch | Config keys | |-------|-------------| -| `anti-font-fingerprinting.patch` | `fonts:spacing_seed` | | `audio-context-spoofing.patch` | `AudioContext:outputLatency` | | `audio-fingerprint-manager.patch` | `audio:seed` | | `chromeutil.patch` | `debug` | @@ -50,7 +49,7 @@ To regenerate the list: `grep -l 'MaskConfig::' patches/*.patch`. ## RoverfoxStorageManager -Per-context values set from Playwright (font spacing seed, WebRTC IP, timezone, +Per-context values set from Playwright (audio seed, WebRTC IP, timezone, screen, navigator, voices, ...) are kept in `RoverfoxStorageManager`, which `anti-font-fingerprinting.patch` adds under `dom/base/`. Its cross-process put/get IPC lives in `cross-process-storage.patch`. Any patch that uses the diff --git a/patches/timezone-spoofing.patch b/patches/timezone-spoofing.patch index 9fdf148..8d3f367 100644 --- a/patches/timezone-spoofing.patch +++ b/patches/timezone-spoofing.patch @@ -134,10 +134,10 @@ index 0000000000..888303c315 + +#endif // mozilla_dom_TimezoneManager_h diff --git a/dom/base/moz.build b/dom/base/moz.build -index 743be1950f..922a79af86 100644 +index 56e0798c8b..eab5894010 100644 --- a/dom/base/moz.build +++ b/dom/base/moz.build -@@ -285,6 +285,7 @@ EXPORTS.mozilla.dom += [ +@@ -284,6 +284,7 @@ EXPORTS.mozilla.dom += [ "TimeoutBudgetManager.h", "TimeoutHandler.h", "TimeoutManager.h", @@ -145,7 +145,7 @@ index 743be1950f..922a79af86 100644 "TreeIterator.h", "TreeOrderedArray.h", "TreeOrderedArrayInlines.h", -@@ -503,6 +504,7 @@ UNIFIED_SOURCES += [ +@@ -501,6 +502,7 @@ UNIFIED_SOURCES += [ "TimeoutExecutor.cpp", "TimeoutHandler.cpp", "TimeoutManager.cpp", @@ -154,20 +154,20 @@ index 743be1950f..922a79af86 100644 "UIDirectionManager.cpp", "UserActivation.cpp", diff --git a/dom/base/nsGlobalWindowInner.cpp b/dom/base/nsGlobalWindowInner.cpp -index 6444fccd9c..c0b2671d5b 100644 +index fb9c5c69d3..d4453fda3f 100644 --- a/dom/base/nsGlobalWindowInner.cpp +++ b/dom/base/nsGlobalWindowInner.cpp -@@ -249,6 +249,8 @@ +@@ -248,6 +248,8 @@ + #include "nsICookieService.h" #include "nsID.h" #include "nsIDOMStorageManager.h" - #include "FontSpacingSeedManager.h" +#include "TimezoneManager.h" +#include "js/Date.h" #include "nsDocShell.h" #include "mozilla/OriginAttributes.h" #include "nsIDOMXULControlElement.h" -@@ -7781,6 +7783,50 @@ void nsGlobalWindowInner::SetFontSpacingSeed(uint32_t seed, ErrorResult& aRv) { - } +@@ -7763,6 +7765,50 @@ IntlUtils* nsGlobalWindowInner::GetIntlUtils(ErrorResult& aError) { + return mIntlUtils; } +void nsGlobalWindowInner::SetTimezone(const nsAString& timezone, ErrorResult& aRv) { @@ -218,17 +218,18 @@ index 6444fccd9c..c0b2671d5b 100644 MOZ_ASSERT(aSharedWorker); MOZ_ASSERT(!mSharedWorkers.Contains(aSharedWorker)); diff --git a/dom/base/nsGlobalWindowInner.h b/dom/base/nsGlobalWindowInner.h -index bb70b2b8fe..bfde19786c 100644 +index 43600d66bf..1f9f283026 100644 --- a/dom/base/nsGlobalWindowInner.h +++ b/dom/base/nsGlobalWindowInner.h -@@ -685,6 +685,7 @@ class nsGlobalWindowInner final : public mozilla::dom::EventTarget, +@@ -683,6 +683,8 @@ class nsGlobalWindowInner final : public mozilla::dom::EventTarget, + + mozilla::dom::IntlUtils* GetIntlUtils(mozilla::ErrorResult& aRv); - // Font spacing seed for privacy-preserving font fingerprinting - void SetFontSpacingSeed(uint32_t seed, mozilla::ErrorResult& aRv); + void SetTimezone(const nsAString& timezone, mozilla::ErrorResult& aRv); - ++ void StoreSharedWorker(mozilla::dom::SharedWorker* aSharedWorker); + void ForgetSharedWorker(mozilla::dom::SharedWorker* aSharedWorker); diff --git a/dom/base/nsGlobalWindowOuter.cpp b/dom/base/nsGlobalWindowOuter.cpp index dd0124f7e9..549e0869d0 100644 --- a/dom/base/nsGlobalWindowOuter.cpp @@ -306,10 +307,10 @@ index 21d69bdfed..fa2a7e3be3 100644 MOZ_ASSERT(NS_IsMainThread()); diff --git a/dom/webidl/Window.webidl b/dom/webidl/Window.webidl -index 6448765fb8..f01e8011cc 100644 +index 162c95ba3d..b20d7c1f6f 100644 --- a/dom/webidl/Window.webidl +++ b/dom/webidl/Window.webidl -@@ -958,6 +958,12 @@ partial interface Window { +@@ -952,6 +952,12 @@ partial interface Window { undefined setSpeechVoices(DOMString voices); }; diff --git a/patches/webrtc-ip-spoofing.patch b/patches/webrtc-ip-spoofing.patch index bc81361..2d27cda 100644 --- a/patches/webrtc-ip-spoofing.patch +++ b/patches/webrtc-ip-spoofing.patch @@ -262,10 +262,10 @@ index 0000000000..c9810a06d2 + +#endif // mozilla_dom_WebRTCIPManager_h diff --git a/dom/base/moz.build b/dom/base/moz.build -index b718f44036..52a61b0ca1 100644 +index ba674a00d9..ef7c9bdff2 100644 --- a/dom/base/moz.build +++ b/dom/base/moz.build -@@ -297,10 +297,15 @@ EXPORTS.mozilla.dom += [ +@@ -296,10 +296,15 @@ EXPORTS.mozilla.dom += [ "VideoFrameProvider.h", "ViewportMetaData.h", "VisualViewport.h", @@ -282,10 +282,10 @@ index b718f44036..52a61b0ca1 100644 # in unified builds (it includes RoverfoxStorageManager.h which can affect # alphabetically-later files like BarProps.cpp) diff --git a/dom/base/nsGlobalWindowInner.cpp b/dom/base/nsGlobalWindowInner.cpp -index 007e8f5eae..7cdaf2767c 100644 +index 740c59ed8b..4b7d3d1297 100644 --- a/dom/base/nsGlobalWindowInner.cpp +++ b/dom/base/nsGlobalWindowInner.cpp -@@ -335,6 +335,10 @@ +@@ -334,6 +334,10 @@ #include "xpcprivate.h" #include "xpcpublic.h" @@ -296,7 +296,7 @@ index 007e8f5eae..7cdaf2767c 100644 #ifdef NS_PRINTING # include "nsIPrintSettings.h" #endif -@@ -7826,6 +7830,42 @@ void nsGlobalWindowInner::SetTimezone(const nsAString& timezone, ErrorResult& aR +@@ -7810,6 +7814,42 @@ void nsGlobalWindowInner::SetTimezone(const nsAString& timezone, ErrorResult& aR } } @@ -340,11 +340,11 @@ index 007e8f5eae..7cdaf2767c 100644 MOZ_ASSERT(aSharedWorker); MOZ_ASSERT(!mSharedWorkers.Contains(aSharedWorker)); diff --git a/dom/base/nsGlobalWindowInner.h b/dom/base/nsGlobalWindowInner.h -index 17e00f408f..d709b07f7d 100644 +index 9199f08fdf..66f858aad5 100644 --- a/dom/base/nsGlobalWindowInner.h +++ b/dom/base/nsGlobalWindowInner.h -@@ -687,6 +687,10 @@ class nsGlobalWindowInner final : public mozilla::dom::EventTarget, - void SetFontSpacingSeed(uint32_t seed, mozilla::ErrorResult& aRv); +@@ -685,6 +685,10 @@ class nsGlobalWindowInner final : public mozilla::dom::EventTarget, + void SetTimezone(const nsAString& timezone, mozilla::ErrorResult& aRv); + // WebRTC IP addresses for privacy-preserving IP spoofing @@ -1117,10 +1117,10 @@ index d32e08c2b0..40a750196a 100644 +# DOM Mask +LOCAL_INCLUDES += ["/camoucfg"] diff --git a/dom/webidl/Window.webidl b/dom/webidl/Window.webidl -index 3a13d4963f..6b4c7101fa 100644 +index 2471488e16..b7833efded 100644 --- a/dom/webidl/Window.webidl +++ b/dom/webidl/Window.webidl -@@ -964,6 +964,17 @@ partial interface Window { +@@ -958,6 +958,17 @@ partial interface Window { undefined setTimezone(DOMString timezone); }; diff --git a/patches/window-setter-seal.patch b/patches/window-setter-seal.patch index fbb45c3..4ab4f34 100644 --- a/patches/window-setter-seal.patch +++ b/patches/window-setter-seal.patch @@ -13,10 +13,11 @@ index 6ec4dc5265..d8a5a5aac0 100644 uint32_t userContextId = 0; if (BrowsingContext* bc = win->GetBrowsingContext()) { diff --git a/dom/base/ChromeUtils.cpp b/dom/base/ChromeUtils.cpp -index 8657d3282b..d8889edb6a 100644 +index 9a04e43224..fb7f231799 100644 --- a/dom/base/ChromeUtils.cpp +++ b/dom/base/ChromeUtils.cpp -@@ -6,5 +6,9 @@ +@@ -5,6 +5,10 @@ + #include "ChromeUtils.h" #include "MaskConfig.hpp" +// Camoufox: for CamouSealFingerprintSetters below. @@ -39,7 +40,7 @@ index 8657d3282b..d8889edb6a 100644 + "setScreenDimensions", "setScreenColorDepth", + "setWebGLVendor", "setWebGLRenderer", + "setWebRTCIPv4", "setWebRTCIPv6", -+ "setFontList", "setFontSpacingSeed", ++ "setFontList", + "setAudioFingerprintSeed", "setSpeechVoices", + "setTimezone", +}; @@ -102,7 +103,7 @@ index 8657d3282b..d8889edb6a 100644 bool ChromeUtils::ShouldResistFingerprinting( GlobalObject& aGlobal, JSRFPTarget aTarget, diff --git a/dom/base/ChromeUtils.h b/dom/base/ChromeUtils.h -index e32ee77dfd..8747d75c44 100644 +index f1de113a05..0937a561f3 100644 --- a/dom/base/ChromeUtils.h +++ b/dom/base/ChromeUtils.h @@ -361,6 +361,12 @@ class ChromeUtils { @@ -132,20 +133,6 @@ index 438bad576d..5fd40e4e48 100644 uint32_t id = 0; if (BrowsingContext* bc = win->GetBrowsingContext()) { id = bc->OriginAttributesRef().mUserContextId; -diff --git a/dom/base/FontSpacingSeedManager.cpp b/dom/base/FontSpacingSeedManager.cpp -index e07de3e753..f16422badf 100644 ---- a/dom/base/FontSpacingSeedManager.cpp -+++ b/dom/base/FontSpacingSeedManager.cpp -@@ -76,6 +76,9 @@ FontSpacingSeedManager::IsFunctionEnabledForWebIDL(JSContext* aCx, JSObject* aOb - if (!win) { - return false; - } -+ // Camoufox: sealed once this window's init scripts have run, so page -+ // script never sees the setter (FrameTree -> camouSealFingerprintSetters). -+ if (win->CamouSettersSealed()) return false; - - uint32_t userContextId = 0; - if (BrowsingContext* bc = win->GetBrowsingContext()) { diff --git a/dom/base/NavigatorManager.cpp b/dom/base/NavigatorManager.cpp index 37a1713159..e53e97de25 100644 --- a/dom/base/NavigatorManager.cpp @@ -229,10 +216,10 @@ index 0119707221..511e92e1f2 100644 if (BrowsingContext* bc = win->GetBrowsingContext()) { id = bc->OriginAttributesRef().mUserContextId; diff --git a/dom/base/TimezoneManager.cpp b/dom/base/TimezoneManager.cpp -index ffbc624040..548ff14a78 100644 +index 206a027918..cd35b265ad 100644 --- a/dom/base/TimezoneManager.cpp +++ b/dom/base/TimezoneManager.cpp -@@ -56,6 +56,9 @@ TimezoneManager::IsFunctionEnabledForWebIDL(JSContext* aCx, JSObject* aObj) { +@@ -75,6 +75,9 @@ TimezoneManager::IsFunctionEnabledForWebIDL(JSContext* aCx, JSObject* aObj) { if (!win) { return false; } @@ -291,11 +278,11 @@ index 8834c93e4b..fa807ad165 100644 uint32_t userContextId = 0; if (BrowsingContext* bc = win->GetBrowsingContext()) { diff --git a/dom/base/nsGlobalWindowInner.h b/dom/base/nsGlobalWindowInner.h -index d709b07f7d..95184adcef 100644 +index 66f858aad5..65ccfc6ea5 100644 --- a/dom/base/nsGlobalWindowInner.h +++ b/dom/base/nsGlobalWindowInner.h -@@ -687,6 +687,36 @@ class nsGlobalWindowInner final : public mozilla::dom::EventTarget, - void SetFontSpacingSeed(uint32_t seed, mozilla::ErrorResult& aRv); +@@ -685,6 +685,36 @@ class nsGlobalWindowInner final : public mozilla::dom::EventTarget, + void SetTimezone(const nsAString& timezone, mozilla::ErrorResult& aRv); + // Camoufox: the window.setXxx() fingerprint setters are configuration API @@ -332,7 +319,7 @@ index d709b07f7d..95184adcef 100644 void SetWebRTCIPv4(const nsAString& ipv4, mozilla::ErrorResult& aRv); void SetWebRTCIPv6(const nsAString& ipv6, mozilla::ErrorResult& aRv); diff --git a/dom/chrome-webidl/ChromeUtils.webidl b/dom/chrome-webidl/ChromeUtils.webidl -index 4c15c49fef..33aa276db2 100644 +index 31c1212350..5f655f6714 100644 --- a/dom/chrome-webidl/ChromeUtils.webidl +++ b/dom/chrome-webidl/ChromeUtils.webidl @@ -938,6 +938,20 @@ partial namespace ChromeUtils { diff --git a/pythonlib/camoufox/fingerprints.py b/pythonlib/camoufox/fingerprints.py index b13830e..2c467aa 100644 --- a/pythonlib/camoufox/fingerprints.py +++ b/pythonlib/camoufox/fingerprints.py @@ -1645,9 +1645,6 @@ def from_preset(preset: Dict, ff_version: Optional[str] = None, salt: Optional[i config['webGl:renderer'] = webgl['unmaskedRenderer'] # Generate a unique audio seed per launch (1 to 2^32-1, excluding 0 which is a no-op in C++) - # fonts:spacing_seed stays 0 (off): glyph-advance perturbation produces text - # widths no real machine emits (see launch_options in utils.py). - config['fonts:spacing_seed'] = 0 config['audio:seed'] = randint(1, 4_294_967_295) # nosec if preset.get('timezone'): @@ -1697,7 +1694,6 @@ def _build_init_script(values: Dict[str, Any]) -> str: lines = ['(function(v) {', ' var w = window;'] setters = [ - ('fontSpacingSeed', 'setFontSpacingSeed', '{val}'), ('audioFingerprintSeed', 'setAudioFingerprintSeed', '{val}'), ('navigatorPlatform', 'setNavigatorPlatform', '{val}'), ('navigatorOscpu', 'setNavigatorOscpu', '{val}'), @@ -1798,8 +1794,7 @@ def generate_context_fingerprint( normalize_locale() and injected into config. Also sets context_options['locale'] for Playwright. config_overrides: Dict of CAMOU_CONFIG keys to override after config - is built but before init_script is rendered. Useful for disabling - perturbation (e.g. {'fonts:spacing_seed': 0}). + is built but before init_script is rendered (e.g. {'audio:seed': 7}). """ if preset is not None: # Use real fingerprint preset @@ -1816,7 +1811,6 @@ def generate_context_fingerprint( _salt = identity_salt() # Add seeds (the generator doesn't produce these) - config.setdefault('fonts:spacing_seed', 0) # perturbation off; see utils.launch_options config.setdefault('audio:seed', randint(1, 4_294_967_295)) # nosec # Determine target OS from platform for font/voice generation @@ -1912,7 +1906,6 @@ def generate_context_fingerprint( # Build the values dict for the init script (works for both paths) init_values: Dict[str, Any] = { - 'fontSpacingSeed': config.get('fonts:spacing_seed'), 'audioFingerprintSeed': config.get('audio:seed'), 'navigatorPlatform': nav.get('platform'), 'navigatorOscpu': config.get('navigator.oscpu'), diff --git a/pythonlib/camoufox/utils.py b/pythonlib/camoufox/utils.py index 5338d77..ab77595 100644 --- a/pythonlib/camoufox/utils.py +++ b/pythonlib/camoufox/utils.py @@ -1267,18 +1267,12 @@ def launch_options( if not _user_set_accept_encoding: config.pop('headers.Accept-Encoding', None) - # Set random seeds for fingerprint noise (per launch) - # Glyph-advance perturbation is OFF by default (seed 0): it moves every - # measured text width off the value the same font produces on a real - # machine (measured 2026-09-14: +1 px per ~100 glyphs, fractional deltas - # on every measureText), which is a fingerprint no stock Firefox emits. - # Pass fonts:spacing_seed explicitly to opt back in. - set_into(config, 'fonts:spacing_seed', 0) # The audio noise seed follows the identity: a returning "same device" must # reproduce its audio hash (#442/#765). Never 0 (0 disables the noise). A # preset draws its own random seed; it is replaced here too so a pinned # preset reproduces it, but a seed the caller set is kept. There is no - # canvas seed: the browser adds no canvas noise (#528). + # canvas seed: the browser adds no canvas noise (#528), and no glyph-spacing + # noise either (ci/tribal-rules.yml: no-glyph-spacing-noise). if not _user_set_audio_seed: _ident = identity_seed(config, _identity_salt) config['audio:seed'] = ((_ident * 2654435761 + 97) & 0xFFFFFFFF) or 1 diff --git a/pythonlib/tests/test_identity_salt.py b/pythonlib/tests/test_identity_salt.py index c61c0d6..50ffe26 100644 --- a/pythonlib/tests/test_identity_salt.py +++ b/pythonlib/tests/test_identity_salt.py @@ -178,3 +178,19 @@ def test_fingerprint_preset_off_never_draws_a_preset(off): checked with `is not None`, so False drew a random bundled preset.""" with mock.patch.object(utils, "get_random_preset", side_effect=AssertionError("preset drawn")): launch(fingerprint_preset=off) + + +def test_no_glyph_spacing_seed_is_generated(): + """Glyph-spacing noise moved every measured text width off what the same + font gives on a real machine, so it was itself a fingerprint; the feature + is gone from the browser, and the launcher sends nothing for it.""" + assert "fonts:spacing_seed" not in launch() + context = fp.generate_context_fingerprint(os="linux") + assert "fonts:spacing_seed" not in context["config"] + assert "setFontSpacingSeed" not in context["init_script"] + + +def test_config_overrides_reach_the_config_and_the_init_script(): + context = fp.generate_context_fingerprint(os="linux", config_overrides={"audio:seed": 7}) + assert context["config"]["audio:seed"] == 7 + assert "setAudioFingerprintSeed(7)" in context["init_script"] diff --git a/settings/properties.json b/settings/properties.json index b55153d..4aee6b6 100644 --- a/settings/properties.json +++ b/settings/properties.json @@ -45,7 +45,6 @@ { "property": "battery:dischargingTime", "type": "double" }, { "property": "battery:level", "type": "double" }, { "property": "fonts", "type": "array" }, - { "property": "fonts:spacing_seed", "type": "uint" }, { "property": "audio:seed", "type": "uint" }, { "property": "geolocation:latitude", "type": "double" }, { "property": "geolocation:longitude", "type": "double" }, diff --git a/tests/patches/config-overrides.py b/tests/patches/config-overrides.py deleted file mode 100644 index 83ae176..0000000 --- a/tests/patches/config-overrides.py +++ /dev/null @@ -1,151 +0,0 @@ -""" -Verify that config_overrides={'fonts:spacing_seed': 0} disables font spacing perturbation. - -The bug: there was no way to disable font spacing perturbation through the Python API. -generate_context_fingerprint() always generated a random non-zero seed, and the caller -couldn't override it because init_script was already rendered by the time config was -returned. config_overrides applies after config is built but before init_script is -rendered, giving callers a clean override point. - -Run: - cd ~/20tech/drivingtest/dvsa-bot - uv run python ~/20tech/oss/camoufox/tests/patches/2026-04-30-font-spacing-seed-override.py -""" - -import asyncio -import sys - -from helpers import MAX_PRESET_ATTEMPTS - - -async def test(): - from camoufox.async_api import AsyncCamoufox - from camoufox.fingerprints import generate_context_fingerprint, get_random_preset - - test_string = "The quick brown fox jumps over the lazy dog" - failures = [] - - # --- Test 1: config_overrides disables font spacing perturbation --- - print("=== Test 1: config_overrides={'fonts:spacing_seed': 0} ===") - - last_error = None - for attempt in range(MAX_PRESET_ATTEMPTS): - preset = get_random_preset(os="macos") - fp = generate_context_fingerprint( - preset=preset, - config_overrides={"fonts:spacing_seed": 0}, - ) - - if fp["config"]["fonts:spacing_seed"] != 0: - failures.append( - f"Config seed is {fp['config']['fonts:spacing_seed']}, expected 0" - ) - break - - try: - async with AsyncCamoufox( - fingerprint_preset=fp["preset"], - headless=True, - os="macos", - ) as browser: - context = await browser.new_context(**fp["context_options"]) - await context.add_init_script(fp["init_script"]) - page = await context.new_page() - await page.goto("about:blank") - - widths = await page.evaluate( - """(testStr) => { - const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d'); - const results = []; - for (let i = 0; i < 5; i++) { - ctx.font = '16px Arial'; - results.push(ctx.measureText(testStr).width); - } - return results; - }""", - test_string, - ) - - unique = set(widths) - if len(unique) == 1: - print(f" Measurements stable (all {widths[0]}): PASS") - else: - failures.append(f"Measurements unstable with seed=0: {widths}") - print(f" Measurements vary: {widths}: FAIL") - break - except ValueError as e: - if "WebGL" in str(e): - last_error = e - continue - raise - else: - raise RuntimeError("Could not find a valid preset") from last_error - - # --- Test 2: without config_overrides, the perturbation is OFF (seed 0) --- - # Glyph-advance perturbation moves every measured text width off the value - # the same font gives on a real machine, so the default is 0; an explicit - # non-zero seed must still be honoured (opt-in). - print("\n=== Test 2: default (no overrides) seed is 0, explicit seed honoured ===") - preset2 = get_random_preset(os="macos") - fp2 = generate_context_fingerprint(preset=preset2) - seed2 = fp2["config"]["fonts:spacing_seed"] - if seed2 == 0: - print(" Default seed is 0 (perturbation off): PASS") - else: - failures.append(f"Default seed is {seed2} — should be 0 (perturbation off by default)") - print(f" Default seed is {seed2}: FAIL") - fp2b = generate_context_fingerprint(preset=preset2, config_overrides={"fonts:spacing_seed": 12345}) - if fp2b["config"]["fonts:spacing_seed"] == 12345: - print(" Explicit seed 12345 honoured: PASS") - else: - failures.append("Explicit fonts:spacing_seed override was not honoured") - print(" Explicit seed override: FAIL") - - # --- Test 3: init_script contains setFontSpacingSeed(0) when overridden --- - print("\n=== Test 3: init_script emits setFontSpacingSeed(0) ===") - preset3 = get_random_preset(os="macos") - fp3 = generate_context_fingerprint( - preset=preset3, - config_overrides={"fonts:spacing_seed": 0}, - ) - if "setFontSpacingSeed(0)" in fp3["init_script"]: - print(" init_script contains setFontSpacingSeed(0): PASS") - elif "setFontSpacingSeed" not in fp3["init_script"]: - print(" init_script omits setFontSpacingSeed entirely: PASS (acceptable)") - else: - import re - - match = re.search(r"setFontSpacingSeed\((\d+)\)", fp3["init_script"]) - val = match.group(1) if match else "?" - failures.append(f"init_script has setFontSpacingSeed({val}), expected 0") - print(f" init_script has setFontSpacingSeed({val}): FAIL") - - # --- Test 4: other seeds are NOT affected by a font-only override --- - print("\n=== Test 4: the audio seed is unaffected by a font override ===") - preset4 = get_random_preset(os="macos") - fp4 = generate_context_fingerprint( - preset=preset4, - config_overrides={"fonts:spacing_seed": 0}, - ) - audio = fp4["config"]["audio:seed"] - if audio != 0: - print(f" audio:seed={audio} (non-zero): PASS") - else: - failures.append(f"audio seed affected: {audio}") - print(f" audio={audio}: FAIL") - - # --- Summary --- - print("\n" + "=" * 50) - if failures: - print(f"FAILED ({len(failures)} issues):") - for f in failures: - print(f" - {f}") - return 1 - else: - print("ALL TESTS PASSED") - return 0 - - -if __name__ == "__main__": - sys.exit(asyncio.run(test())) diff --git a/tests/patches/fingerprint-setter-seal.py b/tests/patches/fingerprint-setter-seal.py index bed8d00..7f4ae4c 100644 --- a/tests/patches/fingerprint-setter-seal.py +++ b/tests/patches/fingerprint-setter-seal.py @@ -65,7 +65,6 @@ SETTERS = [ "setWebRTCIPv4", "setWebRTCIPv6", "setFontList", - "setFontSpacingSeed", "setAudioFingerprintSeed", "setSpeechVoices", "setTimezone",