fix: remove the glyph-spacing seed from the browser and the launcher

anti-font-fingerprinting.patch 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 the
noise was itself a fingerprint, measured in #779 at +1 px per ~100 glyphs
plus fractional deltas on every measureText. #779 defaulted the seed to 0
and kept it as an opt-in, but an opt-in whose only effect is to become
detectable is not worth carrying. Removed:

- The browser side:
  - FontSpacingSeedManager and window.setFontSpacingSeed;
  - the HarfBuzz hook;
  - the plumbing that existed only to carry the context id down to the
    shaper: the userContextId on gfxTextRun, gfxShapedWord and the word-cache
    key, and the extra MakeTextRun argument in nsTextFrame, nsFontMetrics,
    MathML and canvas.
  The font group keeps its userContextId, which font-list-spoofing.patch
  uses to apply the per-context font list. Text is now shaped exactly as
  stock Firefox shapes it.
- The fonts:spacing_seed key. The launcher had been sending 0 on every
  launch, plus a setFontSpacingSeed(0) call in every context's init script.
- tests/patches/config-overrides.py, which tested only the spacing override.
  A pythonlib test now covers config_overrides with another key.

timezone-spoofing, webrtc-ip-spoofing and window-setter-seal change only in
context lines and the setter seal list. Every patch applies cleanly to a
fresh tree, and the result builds. The settled decision is recorded as
no-glyph-spacing-noise, with an automated check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jake Writer
2026-09-25 20:20:15 -06:00
co-authored by Claude Opus 5.5
parent 86a20c73b7
commit 676fb3f7c1
20 changed files with 113 additions and 1249 deletions
+1 -1
View File
@@ -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
-1
View File
@@ -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),
-1
View File
@@ -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),
-1
View File
@@ -7,7 +7,6 @@ export interface PhaseResult {
}
const SELF_DESTRUCT_FUNCTIONS = [
"setFontSpacingSeed",
"setAudioFingerprintSeed",
"setTimezone",
"setScreenDimensions",
-1
View File
@@ -95,7 +95,6 @@ export interface ProfileConfig {
webglVendor: string;
webglRenderer: string;
audioSeed: number;
fontSpacingSeed: number;
fontList: string[];
speechVoices?: string[];
}
+15
View File
@@ -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
+3 -3
View File
@@ -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
+9 -22
View File
@@ -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. |
+15
View File
@@ -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 = {
File diff suppressed because it is too large Load Diff
+1 -2
View File
@@ -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
+16 -15
View File
@@ -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);
};
+10 -10
View File
@@ -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);
};
+11 -24
View File
@@ -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 {
+1 -8
View File
@@ -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'),
+2 -8
View File
@@ -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
+16
View File
@@ -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"]
-1
View File
@@ -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" },
-151
View File
@@ -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()))
-1
View File
@@ -65,7 +65,6 @@ SETTERS = [
"setWebRTCIPv4",
"setWebRTCIPv6",
"setFontList",
"setFontSpacingSeed",
"setAudioFingerprintSeed",
"setSpeechVoices",
"setTimezone",