fix(pythonlib): stop overriding the corpus on core counts; pinning is opt-in

Two findings from auditing the sweep's fixes against one bar: a difference is a
leak only if a page's JavaScript can actually read it.

hardwareConcurrency 2 was excluded from PLAUSIBLE_CORE_COUNTS because "2 is what
Firefox reports under resistFingerprinting". That has not been true for years:
RuntimeService::ClampedHardwareConcurrency hardcodes 4, and 8 on macOS, both of
which are already in the table. The exclusion protected against nothing and cost
every genuinely dual-core machine -- 20% of the macOS presets in the recorded
corpus, 4.2% of Linux draws. It also made the small-host tail worse: a 3-core
host reported 4, which cannot be pinned, so a page measured 3 while being told 4.
At 2 the pin succeeds.

pin_cpu_cores now defaults to False. What it buys is defence against a page
timing N parallel workers; what it costs is a browser-wide CPU cap, a per-driver
launch lock, and nothing at all on macOS. Unpinned, the host's own snapped count
is reported, so reported and measurable still agree -- the identity just loses
one drawn value. Callers who want the draw kept can still ask for it.

The WebGL sampler keeps rejecting software rasterisers, and its docstring now
says so: it described the opposite of what the code does. llvmpipe as the
presented GPU is a live check on a string every fingerprint script reads, which
is worth ~1.5% of corpus fidelity.

251 pythonlib tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jake Writer
2026-09-17 13:01:51 -06:00
co-authored by Claude Opus 5
parent 85d26d4f76
commit fd501e5d01
6 changed files with 58 additions and 39 deletions
+2 -1
View File
@@ -105,7 +105,8 @@ async def AsyncNewBrowser(
virtual_display = None
if not from_options:
kwargs.setdefault('pin_cpu_cores', True)
# Opt-in; see the note in sync_api.launch_options_or_default.
kwargs.setdefault('pin_cpu_cores', False)
from_options = await asyncio.get_event_loop().run_in_executor(
None,
partial(launch_options, headless=headless, debug=debug, **kwargs),
+26 -21
View File
@@ -847,16 +847,22 @@ def host_cpu_count() -> Optional[int]:
# Linux 1/65; 32: Linux 1/65 -- all in -v150). Leaving any of them out snapped
# genuine machines with that count down to the next entry for no reason.
#
# 2 is recorded too -- and is common, 6/30 macOS presets (20%) -- but is
# deliberately EXCLUDED (user, 2026-09-15): 2 is what Firefox reports under
# resistFingerprinting, and the goal is to look like a DEFAULT Firefox, which
# RFP is not. So a draw of 2 snaps up to the table floor of 4.
# 2 was excluded on 2026-09-15 on the grounds that it is what Firefox reports
# under resistFingerprinting. That is no longer true and has not been for years:
# RuntimeService::ClampedHardwareConcurrency (dom/workers/RuntimeService.cpp,
# checked in the 152 tree on 2026-09-17) hardcodes 4 under RFP, and 8 on macOS.
# Both of those are IN this table, so excluding 2 never protected against an
# "is this RFP" check -- it only cost fidelity, on 6/30 macOS presets (20%) and
# 4.2% of Linux draws, i.e. every genuinely dual-core machine. Restored.
#
# (Sundial's cjResistance helper still tested hardwareConcurrency === 2 for the
# same stale reason; corrected the same day.)
#
# A host outside this table would hand its own oddity to the fingerprint: a
# 64-thread build box reports 32, anything under 4 threads reports 4. Odd
# counts (5, 7, 9, 11, 13, 15) never appear in the corpus -- they are
# browserforge Bayesian synthesis -- so they keep getting snapped down.
PLAUSIBLE_CORE_COUNTS = (4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 28, 32)
PLAUSIBLE_CORE_COUNTS = (2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 28, 32)
def fix_hardware_concurrency(config: Dict[str, Any], can_pin: Optional[bool] = None) -> None:
@@ -889,8 +895,9 @@ def fix_hardware_concurrency(config: Dict[str, Any], can_pin: Optional[bool] = N
# The fingerprint's own value survives when the browser can be pinned to
# that many cores (cpu_affinity: Linux, Windows): reported and measurable
# then agree by construction, and the identity keeps its diversity. A draw
# the host cannot honour (more cores than it has), or a host that cannot
# pin (macOS), falls back to the snapped host count.
# the host cannot honour (more cores than it has), a host that cannot pin
# (macOS), or pin_cpu_cores left off -- the default since 2026-09-17 --
# falls back to the snapped host count, which is equally coherent.
from .cpu_affinity import supported as _can_pin
# can_pin=False: the caller launches the browser itself and nothing will
@@ -906,17 +913,13 @@ def fix_hardware_concurrency(config: Dict[str, Any], can_pin: Optional[bool] = N
if pinnable and isinstance(drawn, int) and drawn >= 1:
# The fingerprint's value is kept for diversity, but it still has to be
# a count a real desktop ships with. Accepting any 1..host let
# browserforge's low/odd draws through: over 400 linux draws, 8.0% were
# < 4 cores and 4.2% were exactly 2 -- and hardwareConcurrency == 2 is
# the value Firefox reports under resistFingerprinting, so CreepJS-style
# heuristics label the browser "Firefox resistFingerprinting" (this is
# what intermittently failed sundial's "Privacy mode verdict"). Odd
# counts (5, 7, 9, 11, 13, 15) survived the same way. Snap the draw DOWN
# into the table instead, capped by the host so pinning can honour it.
# browserforge's synthetic odd draws through (5, 7, 9, 11, 13, 15 --
# counts the corpus never records). Snap the draw DOWN into the table
# instead, capped by the host so pinning can honour it.
target = min(drawn, cap)
allowed = [c for c in PLAUSIBLE_CORE_COUNTS if c <= target]
# The floor is the table's even on a 1-3 core host: min(4, cap)
# reported 1, 2 or 3 there, and 2 is the resistFingerprinting value.
# The floor is the table's even on a 1-core host: a draw of 1 reports 2,
# the lowest count the corpus actually records.
config['navigator.hardwareConcurrency'] = (
allowed[-1] if allowed else PLAUSIBLE_CORE_COUNTS[0]
)
@@ -1367,11 +1370,13 @@ def sample_webgl_for_screen(
and widening it here to flatter the GPU would push a headful window back
off the monitor it is drawn on (#499).
The first draw settles hardware-vs-software at the pool's natural rate and
is never resampled once it lands on a rasterizer. Rejecting only hardware
draws would renormalise the survivors onto llvmpipe / WARP / SwiftShader:
on a small screen that turns a 1.5% software rate into a 40% one, trading
a weak incoherence for the strongest VM/headless tell there is.
Software rasterisers are the one exception to keeping the pool's rate: a
draw that lands on llvmpipe / WARP / SwiftShader is resampled, so they
never present as the GPU (see below). That does cost fidelity -- the corpus
records them at ~1.5%, because real users do run without working drivers --
but "no consumer machine reports llvmpipe" is a live, standard check on a
string every fingerprint script already reads, so the trade is worth it.
Reviewed against the JS-detectability bar on 2026-09-17 and kept.
Falls back to that first draw when the pool holds nothing coherent, so an
unusual screen degrades to today's behaviour rather than raising.
+7 -1
View File
@@ -107,7 +107,13 @@ def NewBrowser(
virtual_display = None
if not from_options:
kwargs.setdefault('pin_cpu_cores', True)
# Opt-in (2026-09-17). Pinning keeps the identity's core count by
# constraining the browser to that many cores; it costs real CPU, needs
# a launch lock, and does nothing on macOS. What it defends against is a
# page timing N parallel workers, which is expensive and noisy on a busy
# machine. Off, the host's own snapped count is reported, so reported
# and measurable still agree -- the identity just loses that one draw.
kwargs.setdefault('pin_cpu_cores', False)
from_options = launch_options(headless=headless, debug=debug, **kwargs)
# Playwright's default viewport deadlocks Juggler when the window is spoofed
+6 -4
View File
@@ -812,10 +812,12 @@ def launch_options(
virtual_display (Optional[str]):
Virtual display number. Ex: ':99'. This is handled by Camoufox & AsyncCamoufox.
pin_cpu_cores (Optional[bool]):
The browser will be pinned to navigator.hardwareConcurrency cores
(Linux/Windows), so the fingerprint's core count can be kept. Set by
Camoufox & AsyncCamoufox, which apply the pin; without it the host's
own (snapped) core count is reported, since nothing pins the browser.
Pin the browser to navigator.hardwareConcurrency cores
(Linux/Windows) so the fingerprint's own core count can be kept:
a page timing N parallel workers then measures the number it was
told. OFF by default -- it costs real CPU and serializes concurrent
launches. Without it the host's own (snapped) count is reported,
which is equally coherent, just less diverse.
webgl_config (Optional[Tuple[str, str]]):
Use a specific WebGL vendor/renderer pair. Passed as a tuple of (vendor, renderer).
**launch_options (Dict[str, Any]):
+11 -11
View File
@@ -298,33 +298,31 @@ class TestFixHardwareConcurrency:
assert c["navigator.hardwareConcurrency"] == drawn
def test_snaps_implausible_draws_down_into_the_table(self, monkeypatch):
# browserforge draws counts no desktop ships with: over 400 linux draws
# 8.0% were < 4 cores and 4.2% were exactly 2. hardwareConcurrency == 2
# is what Firefox reports under resistFingerprinting, so CreepJS-style
# heuristics label the browser "Firefox resistFingerprinting"; odd counts
# (5, 7, 9, ...) are equally synthetic. They snap DOWN into
# browserforge draws counts no desktop ships with -- odd ones (5, 7, 9,
# ...) are Bayesian synthesis, not machines. They snap DOWN into
# PLAUSIBLE_CORE_COUNTS, with the table floor for anything below it.
# 2 IS a real count (20% of the macOS presets) and stays: Firefox's
# resistFingerprinting value is 4, or 8 on macOS, not 2.
from camoufox import cpu_affinity, fingerprints as fp
monkeypatch.setattr(cpu_affinity, "supported", lambda: True)
monkeypatch.setattr(fp, "host_cpu_count", lambda: 16)
for drawn, expected in ((1, 4), (2, 4), (3, 4), (5, 4), (7, 6), (9, 8),
for drawn, expected in ((1, 2), (2, 2), (3, 2), (5, 4), (7, 6), (9, 8),
(11, 10), (13, 12), (15, 14), (32, 16)):
c = {"navigator.hardwareConcurrency": drawn}
fp.fix_hardware_concurrency(c)
assert c["navigator.hardwareConcurrency"] == expected, drawn
# never above what the host can be pinned to
monkeypatch.setattr(fp, "host_cpu_count", lambda: 4)
c = {"navigator.hardwareConcurrency": 2}
c = {"navigator.hardwareConcurrency": 8}
fp.fix_hardware_concurrency(c)
assert c["navigator.hardwareConcurrency"] == 4
def test_snaps_host_parallelism_when_it_cannot_pin(self, monkeypatch):
# The host count, snapped DOWN into the
# counts real machines ship with; the tails report 32 / 4. Used when the
# counts real machines ship with; the tails report 32 / 2. Used when the
# draw exceeds the host or the host cannot pin (macOS).
# 18/22/24/28/32 are in the table (recorded on real devices); 2 is NOT,
# although it is recorded, because 2 is the resistFingerprinting value.
# 2/18/22/24/28/32 are all in the table, all recorded on real devices.
from camoufox import cpu_affinity, fingerprints as fp
monkeypatch.setattr(cpu_affinity, "supported", lambda: False)
@@ -338,7 +336,9 @@ class TestFixHardwareConcurrency:
(22, 22),
(7, 6),
(5, 4),
(2, 4),
(3, 2),
(2, 2),
(1, 2),
(9, 8),
):
monkeypatch.setattr(fp, "host_cpu_count", lambda host=host: host)
+6 -1
View File
@@ -100,13 +100,18 @@ class TestVoicesFollowLocale:
class TestCoreCountFloor:
def test_small_pinnable_host_reports_table_floor(self, monkeypatch):
# A 1-3 core host reports 2, the table's floor and the lowest count the
# corpus records. It used to report 4, which no 2-core machine can back
# up: 4 cannot be pinned on a 3-core host, so the page measured 3 while
# being told 4. At 2 the pin succeeds on a 2- or 3-core host, and the
# 1-core tail (told 2, measures 1) is closer than 4 was.
monkeypatch.setattr(cpu_affinity, "supported", lambda: True)
for host_cores in (1, 2, 3):
monkeypatch.setattr(fp, "host_cpu_count", lambda n=host_cores: n)
for drawn_cores in (1, 2, 3, 8):
c = {"navigator.hardwareConcurrency": drawn_cores}
fp.fix_hardware_concurrency(c)
assert c["navigator.hardwareConcurrency"] == 4, (host_cores, drawn_cores)
assert c["navigator.hardwareConcurrency"] == 2, (host_cores, drawn_cores)
def test_unpinned_launch_reports_host(self, monkeypatch):
monkeypatch.setattr(cpu_affinity, "supported", lambda: True)