Files
camoufox/docs/patch-upgrading-guide.md
T
Jake WriterandClaude Opus 5.5 676fb3f7c1 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>
2026-09-25 20:20:15 -06:00

475 lines
14 KiB
Markdown

# 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
1. [Understanding the Patch System](#understanding-the-patch-system)
2. [The Source Tree and Its Make Targets](#the-source-tree-and-its-make-targets)
3. [General Workflow](#general-workflow)
4. [Fixing Common Reject Types](#fixing-common-reject-types)
5. [Per-Context Machinery](#per-context-machinery)
6. [Testing and Validation](#testing-and-validation)
7. [Best Practices](#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) and
`playwright/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`](../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:
```bash
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.
```bash
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:
```bash
cd camoufox-<version>-<release>
find . -name '*.rej' -type f
```
### Step 3: Analyze Each Reject File
Read the reject file to understand what failed:
```bash
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:
1. **Search for unique context** around the reject
2. **Understand what the patch is doing**
3. **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:
```bash
find . -name '*.rej' -o -name '*.orig' | xargs rm -f
```
### Step 7: Write the Updated Patch
From the repository root:
```bash
(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**:
1. Read the actual file to find the includes section
2. Search for nearby includes (e.g., `xpcpublic.h`)
3. Add the new include in the appropriate location
4. Firefox include order: system headers, then Mozilla headers, alphabetically within groups
**Example**:
```cpp
// 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**:
1. Search for the function definition in Firefox source
2. Understand the new API structure
3. Port the patch logic to the new API
**Example - Firefox 146 Mouse Event Refactoring**:
Old Firefox 144 API (individual parameters):
```cpp
void SynthesizeMouseEvent(int x, int y, int button, ...)
```
New Firefox 146 API (structured data):
```cpp
void SynthesizeMouseEvent(SynthesizeMouseEventData& aData,
SynthesizeMouseEventOptions& aOptions)
```
Port the patch:
```cpp
// 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**:
1. Use grep to search for unique function names or variables in the reject
2. Find where Firefox moved the code
3. Apply the patch to the new location
```bash
# 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:
```cpp
MakeTextRun(text, len, drawTarget, appUnitsPerDevPixel, flags, recorder);
```
New Firefox expects:
```cpp
MakeTextRun(text, len, drawTarget, appUnitsPerDevPixel, flags, recorder, userContextId);
```
**How to Fix**:
1. Extract userContextId from available context (Document, PresContext, etc.)
2. Add proper extraction code before the call
3. Pass userContextId as the last parameter
**Standard userContextId Extraction Pattern**:
```cpp
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:
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: `setAudioFingerprintSeed()`, `setWebRTCIPv4()`
3. **nsGlobalWindowInner.cpp implementations**:
- Extract userContextId from window/document/docshell
- Call manager classes
- Self-destruct logic (remove function after first use)
4. **Core logic changes**:
- Consult the per-context manager before the global config (MaskConfig)
- Pass userContextId through call chains
See [`per-context-patches.md`](per-context-patches.md) for the full list.
---
## Testing and Validation
### Minimal Verification
After updating a patch, always verify:
1. **Every patch applies cleanly**: `make dir` lists no failures.
2. **Build compiles** (if feasible):
```bash
make build
```
### Full Testing
Run the suites that cover the patch (see [`ci/README.md`](../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:
1. ✅ **Start each patch from `make clean`** plus the patches it builds on
2. ✅ **Read and understand** what the patch is trying to do before fixing rejects
3. ✅ **Search for API changes** in Firefox release notes when functions have changed
4. ✅ **Use grep/search** extensively to find where code moved
5. ✅ **Extract userContextId properly** using the standard pattern
6. ✅ **Check with `make dir`** that the whole stack applies before considering a patch done
7. ✅ **Keep commits atomic** - one patch fix per session
8. ✅ **Document major API changes** you discover
### DON'T:
1. ❌ **Don't hand-edit `.patch` files** - edit the tree and regenerate with `make diff`
2. ❌ **Don't leave TODO comments** - fix things properly as you go
3. ❌ **Don't guess parameter values** - extract them properly or investigate
4. ❌ **Don't skip verification** - always test the patch applies cleanly
5. ❌ **Don't batch multiple patch updates** - do them one at a time
6. ❌ **Don't assume line numbers are correct** in reject files
7. ❌ **Don't ignore warnings** during patch application
### Common Pitfalls
1. **Assuming reject line numbers are accurate**: They're usually wrong in new Firefox versions
2. **Not understanding API changes**: Firefox refactors often - read the new code
3. **Forgetting new files**: `git diff` skips untracked files; `git add -N` them before `make diff`
4. **Diffing against the wrong base**: `make first-checkpoint` before applying the patch you are fixing, or `make diff` will include its dependencies
5. **Leaving reject files**: Remove all `.rej` and `.orig` files after fixing
---
## Appendix: Firefox Source Navigation
### Finding Files
```bash
# 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 implementation
- `dom/base/`: Core DOM classes (Window, Document, Navigator, etc.)
- `dom/webidl/`: WebIDL interface definitions
- `dom/media/webrtc/`: WebRTC implementation
- `gfx/`: Graphics and font rendering
- `gfx/thebes/`: Text rendering (fonts, glyphs, shaping)
- `layout/`: Layout engine
- `layout/generic/`: Text frames
- `layout/mathml/`: MathML rendering
### Common Firefox Patterns
**User Context ID Extraction**:
```cpp
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):
```cpp
// 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.sh` and run `make dir` to list the failing patches
- [ ] For each: `make clean`, apply its dependencies, `make first-checkpoint`, `make patch` it
- [ ] 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 `.rej` and `.orig` files after fixing
- [ ] `git add -N` any new files
- [ ] `make diff > patches/<name>.patch`
- [ ] `make dir` applies 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/