mirror of
https://github.com/daijro/camoufox.git
synced 2026-10-05 08:00:21 +00:00
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:
co-authored by
Claude Opus 5.5
parent
86a20c73b7
commit
676fb3f7c1
@@ -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
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -7,7 +7,6 @@ export interface PhaseResult {
|
||||
}
|
||||
|
||||
const SELF_DESTRUCT_FUNCTIONS = [
|
||||
"setFontSpacingSeed",
|
||||
"setAudioFingerprintSeed",
|
||||
"setTimezone",
|
||||
"setScreenDimensions",
|
||||
|
||||
@@ -95,7 +95,6 @@ export interface ProfileConfig {
|
||||
webglVendor: string;
|
||||
webglRenderer: string;
|
||||
audioSeed: number;
|
||||
fontSpacingSeed: number;
|
||||
fontList: string[];
|
||||
speechVoices?: string[];
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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()))
|
||||
@@ -65,7 +65,6 @@ SETTERS = [
|
||||
"setWebRTCIPv4",
|
||||
"setWebRTCIPv6",
|
||||
"setFontList",
|
||||
"setFontSpacingSeed",
|
||||
"setAudioFingerprintSeed",
|
||||
"setSpeechVoices",
|
||||
"setTimezone",
|
||||
|
||||
Reference in New Issue
Block a user