Skip to content

Font spacing perturbation has no off switch (unlike audio/canvas) #547

@rubenvereecken

Description

@rubenvereecken

Related: #76 — PixelScan reports 5 checks; this is the fix for the "Fingerprint: Masking detected" one specifically. The other four (Browser, Location, Proxy, Bot check) are unrelated to font spacing — see below.

Problem

AudioFingerprintManager::ApplyTransformation() has a clean off switch — when seed == 0, it returns early and produces vanilla audio output. Font spacing perturbation doesn't have this: when no seed is set, gfxHarfBuzzShaper.cpp falls back to a hardcoded constant (0x6D2B79F5u) and always applies perturbation.

This means every Camoufox session produces font metrics and canvas text hashes that no real device has ever generated. Fingerprint checkers that maintain a population database (PixelScan's "Masking detected" verdict) can trivially flag this as synthetic.

Perturbation seed == 0 behaviour Hardcoded fallback Can disable from Python?
Audio returns early, no-op none yes — set audio:seed to 0
Font spacing LCG still runs (0 → 12345) 0x6D2B79F5u when no seed set no
Canvas returns early, no-op none yes — set canvas:seed to 0

The audio and canvas paths let you opt out. Font spacing doesn't — inconsistent and makes A/B testing impossible from the Python API.

Evidence

PixelScan runs 5 checks. With the font spacing fallback removed, the Fingerprint check flips from fail to pass:

PixelScan check Before fix After fix Notes
Browser pass pass Firefox correctly identified
Location pass pass Geoip/timezone cross-check
Proxy fail (when proxied) fail (when proxied) IP reputation — unrelated
Fingerprint fail — "Masking detected" pass Font spacing was the trigger
Bot check pass pass Juggler protocol not flagged

Tested across 6 Camoufox runs + 1 vanilla Firefox baseline. A custom build with the hardcoded fallback removed passes the Fingerprint check cleanly.

Current C++ behaviour

// gfxHarfBuzzShaper.cpp — ShapeText()
uint32_t seed = FontSpacingSeedManager::GetSeed(pbid);
bool seedFromManager = (seed != 0);
if (!seedFromManager) {
    seed = 0x6D2B79F5u; // always-on fallback
}
// LCG + spacing applied unconditionally

Compare with audio:

// AudioFingerprintManager::ApplyTransformation()
if (seed == 0 || length == 0 || !data) {
    return;  // clean no-op
}

Suggested fix

Remove the hardcoded fallback and guard the perturbation block, same as audio:

uint32_t seed = FontSpacingSeedManager::GetSeed(pbid);

if (seed != 0) {
    // LCG + apply spacing (existing code, just wrapped in the guard)
}

When no seed is set, GetSeed() returns 0, the guard skips perturbation, and you get vanilla font metrics. To disable from the Python API, set fonts:spacing_seed to 0 in the config/preset — same convention as audio:seed and canvas:seed.

Happy to put up a PR if this looks reasonable.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions