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>
14 KiB
Firefox Patch Upgrading Guide
How to update Camoufox's patches when upstream.sh moves to a new Firefox
version. Patches break because Firefox renames APIs, moves code and shifts line
numbers; this guide covers finding and fixing those rejects.
Table of Contents
- Understanding the Patch System
- The Source Tree and Its Make Targets
- General Workflow
- Fixing Common Reject Types
- Per-Context Machinery
- Testing and Validation
- Best Practices
Understanding the Patch System
Patch Categories
All patches live under patches/, and scripts/patch.py applies every
*.patch in it (subdirectories included), sorted by file name:
- Playwright:
playwright/0-playwright.patch(Juggler integration) andplaywright/1-leak-fixes.patch. Their names sort first, and every other patch is written against a tree that already has them. - Feature patches:
webrtc-ip-spoofing.patch,anti-font-fingerprinting.patch, etc. Per-user-context (per-Playwright-context) support is built into each one. librewolf/,ghostery/: patches taken from those projects.
Compile-time dependencies between patches (MaskConfig, RoverfoxStorageManager)
are listed in patches/patch-dependencies.md.
Key Infrastructure Files
- RoverfoxStorageManager.cpp/h: Thread-safe key-value storage for per-context data
- Manager Classes: AudioFingerprintManager, WebRTCIPManager, etc.
- Window.webidl: Exposes the per-context setters to Playwright
The Source Tree and Its Make Targets
The Firefox tree is camoufox-<version>-<release>/ (from upstream.sh). It is
a git repository whose unpatched tag is plain Firefox plus additions/ and
settings/. Run every target from the repository root:
| Target | What it does |
|---|---|
make dir |
Fetches and extracts Firefox if the tree is missing. Otherwise resets it to unpatched, runs mach clobber and git clean -fdx (the object directory goes too), re-copies additions, then applies every patch and lists the ones that left rejects. |
make revert |
git reset --hard unpatched. Untracked files stay, including new files that patches created. |
make clean |
mach clobber, git clean -fdx, then make revert: unpatched Firefox with nothing left over, without re-fetching. |
make patch ./patches/x.patch |
Applies one patch (patch -p1). |
make unpatch ./patches/x.patch |
Reverses one patch. |
make first-checkpoint |
Commits the current tree and tags it first-checkpoint. |
make workspace ./patches/x.patch |
Unapplies x if it is applied, runs first-checkpoint, then applies x again, so the working tree differs from the checkpoint by exactly that patch. |
make diff |
git diff first-checkpoint. Redirect it into the patch file. |
git diff does not show untracked files. Before make diff, mark new files
with git add -N <file> inside the source tree, or they will be missing from
the patch.
General Workflow
Step 1: Bump the Version and Find the Broken Patches
Update version and release in upstream.sh, then:
make dir
patch.py applies every patch and ends with a list of the ones that failed and
their reject files. It deletes the .rej files after listing them, so reproduce
each failure one patch at a time (Step 2).
Step 2: Set Up One Patch
Start from a clean unpatched tree, apply what the patch builds on (at least the
Playwright patches, plus anything from patches/patch-dependencies.md),
checkpoint, then apply the broken patch. Use make clean rather than
make revert here: files that other patches created survive a revert and make
patch stop on "previously applied" prompts.
make clean
make patch ./patches/playwright/0-playwright.patch
make patch ./patches/playwright/1-leak-fixes.patch
make first-checkpoint
make patch ./patches/patch-name.patch # fails, leaving .rej files
Find the reject files:
cd camoufox-<version>-<release>
find . -name '*.rej' -type f
Step 3: Analyze Each Reject File
Read the reject file to understand what failed:
cat path/to/file.cpp.rej
Reject files show:
@@lines: Line numbers where patch expected to apply-lines: What the patch expected to find (old code)+lines: What the patch wanted to add (new code)
Step 4: Locate the Correct Position in Firefox Code
The line numbers in rejects are usually wrong for the new Firefox version. You need to:
- Search for unique context around the reject
- Understand what the patch is doing
- Find equivalent location in new Firefox code
Step 5: Apply Changes Manually
Edit the file to make the rejected change at the correct location.
Step 6: Remove Reject Files
After fixing all rejects, delete the .rej files and any .orig backups
patch left, so they do not end up in the diff:
find . -name '*.rej' -o -name '*.orig' | xargs rm -f
Step 7: Write the Updated Patch
From the repository root:
(cd camoufox-<version>-<release> && git add -N path/to/new/file.cpp) # new files only
make diff > patches/patch-name.patch
Step 8: Verify
Run make dir again. The patch should no longer be listed as failing.
Fixing Common Reject Types
Type 1: Include Directive Rejects
Symptom: Reject shows failed #include additions
Example Reject:
@@ -325,6 +325,7 @@
#include "xpcpublic.h"
+#include "WebRTCIPManager.h"
#include "nsDocShell.h"
How to Fix:
- Read the actual file to find the includes section
- Search for nearby includes (e.g.,
xpcpublic.h) - Add the new include in the appropriate location
- Firefox include order: system headers, then Mozilla headers, alphabetically within groups
Example:
// Find this in the actual file:
#include "xpcpublic.h"
// Add the missing includes after it:
#include "xpcpublic.h"
#include "WebRTCIPManager.h"
#include "nsDocShell.h"
#include "mozilla/OriginAttributes.h"
Type 2: Function Signature Changes
Symptom: Reject shows function call with changed parameters
Example Reject:
- mouseOrPointerEvent.mButton = aButton;
+ mouseOrPointerEvent.mJugglerEventId = aMouseEventData.mJugglerEventId;
Common Causes:
- Firefox refactored the API
- Parameters moved from individual args to struct/data object
- Parameter order changed
How to Fix:
- Search for the function definition in Firefox source
- Understand the new API structure
- Port the patch logic to the new API
Example - Firefox 146 Mouse Event Refactoring:
Old Firefox 144 API (individual parameters):
void SynthesizeMouseEvent(int x, int y, int button, ...)
New Firefox 146 API (structured data):
void SynthesizeMouseEvent(SynthesizeMouseEventData& aData,
SynthesizeMouseEventOptions& aOptions)
Port the patch:
// Old patch code:
mouseEvent.mButton = aButton;
mouseEvent.jugglerEventId = aJugglerEventId;
// New patch code for Firefox 146:
mouseOrPointerEvent.mButton = aMouseEventData.mButton;
mouseOrPointerEvent.mJugglerEventId = aMouseEventData.mJugglerEventId;
mouseOrPointerEvent.convertToPointer = aOptions.mConvertToPointer;
Type 3: Missing Context - Code Moved
Symptom: Reject shows context that doesn't exist in the file
How to Fix:
- Use grep to search for unique function names or variables in the reject
- Find where Firefox moved the code
- Apply the patch to the new location
# Search across the codebase
grep -r "FunctionName" camoufox-<version>/ --include="*.cpp"
Type 4: New Parameter Added to Function Calls
Symptom: Reject shows function call, but Firefox added/removed parameters
Example - MakeTextRun userContextId:
Old call:
MakeTextRun(text, len, drawTarget, appUnitsPerDevPixel, flags, recorder);
New Firefox expects:
MakeTextRun(text, len, drawTarget, appUnitsPerDevPixel, flags, recorder, userContextId);
How to Fix:
- Extract userContextId from available context (Document, PresContext, etc.)
- Add proper extraction code before the call
- Pass userContextId as the last parameter
Standard userContextId Extraction Pattern:
uint32_t userContextId = 0;
if (mozilla::dom::Document* doc = presContext->Document()) {
if (nsIPrincipal* principal = doc->NodePrincipal()) {
auto* bp = mozilla::BasePrincipal::Cast(principal);
if (bp) {
userContextId = bp->OriginAttributesRef().mUserContextId;
}
}
}
// Now pass userContextId to the function
MakeTextRun(..., userContextId);
Type 5: Line Number Shifts (No Code Changes)
Symptom: Reject shows patch tried to apply at wrong line number, but code is identical
How to Fix:
Simply apply the patch manually at the correct line number. The code hasn't changed, just the location.
Per-Context Machinery
Most spoofing patches carry per-context support. When porting one, expect these pieces:
-
Manager classes (e.g., AudioFingerprintManager, WebRTCIPManager):
- Store per-context settings using RoverfoxStorageManager
- Provide WebIDL-compatible enable/disable checks
- Handle self-destructing functions
-
Window.webidl functions:
- JavaScript APIs exposed to Playwright
- Examples:
setAudioFingerprintSeed(),setWebRTCIPv4()
-
nsGlobalWindowInner.cpp implementations:
- Extract userContextId from window/document/docshell
- Call manager classes
- Self-destruct logic (remove function after first use)
-
Core logic changes:
- Consult the per-context manager before the global config (MaskConfig)
- Pass userContextId through call chains
See per-context-patches.md for the full list.
Testing and Validation
Minimal Verification
After updating a patch, always verify:
-
Every patch applies cleanly:
make dirlists no failures. -
Build compiles (if feasible):
make build
Full Testing
Run the suites that cover the patch (see ci/README.md):
python3 -m ci.run_patch_guards --binary <camoufox-bin> is the most direct
evidence that a patch which still applies was not neutered by the upgrade.
Best Practices
DO:
- ✅ Start each patch from
make cleanplus the patches it builds on - ✅ Read and understand what the patch is trying to do before fixing rejects
- ✅ Search for API changes in Firefox release notes when functions have changed
- ✅ Use grep/search extensively to find where code moved
- ✅ Extract userContextId properly using the standard pattern
- ✅ Check with
make dirthat the whole stack applies before considering a patch done - ✅ Keep commits atomic - one patch fix per session
- ✅ Document major API changes you discover
DON'T:
- ❌ Don't hand-edit
.patchfiles - edit the tree and regenerate withmake diff - ❌ Don't leave TODO comments - fix things properly as you go
- ❌ Don't guess parameter values - extract them properly or investigate
- ❌ Don't skip verification - always test the patch applies cleanly
- ❌ Don't batch multiple patch updates - do them one at a time
- ❌ Don't assume line numbers are correct in reject files
- ❌ Don't ignore warnings during patch application
Common Pitfalls
- Assuming reject line numbers are accurate: They're usually wrong in new Firefox versions
- Not understanding API changes: Firefox refactors often - read the new code
- Forgetting new files:
git diffskips untracked files;git add -Nthem beforemake diff - Diffing against the wrong base:
make first-checkpointbefore applying the patch you are fixing, ormake diffwill include its dependencies - Leaving reject files: Remove all
.rejand.origfiles after fixing
Appendix: Firefox Source Navigation
Finding Files
# Find files by name
find . -name "Navigator.cpp" -type f
# Find files containing a symbol
grep -r "GetAcceptLanguages" . --include="*.cpp"
# Find class definitions
grep -r "class Navigator" . --include="*.h"
Understanding Firefox Code Structure
dom/: DOM implementationdom/base/: Core DOM classes (Window, Document, Navigator, etc.)dom/webidl/: WebIDL interface definitionsdom/media/webrtc/: WebRTC implementation
gfx/: Graphics and font renderinggfx/thebes/: Text rendering (fonts, glyphs, shaping)
layout/: Layout enginelayout/generic/: Text frameslayout/mathml/: MathML rendering
Common Firefox Patterns
User Context ID Extraction:
uint32_t userContextId = 0;
if (Document* doc = GetDocument()) {
if (nsIPrincipal* principal = doc->NodePrincipal()) {
auto* bp = mozilla::BasePrincipal::Cast(principal);
if (bp) {
userContextId = bp->OriginAttributesRef().mUserContextId;
}
}
}
Three-tier userContextId fallback (for WebIDL functions):
// 1) Document's principal (preferred)
if (Document* doc = win->GetDoc()) {
if (nsIPrincipal* p = doc->NodePrincipal()) {
userContextId = p->OriginAttributesRef().mUserContextId;
}
}
// 2) DocShell origin attributes
if (userContextId == 0) {
if (nsIDocShell* ds = win->GetDocShell()) {
auto* concrete = static_cast<nsDocShell*>(ds);
userContextId = concrete->GetOriginAttributes().mUserContextId;
}
}
// 3) Top browsing context
if (userContextId == 0) {
if (BrowsingContext* bc = win->GetBrowsingContext()) {
RefPtr<BrowsingContext> top = bc->Top();
// ... extract from top window
}
}
Summary Checklist
When updating patches for a new Firefox version:
- Bump
upstream.shand runmake dirto list the failing patches - For each:
make clean, apply its dependencies,make first-checkpoint,make patchit - Analyze each reject to understand what changed
- Search Firefox source for moved/refactored code
- Fix rejects by porting logic to new Firefox APIs
- Extract userContextId properly using standard patterns
- Don't leave TODO comments - fix everything immediately
- Remove all
.rejand.origfiles after fixing git add -Nany new filesmake diff > patches/<name>.patchmake dirapplies the whole stack cleanly- Document any major API changes discovered
Additional Resources
- Firefox source: https://searchfox.org/
- Firefox API documentation: https://firefox-source-docs.mozilla.org/
- Mercurial repository: https://hg.mozilla.org/mozilla-central/