From 12a1bb4fc6bc8fbdd88db2fd647bd1d554473f13 Mon Sep 17 00:00:00 2001 From: Jake Writer Date: Sun, 27 Sep 2026 13:21:01 -0600 Subject: [PATCH] fix(fingerprints): a NewContext Linux identity is Linux in platform and oscpu too fpgen's Linux pool now and then pairs a Linux user agent with navigator.platform Win32 and a Windows oscpu. launch_options() corrects that with fix_navigator_arch(); generate_context_fingerprint() never called it, so 24 of 1,500 Linux NewContext identities (1.6%) said Win32 under a Linux UA -- and because that path reads the OS for fonts and voices from the platform, they drew Windows fonts and voices as well. It now applies the same fix right after the fpgen draw, in pythonlib and in the TypeScript twin. The fix draws nothing, so the parity goldens are unchanged. The new tests feed the context path a real Linux draw with the Windows platform and oscpu, and fail without the fix ('Win32' != 'Linux x86_64') in both ports. After it: 0 of 1,500 sampled contexts mismatch. pythonlib 412 passed; typescript 585 passed. Co-Authored-By: Claude Opus 5.5 --- pythonlib/camoufox/fingerprints.py | 6 ++++++ pythonlib/tests/test_identity_salt.py | 22 ++++++++++++++++++++++ typescript/src/fingerprints.ts | 6 ++++++ typescript/tests/fingerprints.test.ts | 27 ++++++++++++++++++++++++++- 4 files changed, 60 insertions(+), 1 deletion(-) diff --git a/pythonlib/camoufox/fingerprints.py b/pythonlib/camoufox/fingerprints.py index cb18def..42c8ba1 100644 --- a/pythonlib/camoufox/fingerprints.py +++ b/pythonlib/camoufox/fingerprints.py @@ -1619,6 +1619,12 @@ def generate_context_fingerprint( # Fall back to synthetic generation fp = generate_fingerprint(os=os) config = from_fpgen(fp, ff_version) + # fpgen's Linux pool now and then pairs the Linux UA with a Windows + # platform and oscpu. launch_options() corrects that; this path did + # not, so ~1.6% of Linux contexts said Win32 -- and, since the OS below + # is read from the platform, drew Windows fonts and voices as well. + if 'Linux' in str(config.get('navigator.userAgent', '')): + fix_navigator_arch(config, 'lin') # A fresh identity: every seeded draw below gets its own salt. _salt = identity_salt() diff --git a/pythonlib/tests/test_identity_salt.py b/pythonlib/tests/test_identity_salt.py index 57fb5f2..7a386eb 100644 --- a/pythonlib/tests/test_identity_salt.py +++ b/pythonlib/tests/test_identity_salt.py @@ -199,3 +199,25 @@ def test_is_mobile_warns_that_camoufox_is_desktop_only(): with pytest.warns(LeakWarning, match="built for desktops"): launch(is_mobile=True, i_know_what_im_doing=False) + + +def test_a_context_with_a_linux_ua_is_linux_throughout(monkeypatch): + """fpgen's Linux pool now and then pairs the Linux UA with platform Win32 and + a Windows oscpu (~1.6% of NewContext Linux identities). launch_options() + fixed that and generate_context_fingerprint() did not -- and it reads the OS + for fonts and voices from the platform, so those came out Windows too.""" + real = fp.generate_fingerprint + + def mismatched(**kwargs): + drawn = real(**kwargs) + drawn["navigator"]["platform"] = "Win32" + drawn["navigator"]["oscpu"] = "Windows NT 10.0; Win64; x64" + return drawn + + monkeypatch.setattr(fp, "generate_fingerprint", mismatched) + config = fp.generate_context_fingerprint(os="linux")["config"] + + assert "Linux x86_64" in config["navigator.userAgent"] + assert config["navigator.platform"] == "Linux x86_64" + assert config["navigator.oscpu"] == "Linux x86_64" + assert "Segoe UI" not in config["fonts"] diff --git a/typescript/src/fingerprints.ts b/typescript/src/fingerprints.ts index a2fb4a0..897b505 100644 --- a/typescript/src/fingerprints.ts +++ b/typescript/src/fingerprints.ts @@ -1726,6 +1726,12 @@ export function generateContextFingerprint({ } else { const fp = generateFingerprint({ os: os ?? undefined }); config = fromFpgen(fp, ff_version); + // fpgen's Linux pool now and then pairs the Linux UA with a Windows + // platform and oscpu. launchOptions() corrects that; this path did not, + // so ~1.6% of Linux contexts said Win32 -- and, since the OS below is + // read from the platform, drew Windows fonts and voices as well. + if (pyStr(get(config, "navigator.userAgent", "")).includes("Linux")) + fixNavigatorArch(config, "lin"); // A fresh identity: every seeded draw below gets its own salt. const salt = identitySalt(); diff --git a/typescript/tests/fingerprints.test.ts b/typescript/tests/fingerprints.test.ts index 3970374..0763046 100644 --- a/typescript/tests/fingerprints.test.ts +++ b/typescript/tests/fingerprints.test.ts @@ -6,7 +6,7 @@ */ import * as fs from "node:fs"; import * as path from "node:path"; -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { InvalidIP } from "../src/exceptions.js"; import { appVersionFromUserAgent, @@ -35,6 +35,7 @@ import { setMediaDevicesDefaults, WINDOWS_11_MARKER_FONTS, } from "../src/fingerprints.js"; +import { Generator } from "../src/fpgen/index.js"; import { LOCAL_DATA } from "../src/pkgman.js"; import { MODEL } from "./fpgen-setup.js"; @@ -887,4 +888,28 @@ describe.skipIf(!MODEL.ok)("fpgen generation (needs the model)", () => { expect(fromP.context_options.timezoneId).toBe("Europe/Paris"); expect(fromP.config["navigator.platform"]).toBe("MacIntel"); }); + + it("a context with a Linux UA is Linux throughout", () => { + // fpgen's Linux pool now and then pairs the Linux UA with platform Win32 + // and a Windows oscpu. launchOptions() fixed that and this path did not, + // and it reads the OS for fonts and voices from the platform. + const real = Generator.prototype.generate; + const spy = vi + .spyOn(Generator.prototype, "generate") + .mockImplementation(function (this: Generator, ...args: any[]) { + const drawn = (real as any).apply(this, args); + drawn.navigator.platform = "Win32"; + drawn.navigator.oscpu = "Windows NT 10.0; Win64; x64"; + return drawn; + }); + try { + const { config } = generateContextFingerprint({ os: "linux" }); + expect(config["navigator.userAgent"]).toContain("Linux x86_64"); + expect(config["navigator.platform"]).toBe("Linux x86_64"); + expect(config["navigator.oscpu"]).toBe("Linux x86_64"); + expect(config.fonts).not.toContain("Segoe UI"); + } finally { + spy.mockRestore(); + } + }); });