Files
coffeegrind123 c2817c1cae fix(coherence): let macOS draw Intel Mac GPUs, checked as machines
coherence rejected "Intel(R) HD Graphics 400" and "Radeon R9 200
Series" on macOS as a Braswell Atom IGP and a desktop PC card. They are
Firefox's sanitized buckets, not devices: SanitizeRenderer
(FIREFOX_152_0_4_RELEASE) maps an Intel Mac's UHD 630 to the first and
a Radeon Pro 5300M to the second, per Firefox's own TestCiMac and
TestMacAmd. fpgen's pinned model records both from Firefox on macOS at
1.14% each. The rule kept every generated Mac off them, removed 24
real macOS presets in #779 (restored here, 9 + 15), and dropped either
GPU from a caller's Mac preset. ANGLE and llvmpipe stay rejected:
Firefox 152 has no ANGLE-on-Metal path (Bug 2046027 came later).

Intel Macs are now checked as machines instead (intel-mac-hardware),
for every non-Apple GPU on macOS:

- a core count some Intel Mac with that GPU reports. Firefox reports
  physical cores where kern.tcsm_available is set and logical ones
  otherwise, so either counts: 2-8 physical / 4-16 logical for the
  IGP, up to the 2019 Mac Pro for a discrete GPU;
- a screen that is not a notched MacBook's or the 24" iMac's. 45.5% of
  fpgen's Firefox macOS screens are one, and the draw already put an
  Intel or AMD bucket behind 4.9% of them.

The WebGL draw applies the same check, given the core count. The preset
GPU drop moves next to the WebGL draw, after the host core count and
the display clamp: before, a preset's GPU was judged against cores the
launch then replaced. The TypeScript launcher gets the same changes,
and the goldens cover the new check and the narrowed draws.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit d6e2adcb6c)
2026-09-27 20:09:03 +00:00

384 lines
12 KiB
TypeScript

/**
* Port of pythonlib/tests/test_coherence.py (the rule-level half; the
* launch-level half lives with the launcher tests) and test_shipped_data.py.
*
* Every identity Camoufox can produce has to be a machine that could exist:
* the pools are sampled independently, so an incoherent identity is assembled
* rather than inherited, and cleaning the pools cannot prevent it.
*/
import * as fs from "node:fs";
import * as path from "node:path";
import { describe, expect, it } from "vitest";
import * as coherence from "../src/coherence.js";
import { fromPreset } from "../src/fingerprints.js";
import { LOCAL_DATA } from "../src/pkgman.js";
import { firefoxGpus } from "../src/webgl.js";
import { MODEL } from "./fpgen-setup.js";
const rules = (config: Record<string, any>, os: string) =>
coherence.validate(config, os).map((v) => v.rule);
describe("rules", () => {
it("Apple Silicon never has fewer than eight cores", () => {
const config = {
"webGl:renderer": "Apple M1, or similar",
"navigator.hardwareConcurrency": 2,
};
expect(rules(config, "mac")).toEqual(["apple-silicon-cores"]);
expect(coherence.apply(config, "mac")).toEqual([]);
expect(config["navigator.hardwareConcurrency"]).toBe(8);
});
it("an Intel Mac reports its Firefox bucket", () => {
// SanitizeRenderer's buckets for a UHD 630 and a Radeon Pro 5300M
// (TestSanitizeRenderer.cpp TestCiMac, TestMacAmd; Firefox 152).
for (const bucket of [
"Intel(R) HD Graphics 400, or similar",
"Radeon R9 200 Series, or similar",
]) {
expect(coherence.gpuFitsOs(bucket, "mac"), bucket).toBe(true);
}
expect(coherence.gpuFitsOs("llvmpipe, or similar", "mac")).toBe(false);
});
it("an Intel IGP Mac reports its physical or logical cores", () => {
const igp = "Intel(R) HD Graphics 400, or similar";
const at = (cores: number) =>
rules(
{ "webGl:renderer": igp, "navigator.hardwareConcurrency": cores },
"mac",
);
for (const cores of [2, 4, 6, 8, 12, 16]) {
expect(at(cores), String(cores)).toEqual([]);
}
for (const cores of [10, 14, 20, 24, 32]) {
expect(at(cores), String(cores)).toEqual(["intel-mac-hardware"]);
}
});
it("a discrete GPU Mac reaches the Mac Pro", () => {
const amd = "Radeon R9 200 Series, or similar";
const at = (cores: number) =>
rules(
{ "webGl:renderer": amd, "navigator.hardwareConcurrency": cores },
"mac",
);
for (const cores of [
2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 24, 28, 32, 48, 56,
]) {
expect(at(cores), String(cores)).toEqual([]);
}
expect(at(22)).toEqual(["intel-mac-hardware"]);
});
it.each([
"Intel(R) HD Graphics 400, or similar",
"Intel(R) HD Graphics, or similar",
"Radeon R9 200 Series, or similar",
"Radeon HD 3200 Graphics, or similar",
])("an Intel Mac (%s) has no notched panel", (renderer) => {
for (const [w, h] of [
[1470, 956],
[1512, 982],
[1728, 1117],
[2056, 1329],
[1710, 1107],
]) {
expect(
rules(
{ "webGl:renderer": renderer, "screen.width": w, "screen.height": h },
"mac",
),
`${w}x${h}`,
).toEqual(["intel-mac-hardware"]);
}
// The same panels are what Apple Silicon MacBooks report.
expect(
rules(
{
"webGl:renderer": "Apple M1, or similar",
"screen.width": 1470,
"screen.height": 956,
},
"mac",
),
).toEqual([]);
});
it("a Mac cannot report an ANGLE renderer", () => {
expect(
rules(
{
"webGl:renderer":
"ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0), or similar",
},
"mac",
),
).toEqual(["gpu-matches-os"]);
});
it("Windows renders through ANGLE", () => {
expect(
coherence.gpuFitsOs(
"ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11), or similar",
"win",
),
).toBe(true);
expect(coherence.gpuFitsOs("Apple M1, or similar", "win")).toBe(false);
});
it("Apple Silicon reports deep colour", () => {
const config: Record<string, any> = {
"webGl:renderer": "Apple M1, or similar",
"screen.colorDepth": 24,
};
expect(rules(config, "mac")).toEqual(["color-depth"]);
expect(coherence.apply(config, "mac")).toEqual([]);
expect(config["screen.colorDepth"]).toBe(30);
expect(config["screen.pixelDepth"]).toBe(30);
});
it("colour depth is 24 or 30", () => {
const config = { "screen.colorDepth": 32 };
expect(rules(config, "lin")).toEqual(["color-depth"]);
expect(coherence.apply(config, "lin")).toEqual([]);
expect(config["screen.colorDepth"]).toBe(24);
});
it("touch points are a digitiser count", () => {
const config = { "navigator.maxTouchPoints": 256 };
expect(rules(config, "win")).toEqual(["touch-points"]);
expect(coherence.apply(config, "win")).toEqual([]);
expect(config["navigator.maxTouchPoints"]).toBe(0);
for (const real of [0, 1, 2, 5, 10]) {
expect(
coherence.validate({ "navigator.maxTouchPoints": real }, "win"),
).toEqual([]);
}
});
it("a Mac has no touchscreen", () => {
expect(rules({ "navigator.maxTouchPoints": 5 }, "mac")).toEqual([
"touch-points",
]);
});
it("devicePixelRatio is a real display mode", () => {
const config = { "window.devicePixelRatio": 1.8181818181818181 };
expect(rules(config, "win")).toEqual(["device-pixel-ratio"]);
expect(coherence.apply(config, "win")).toEqual([]);
expect(config["window.devicePixelRatio"]).toBe(1.75);
expect(coherence.validate({ "window.devicePixelRatio": 1 }, "lin")).toEqual(
[],
);
expect(
coherence.validate({ "window.devicePixelRatio": 2.5 }, "win"),
).toEqual([]);
expect(coherence.validate({ "window.devicePixelRatio": 2 }, "mac")).toEqual(
[],
);
expect(rules({ "window.devicePixelRatio": 1.5 }, "mac")).toEqual([
"device-pixel-ratio",
]);
});
it("desktop screens are landscape", () => {
const config = { "screen.width": 1440, "screen.height": 2560 };
expect(rules(config, "win")).toEqual(["screen-shape"]);
expect(coherence.repairScreenOrientation(config)).toBe(true);
expect([config["screen.width"], config["screen.height"]]).toEqual([
2560, 1440,
]);
});
it("a phone viewport is not a desktop screen", () => {
expect(rules({ "screen.width": 736, "screen.height": 414 }, "mac")).toEqual(
["screen-shape"],
);
});
it("avail never exceeds the screen", () => {
const config = {
"screen.width": 1920,
"screen.height": 1080,
"screen.availWidth": 2000,
};
expect(rules(config, "lin")).toEqual(["avail-bounds"]);
expect(coherence.apply(config, "lin")).toEqual([]);
expect(config["screen.availWidth"]).toBe(1920);
});
it("the platform agrees with the user agent arch", () => {
expect(
rules(
{
"navigator.userAgent":
"Mozilla/5.0 (X11; Linux x86_64; rv:152.0) Gecko/20100101 Firefox/152.0",
"navigator.platform": "Linux armv81",
},
"lin",
),
).toEqual(["arch-agreement"]);
});
it("the viewport leaves room for the browser chrome", () => {
const config = {
"window.outerHeight": 801,
"window.innerHeight": 717,
"screen.availHeight": 1040,
};
expect(rules(config, "lin")).toEqual(["window-chrome"]);
expect(coherence.apply(config, "lin")).toEqual([]);
expect(config["window.outerHeight"]).toBe(
717 + coherence.BROWSER_CHROME_HEIGHT,
);
});
it("the real machines pass", () => {
const real: Array<[string, Record<string, any>]> = [
[
"lin",
{
"navigator.userAgent":
"Mozilla/5.0 (X11; Linux x86_64; rv:152.0) Gecko/20100101 Firefox/152.0",
"navigator.platform": "Linux x86_64",
"navigator.hardwareConcurrency": 16,
"navigator.maxTouchPoints": 0,
"screen.width": 1920,
"screen.height": 1080,
"screen.colorDepth": 24,
"webGl:renderer": "Radeon HD 3200 Graphics, or similar",
},
],
[
"win",
{
"navigator.platform": "Win32",
"navigator.hardwareConcurrency": 16,
"navigator.maxTouchPoints": 5,
"screen.width": 1382,
"screen.height": 864,
"screen.colorDepth": 24,
"webGl:renderer":
"ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar",
},
],
[
"mac",
{
"navigator.platform": "MacIntel",
"navigator.hardwareConcurrency": 10,
"navigator.maxTouchPoints": 0,
"screen.width": 2560,
"screen.height": 1440,
"screen.colorDepth": 30,
"webGl:renderer": "Apple M1, or similar",
},
],
// fpgen model-2/2026's Radeon R9 200 macOS record: a 16" MacBook
// Pro (6 physical cores) at its default scaled resolution.
[
"mac",
{
"navigator.platform": "MacIntel",
"navigator.hardwareConcurrency": 6,
"navigator.maxTouchPoints": 0,
"screen.width": 1792,
"screen.height": 1120,
"screen.colorDepth": 30,
"webGl:renderer": "Radeon R9 200 Series, or similar",
},
],
];
for (const [os, config] of real) {
expect(coherence.validate(config, os), os).toEqual([]);
}
});
it("drops a source GPU the OS cannot report, and only that", () => {
const config: Record<string, any> = {
"webGl:vendor": "Google Inc. (Intel)",
"webGl:renderer":
"ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar",
"screen.width": 1920,
};
const dropped = coherence.dropIncoherentSourceValues(config, "mac");
expect(dropped.map((v) => v.rule)).toEqual(["gpu-matches-os"]);
expect(config).toEqual({ "screen.width": 1920 });
});
it("drops a source GPU the final core count rules out", () => {
const config: Record<string, any> = {
"webGl:vendor": "Intel Inc.",
"webGl:renderer": "Intel(R) HD Graphics 400, or similar",
"navigator.hardwareConcurrency": 20,
};
const dropped = coherence.dropIncoherentSourceValues(config, "mac");
expect(dropped.map((v) => v.rule)).toEqual(["intel-mac-hardware"]);
expect(config).toEqual({ "navigator.hardwareConcurrency": 20 });
});
});
describe("shipped data (test_shipped_data.py)", () => {
const OS_KEY: Record<string, string> = {
macos: "mac",
windows: "win",
linux: "lin",
};
for (const file of [
"fingerprint-presets.json",
"fingerprint-presets-v150.json",
]) {
it(`every preset in ${file} is coherent as stored`, () => {
const presets = JSON.parse(
fs.readFileSync(path.join(LOCAL_DATA, file), "utf-8"),
).presets as Record<string, any[]>;
for (const [os, entries] of Object.entries(presets)) {
expect(
entries.length,
`${file}/${os} has no presets left`,
).toBeGreaterThan(0);
entries.forEach((preset, i) => {
const config = fromPreset(preset, "152", 0);
const violations = coherence.validate(config, OS_KEY[os]);
expect(
violations,
`${file} ${os}[${i}]: ${violations.map((v) => v.detail).join("; ")}`,
).toEqual([]);
});
}
});
}
for (const file of [
"fingerprint-presets.json",
"fingerprint-presets-v150.json",
]) {
// A preset records only its GPU's name; the WebGL parameters behind it
// come from fpgen, and a GPU fpgen never saw from Firefox on that OS has
// none, so launching it would borrow another device's.
it.skipIf(!MODEL.ok)(`every preset GPU in ${file} has WebGL data`, () => {
const presets = JSON.parse(
fs.readFileSync(path.join(LOCAL_DATA, file), "utf-8"),
).presets as Record<string, any[]>;
for (const [os, entries] of Object.entries(presets)) {
const known = new Set(
firefoxGpus(
os === "windows" ? "win" : os === "macos" ? "mac" : "lin",
).map((gpu) => JSON.stringify(gpu)),
);
entries.forEach((preset, i) => {
const gpu = [
preset.webgl.unmaskedVendor,
preset.webgl.unmaskedRenderer,
];
expect(
known.has(JSON.stringify(gpu)),
`${file} ${os}[${i}]: ${gpu[1]} has no WebGL data`,
).toBe(true);
});
}
});
}
});