mirror of
https://github.com/daijro/camoufox.git
synced 2026-10-03 16:00:19 +00:00
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)
384 lines
12 KiB
TypeScript
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);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
});
|