Files
camoufox/pythonlib/tests/test_fingerprint_fixes.py
T
Jake WriterandClaude Opus 5 fd501e5d01 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>
2026-09-17 13:01:51 -06:00

362 lines
14 KiB
Python

"""
Tests for the BrowserForge fingerprint-correction helpers in
camoufox.fingerprints, ported to parity with the camoufox-js launcher.
Run with:
cd pythonlib && python -m pytest tests/test_fingerprint_fixes.py -v
These guard the headless / impossible-geometry tells that BrowserForge
occasionally ships and that the camoufox-js wrapper already corrected but the
pythonlib did not.
"""
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from camoufox.fingerprints import ( # noqa: E402
clamp_screen_to_display,
clamp_window_dimensions,
clamp_window_position,
fix_navigator_arch,
fix_screen_no_taskbar,
set_media_devices_defaults,
)
class TestFixNavigatorArch:
def test_corrects_armv81_to_ua_arch(self):
c = {
"navigator.userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:135.0) ...",
"navigator.platform": "Linux armv81",
"navigator.oscpu": "Linux armv81",
}
fix_navigator_arch(c, "lin")
assert c["navigator.platform"] == "Linux x86_64"
assert c["navigator.oscpu"] == "Linux x86_64"
def test_noop_when_already_consistent(self):
c = {
"navigator.userAgent": "... Linux x86_64 ...",
"navigator.platform": "Linux x86_64",
"navigator.oscpu": "Linux x86_64",
}
fix_navigator_arch(c, "lin")
assert c["navigator.platform"] == "Linux x86_64"
def test_only_runs_on_linux(self):
c = {"navigator.userAgent": "... Macintosh ...", "navigator.platform": "MacIntel"}
fix_navigator_arch(c, "mac")
assert c["navigator.platform"] == "MacIntel"
def test_noop_without_ua(self):
c = {"navigator.platform": "Linux armv81"}
fix_navigator_arch(c, "lin")
assert c["navigator.platform"] == "Linux armv81"
class TestFixScreenNoTaskbar:
def test_subtracts_taskbar_when_avail_equals_screen(self):
c = {
"screen.width": 1920,
"screen.height": 1080,
"screen.availWidth": 1920,
"screen.availHeight": 1080,
"window.outerHeight": 1080,
"window.innerHeight": 1040,
}
fix_screen_no_taskbar(c, "lin")
assert c["screen.availHeight"] == 1080 - 27 # linux panel
assert c["window.outerHeight"] == 1053
# chrome delta (1080-1040=40) preserved
assert c["window.innerHeight"] == 1053 - 40
def test_per_os_taskbar_height(self):
for os_name, px in (("win", 40), ("mac", 25), ("lin", 27)):
c = {
"screen.width": 1920,
"screen.height": 1080,
"screen.availWidth": 1920,
"screen.availHeight": 1080,
}
fix_screen_no_taskbar(c, os_name)
assert c["screen.availHeight"] == 1080 - px
def test_noop_when_avail_already_less_than_screen(self):
c = {
"screen.width": 1920,
"screen.height": 1080,
"screen.availWidth": 1920,
"screen.availHeight": 1040,
}
fix_screen_no_taskbar(c, "lin")
assert c["screen.availHeight"] == 1040
class TestClampWindowDimensions:
def test_clamps_impossible_geometry_both_axes(self):
c = {
"screen.width": 1920,
"screen.height": 1080,
"screen.availWidth": 2000, # > screen
"window.outerWidth": 2200, # > avail
"window.innerWidth": 2100, # > outer
}
clamp_window_dimensions(c)
assert c["screen.availWidth"] == 1920
assert c["window.outerWidth"] == 1920
assert c["window.innerWidth"] <= c["window.outerWidth"]
def test_preserves_chrome_delta(self):
c = {
"screen.width": 1000,
"window.outerWidth": 1200, # 200 over screen
"window.innerWidth": 1180, # 20px chrome
}
clamp_window_dimensions(c)
assert c["window.outerWidth"] == 1000
assert c["window.innerWidth"] == 1000 - 20
def test_noop_when_hierarchy_already_valid(self):
c = {
"screen.width": 1920,
"screen.availWidth": 1920,
"window.outerWidth": 1280,
"window.innerWidth": 1264,
}
clamp_window_dimensions(c)
assert c["window.outerWidth"] == 1280
assert c["window.innerWidth"] == 1264
class TestClampScreenToDisplay:
def test_shrinks_screen_to_display(self):
# BrowserForge routinely ships a 2560x1440 fingerprint to a 1366x768
# laptop; browser-init then resizes the real window past the monitor.
c = {
"screen.width": 2560,
"screen.height": 1440,
"screen.availWidth": 2560,
"screen.availHeight": 1400,
}
clamp_screen_to_display(c, 1366, 768)
assert c["screen.width"] == 1366
assert c["screen.height"] == 768
# taskbar delta (1440-1400=40) preserved, so fix_screen_no_taskbar's
# avail < height invariant still holds
assert c["screen.availWidth"] == 1366
assert c["screen.availHeight"] == 768 - 40
def test_noop_when_already_within_display(self):
c = {"screen.width": 1280, "screen.height": 720, "screen.availHeight": 700}
clamp_screen_to_display(c, 1366, 768)
assert c["screen.width"] == 1280
assert c["screen.height"] == 720
assert c["screen.availHeight"] == 700
def test_ignores_unset_bounds(self):
c = {"screen.width": 2560, "screen.height": 1440}
clamp_screen_to_display(c, None, None)
assert c["screen.width"] == 2560
assert c["screen.height"] == 1440
def test_avail_never_drops_below_one(self):
# taskbar delta larger than the display must not produce a <= 0 avail
c = {"screen.height": 2000, "screen.availHeight": 100}
clamp_screen_to_display(c, None, 768)
assert c["screen.availHeight"] >= 1
def test_clamped_result_survives_cascade(self):
c = {
"screen.width": 2560,
"screen.height": 1440,
"screen.availWidth": 2560,
"screen.availHeight": 1400,
"window.outerWidth": 1920,
"window.outerHeight": 1055,
"window.innerWidth": 1920,
"window.innerHeight": 1000,
}
clamp_screen_to_display(c, 1366, 768)
clamp_window_dimensions(c)
assert c["window.outerWidth"] <= c["screen.availWidth"] <= c["screen.width"] == 1366
assert c["window.outerHeight"] <= c["screen.availHeight"] <= c["screen.height"] == 768
assert c["window.innerWidth"] <= c["window.outerWidth"]
assert c["window.innerHeight"] <= c["window.outerHeight"]
class TestClampWindowPosition:
def test_pulls_window_back_inside_screen(self):
c = {
"screen.width": 1366,
"screen.height": 768,
"window.outerWidth": 1366,
"window.outerHeight": 728,
"window.screenX": 250,
"window.screenY": 281,
}
clamp_window_position(c)
assert c["window.screenX"] == 0
assert c["window.screenY"] == 40
def test_noop_when_window_already_inside(self):
c = {
"screen.width": 1920,
"screen.height": 1080,
"window.outerWidth": 1280,
"window.outerHeight": 720,
"window.screenX": 100,
"window.screenY": 50,
}
clamp_window_position(c)
assert c["window.screenX"] == 100
assert c["window.screenY"] == 50
def test_never_negative(self):
c = {
"screen.width": 800,
"window.outerWidth": 1000, # wider than screen
"window.screenX": 50,
}
clamp_window_position(c)
assert c["window.screenX"] == 0
class TestSetMediaDevicesDefaults:
def test_draws_common_desktop_devices(self):
# A pre-permission page only sees "has a mic" / "has a camera"; the draw
# is seeded by the identity and follows the common desktop population.
c = {"navigator.userAgent": "ua", "navigator.platform": "Win32"}
set_media_devices_defaults(c)
assert c["mediaDevices:enabled"] is True
# counts and the post-grant label/group lists agree
for kind, key in (("micros", "microphone"), ("webcams", "webcam"), ("speakers", "speaker")):
n = c[f"mediaDevices:{kind}"]
assert n >= 0
assert len(c[f"mediaDevices:{key}Labels"]) == n
assert len(c[f"mediaDevices:{key}Groups"]) == n
# a Windows machine always has an output, in WASAPI label form
assert c["mediaDevices:speakers"] >= 1
assert all("(" in s for s in c["mediaDevices:speakerLabels"])
again = {"navigator.userAgent": "ua", "navigator.platform": "Win32"}
set_media_devices_defaults(again)
assert again == c
# over many identities laptops dominate Windows: most have both
mics = cams = 0
for i in range(400):
d = {"navigator.userAgent": f"ua{i}", "navigator.platform": "Win32"}
set_media_devices_defaults(d)
mics += d["mediaDevices:micros"] > 0
cams += d["mediaDevices:webcams"] > 0
assert 0.8 < mics / 400 < 1.0
assert 0.55 < cams / 400 < 0.95
def test_per_os_label_style(self):
from camoufox.fingerprints import draw_media_devices
mac = draw_media_devices("mac", 7)
assert all(not s.startswith("Microphone (") for s in mac["mediaDevices:microphoneLabels"])
lin = draw_media_devices("lin", 7)
# PulseAudio lists a monitor source per output as a capture device
outs = lin["mediaDevices:speakerLabels"]
assert all(f"Monitor of {o}" in lin["mediaDevices:microphoneLabels"] for o in outs)
# a sound card's mic and speakers share a group, like a real groupId
win = draw_media_devices("win", 11)
if win["mediaDevices:micros"] and win["mediaDevices:speakers"]:
assert win["mediaDevices:microphoneGroups"][0] == win["mediaDevices:speakerGroups"][0]
# never the fake engine's names
for os_key in ("win", "mac", "lin"):
for seed in range(50):
d = draw_media_devices(os_key, seed)
mics = d["mediaDevices:microphoneLabels"]
cams = d["mediaDevices:webcamLabels"]
assert "Default Audio Device" not in mics
assert "Default Video Device" not in cams
# distinct within a kind (macOS names a webcam and its mic alike)
assert len(set(mics)) == len(mics)
assert len(set(cams)) == len(cams)
def test_respects_user_set_media_devices(self):
c = {"mediaDevices:webcams": 5}
set_media_devices_defaults(c)
assert c == {"mediaDevices:webcams": 5}
class TestFixHardwareConcurrency:
def test_keeps_a_plausible_draw_the_host_can_be_pinned_to(self, monkeypatch):
# The fingerprint's value survives when it is a count a real desktop
# ships with AND the browser can be pinned to it (reported == measurable
# by construction).
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 in (4, 6, 8, 10, 12, 14, 16):
c = {"navigator.hardwareConcurrency": drawn}
fp.fix_hardware_concurrency(c)
assert c["navigator.hardwareConcurrency"] == drawn
def test_snaps_implausible_draws_down_into_the_table(self, monkeypatch):
# 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, 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": 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 / 2. Used when the
# draw exceeds the host or the host cannot pin (macOS).
# 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)
for host, expected in (
(16, 16),
(10, 10),
(24, 24),
(26, 24),
(32, 32),
(64, 32),
(22, 22),
(7, 6),
(5, 4),
(3, 2),
(2, 2),
(1, 2),
(9, 8),
):
monkeypatch.setattr(fp, "host_cpu_count", lambda host=host: host)
c = {"navigator.hardwareConcurrency": 2}
fp.fix_hardware_concurrency(c)
assert c["navigator.hardwareConcurrency"] == expected, host
# a draw above the host count cannot be honoured even where pinning works
monkeypatch.setattr(cpu_affinity, "supported", lambda: True)
monkeypatch.setattr(fp, "host_cpu_count", lambda: 8)
c = {"navigator.hardwareConcurrency": 32}
fp.fix_hardware_concurrency(c)
assert c["navigator.hardwareConcurrency"] == 8
def test_no_host_count_leaves_the_draw(self, monkeypatch):
from camoufox import fingerprints as fp
monkeypatch.setattr(fp, "host_cpu_count", lambda: None)
c = {"navigator.hardwareConcurrency": 8}
fp.fix_hardware_concurrency(c)
assert c["navigator.hardwareConcurrency"] == 8