diff --git a/build-tester/scripts/generate-presets.py b/build-tester/scripts/generate-presets.py index 9f15558..9c14947 100644 --- a/build-tester/scripts/generate-presets.py +++ b/build-tester/scripts/generate-presets.py @@ -31,7 +31,6 @@ def convert_preset(ctx): 'profileConfig': { 'fontSpacingSeed': config.get('fonts:spacing_seed', 0), 'audioSeed': config.get('audio:seed', 0), - 'canvasSeed': config.get('canvas:seed', 0), 'screenWidth': screen.get('width', 1920), 'screenHeight': screen.get('height', 1080), 'screenColorDepth': screen.get('colorDepth', 24), diff --git a/build-tester/scripts/presets.py b/build-tester/scripts/presets.py index 34b4749..4a39d10 100644 --- a/build-tester/scripts/presets.py +++ b/build-tester/scripts/presets.py @@ -32,7 +32,6 @@ def convert_preset(ctx: dict) -> dict: "profileConfig": { "fontSpacingSeed": config.get("fonts:spacing_seed", 0), "audioSeed": config.get("audio:seed", 0), - "canvasSeed": config.get("canvas:seed", 0), "screenWidth": screen.get("width", 1920), "screenHeight": screen.get("height", 1080), "screenColorDepth": screen.get("colorDepth", 24), diff --git a/build-tester/src/lib/types.ts b/build-tester/src/lib/types.ts index 6c0c21d..99346d3 100644 --- a/build-tester/src/lib/types.ts +++ b/build-tester/src/lib/types.ts @@ -95,7 +95,6 @@ export interface ProfileConfig { webglVendor: string; webglRenderer: string; audioSeed: number; - canvasSeed: number; fontSpacingSeed: number; fontList: string[]; speechVoices?: string[]; diff --git a/ci/run_build_tester.py b/ci/run_build_tester.py index 9e01846..894f3f8 100644 --- a/ci/run_build_tester.py +++ b/ci/run_build_tester.py @@ -33,30 +33,12 @@ CONFIG_PATH = CI_DIR / "build-tester.yml" # this whole suite exists to catch, so a single collision here is fatal. _MUST_VARY = ("uniqueAudio", "uniqueTimezones") -# Canvas belongs in _MUST_VARY and is not there yet, because it does not -# currently hold. Measured across 24 profiles in 3 runs against -# v152.0.4-beta.30, once the canvas fingerprint was actually being hashed -# (it had been a 100-character prefix of the data URL, which compared equal -# for almost anything): -# -# audio 24 distinct / 24 samples every one unique -# canvas 16 distinct / 24 samples values recurring across runs -# macOS 7/12, Linux 9/12 one value seen three times -# -# Same hash, same harness, same runs -- so this is not the measurement. Two -# contexts share a canvas fingerprint roughly a third of the time, which matches -# the rate at which CI flagged it. Gating on it would fail about one run in -# three for a real reason nobody has fixed yet, so it is reported every run and -# tracked here rather than quietly dropped or quietly tolerated. -# -# See ci/tribal-rules.yml: canvas-noise-entropy-is-lower-than-audio. -_TRACKED_LOW_ENTROPY = ("uniqueCanvas",) - -# Values drawn from the preset pool. Three draws from a pool of a dozen collide -# regularly -- that is the birthday paradox, not a leak, and the pools are -# deliberately small because they hold real devices. Counted and reported, -# never fatal on their own. -_MAY_COLLIDE = ("uniqueFonts", "uniqueScreens", "uniqueVoices", "uniqueWebGL") +# Values that follow the device rather than the context. fonts, screens, voices +# and WebGL are drawn from a pool of real devices, and three draws from a pool +# of a dozen collide regularly -- the birthday paradox, not a leak. The canvas +# is rendered, not noised (ci/tribal-rules.yml: canvas-is-not-noised), so it +# follows the fonts and GPU the same way. Counted and reported, never fatal. +_MAY_COLLIDE = ("uniqueCanvas", "uniqueFonts", "uniqueScreens", "uniqueVoices", "uniqueWebGL") # Properties of the operating system. Every macOS context reports MacIntel and # every Linux one reports Linux x86_64, because that is what those systems @@ -169,7 +151,7 @@ def uniqueness(full: dict) -> Dict[str, List[str]]: Only `leaks` and `not_constant` should fail a build. """ out: Dict[str, List[str]] = { - "leaks": [], "noise": [], "low_entropy": [], "absent": [], "not_constant": [] + "leaks": [], "noise": [], "absent": [], "not_constant": [] } for group, stats in (full.get("crossProfile") or {}).items(): @@ -180,7 +162,7 @@ def uniqueness(full: dict) -> Dict[str, List[str]]: def describe(key: str) -> str: return f"{group}.{key} ({stats.get(key)}/{total} distinct)" - for key in _MUST_VARY + _MAY_COLLIDE + _TRACKED_LOW_ENTROPY: + for key in _MUST_VARY + _MAY_COLLIDE: value = stats.get(key) if not isinstance(value, int): continue @@ -190,13 +172,7 @@ def uniqueness(full: dict) -> Dict[str, List[str]]: # collision reports a leak where there is no data at all. out["absent"].append(describe(key)) elif value < total: - if key in _MUST_VARY: - bucket = "leaks" - elif key in _TRACKED_LOW_ENTROPY: - bucket = "low_entropy" - else: - bucket = "noise" - out[bucket].append(describe(key)) + out["leaks" if key in _MUST_VARY else "noise"].append(describe(key)) for key in _MUST_MATCH: value = stats.get(key) @@ -278,19 +254,9 @@ def main(argv: Optional[List[str]] = None) -> int: allowed = int(cfg.get("allow_uniqueness_collisions", 0)) result.metrics["uniqueness"] = slots - if slots["low_entropy"]: - result.note( - "KNOWN, UNFIXED -- per-context values that collide more often than they should: " - + ", ".join(slots["low_entropy"]) - + ". Measured 16 distinct canvas fingerprints in 24 samples where audio gave " - "24/24, so two contexts are linkable by canvas roughly a third of the time. " - "Reported every run, not gated, because it is real and unfixed. See " - "ci/tribal-rules.yml: canvas-noise-entropy-is-lower-than-audio." - ) - if slots["noise"]: result.note( - f"{len(slots['noise'])} preset-pool collision(s), not gated: " + f"{len(slots['noise'])} device-level collision(s), not gated: " + ", ".join(slots["noise"]) ) if slots["absent"]: diff --git a/ci/tests/test_ci.py b/ci/tests/test_ci.py index ac15562..17c154e 100644 --- a/ci/tests/test_ci.py +++ b/ci/tests/test_ci.py @@ -602,7 +602,7 @@ def _cross(**kw): def test_a_shared_per_context_value_is_a_leak(): - """audio, canvas and timezone are derived per context. + """audio and timezone are derived per context. Two contexts sharing one is the failure this whole suite exists to catch. """ @@ -614,19 +614,14 @@ def test_a_shared_per_context_value_is_a_leak(): assert not out["noise"] -def test_canvas_collisions_are_tracked_but_do_not_gate(): - """Canvas belongs in must-vary and does not hold there yet. - - Measured 16 distinct canvas fingerprints in 24 samples where audio gave - 24/24 -- so two contexts collide about a third of the time. Gating would - fail one run in three for a real, unfixed reason; silence would lose the - finding. It gets its own bucket and is reported every run. - """ +def test_a_shared_canvas_is_noise_not_a_leak(): + """The canvas is rendered, not noised (#528), so contexts with the same + fonts and GPU draw the same image, as two real machines would.""" from ci.run_build_tester import uniqueness out = uniqueness(_cross(uniqueCanvas=2)) - assert out["low_entropy"] == ["macPerContext.uniqueCanvas (2/3 distinct)"] - assert not out["leaks"] and not out["noise"] + assert out["noise"] == ["macPerContext.uniqueCanvas (2/3 distinct)"] + assert not out["leaks"] def test_a_shared_preset_value_is_noise_not_a_leak(): diff --git a/ci/tribal-rules.yml b/ci/tribal-rules.yml index 864c9a5..48288ee 100644 --- a/ci/tribal-rules.yml +++ b/ci/tribal-rules.yml @@ -159,28 +159,22 @@ rules: # Measurement # ------------------------------------------------------------------------- - - id: canvas-noise-entropy-is-lower-than-audio - title: Two contexts share a canvas fingerprint far more often than they should - check: manual # an open finding, not a settled decision + - id: canvas-is-not-noised + title: The canvas is rendered, not noised, and there is no canvas seed + check: automated evidence: - - "24 profiles across 3 runs on v152.0.4-beta.30, once the canvas was actually hashed" - - "audio 24 distinct / 24 samples; canvas 16 / 24, values recurring across independent runs" - - "macOS 7 distinct of 12, Linux 9 of 12; one value seen three times" - - "matches the rate at which CI flagged cross-profile canvas collisions" + - "PR #528 (e4528a2): 'Disable Canvas Noise' -- removed the canvas noise patch" + - "issue #721: canvas:seed declared and passed through, but nothing consumed it" + - "24 profiles across 3 runs on v152.0.4-beta.30: canvas 16 distinct of 24, audio 24 of 24" rationale: >- - Audio and canvas are both noised per context, by the same machinery, and - were measured in the same runs with the same hash. Audio came back - completely unique; canvas did not, and its values repeat across runs that - share nothing. So two contexts in one browser are linkable by canvas - roughly a third of the time, which is the property per-context spoofing - exists to prevent. - This was invisible until the fingerprint stopped being a 100-character - prefix of a data URL, which compared equal for almost anything. It is - recorded as an open question rather than a decision: it may be that canvas - output is deliberately tied to the device preset rather than to - canvas:seed, in which case the seed is not doing what its name suggests. - Not gated, because failing one run in three helps nobody, and not dropped, - because it is real. + No stock Firefox perturbs its canvas output, so noise is itself a tell: + a page that renders the same image twice and gets two answers has found + a spoofing browser. A context's canvas therefore follows what really + determines it on a real machine, the GPU and the fonts, and two contexts + that share those share a canvas. That is why build-tester counts canvas + collisions with the device-level values and does not gate on them. The + launchers generated a canvas:seed for three releases after the patch + that read it was gone; it is not generated or declared any more. - id: canvas-fingerprint-is-hashed-not-truncated title: The canvas check hashes pixels, not a prefix of the data URL @@ -197,8 +191,7 @@ rules: contexts whose canvas differed everywhere else read as identical. Worse, the stability check compared the same prefix between two collections, so it passed on PNG headers and could not have noticed non-deterministic - noise. getImageData is the surface Camoufox noises and the one a - fingerprinter reads; hash that. + output. getImageData is the surface a fingerprinter reads; hash that. # ------------------------------------------------------------------------- # Teardown diff --git a/native-tests/_churn.py b/native-tests/_churn.py index 3599f43..feeb0e0 100644 --- a/native-tests/_churn.py +++ b/native-tests/_churn.py @@ -6,7 +6,7 @@ drive one mechanism thousands of times and watch whether the content process's memory scales with the count. The theory this is built to test is that Camoufox adds per-something state that -stock Firefox does not have -- an isolated world, a canvas noise seed, a font +stock Firefox does not have -- an isolated world, an audio noise seed, a font list -- and that something churning fast enough (an ad stack creating and destroying iframes, compiling scripts, drawing to canvases) accumulates it. diff --git a/native-tests/test_tribal_rules.py b/native-tests/test_tribal_rules.py index 2066616..0297d51 100644 --- a/native-tests/test_tribal_rules.py +++ b/native-tests/test_tribal_rules.py @@ -18,6 +18,7 @@ from __future__ import annotations import importlib import inspect +import json import sys from pathlib import Path from typing import Any, Dict, List @@ -541,6 +542,19 @@ def test_a_sandbox_held_over_a_page_window_is_nuked_not_just_dropped(): ) +def test_no_canvas_seed_is_declared_or_sent(): + """Nothing in the browser reads a canvas seed, so neither launcher sends one.""" + declared = { + entry["property"] + for entry in json.loads((REPO_ROOT / "settings" / "properties.json").read_text()) + } + assert not {k for k in declared if k.startswith("canvas:")}, explain("canvas-is-not-noised") + fingerprints = REPO_ROOT / "pythonlib" / "camoufox" / "fingerprints.py" + assert "setCanvasSeed" not in fingerprints.read_text(encoding="utf-8"), ( + "fingerprints.py still calls setCanvasSeed" + explain("canvas-is-not-noised") + ) + + def test_the_canvas_check_hashes_pixels_rather_than_a_data_url_prefix(): """A truncated data URL is mostly PNG header, not image. diff --git a/pythonlib/camoufox/async_api.py b/pythonlib/camoufox/async_api.py index 5054ff1..ee4357d 100644 --- a/pythonlib/camoufox/async_api.py +++ b/pythonlib/camoufox/async_api.py @@ -196,7 +196,7 @@ async def AsyncNewContext( Creates a new browser context with a unique fingerprint identity. Each context gets its own real fingerprint preset (navigator, screen, WebGL, fonts, etc.) - with unique seeds for audio, canvas, and font spacing noise. All values are applied + with its own audio noise seed. All values are applied via addInitScript so they self-destruct before page scripts can detect them. Parameters: diff --git a/pythonlib/camoufox/fingerprints.py b/pythonlib/camoufox/fingerprints.py index dc09ef9..b13830e 100644 --- a/pythonlib/camoufox/fingerprints.py +++ b/pythonlib/camoufox/fingerprints.py @@ -1644,12 +1644,11 @@ def from_preset(preset: Dict, ff_version: Optional[str] = None, salt: Optional[i if webgl.get('unmaskedRenderer'): config['webGl:renderer'] = webgl['unmaskedRenderer'] - # Generate unique random seeds per launch (1 to 2^32-1, excluding 0 which is a no-op in C++) + # Generate a unique audio seed per launch (1 to 2^32-1, excluding 0 which is a no-op in C++) # fonts:spacing_seed stays 0 (off): glyph-advance perturbation produces text # widths no real machine emits (see launch_options in utils.py). config['fonts:spacing_seed'] = 0 config['audio:seed'] = randint(1, 4_294_967_295) # nosec - config['canvas:seed'] = randint(1, 4_294_967_295) # nosec if preset.get('timezone'): config['timezone'] = preset['timezone'] @@ -1700,7 +1699,6 @@ def _build_init_script(values: Dict[str, Any]) -> str: setters = [ ('fontSpacingSeed', 'setFontSpacingSeed', '{val}'), ('audioFingerprintSeed', 'setAudioFingerprintSeed', '{val}'), - ('canvasSeed', 'setCanvasSeed', '{val}'), ('navigatorPlatform', 'setNavigatorPlatform', '{val}'), ('navigatorOscpu', 'setNavigatorOscpu', '{val}'), ('navigatorUserAgent', 'setNavigatorUserAgent', '{val}'), @@ -1820,7 +1818,6 @@ def generate_context_fingerprint( # Add seeds (the generator doesn't produce these) config.setdefault('fonts:spacing_seed', 0) # perturbation off; see utils.launch_options config.setdefault('audio:seed', randint(1, 4_294_967_295)) # nosec - config.setdefault('canvas:seed', randint(1, 4_294_967_295)) # nosec # Determine target OS from platform for font/voice generation plat = config.get('navigator.platform', '') @@ -1917,7 +1914,6 @@ def generate_context_fingerprint( init_values: Dict[str, Any] = { 'fontSpacingSeed': config.get('fonts:spacing_seed'), 'audioFingerprintSeed': config.get('audio:seed'), - 'canvasSeed': config.get('canvas:seed'), 'navigatorPlatform': nav.get('platform'), 'navigatorOscpu': config.get('navigator.oscpu'), 'navigatorUserAgent': config.get('navigator.userAgent'), diff --git a/pythonlib/camoufox/fpgen.yml b/pythonlib/camoufox/fpgen.yml index 0d46c11..c3f5d33 100644 --- a/pythonlib/camoufox/fpgen.yml +++ b/pythonlib/camoufox/fpgen.yml @@ -18,13 +18,10 @@ navigator: # fpgen's UAs trail the current release; the version is rewritten to the # Camoufox Firefox version in _cast_to_properties. userAgent: navigator.userAgent - appCodeName: navigator.appCodeName - appName: navigator.appName appVersion: navigator.appVersion oscpu: navigator.oscpu platform: navigator.platform hardwareConcurrency: navigator.hardwareConcurrency - product: navigator.product # Never override productSub (#105) # deviceMemory is not in Firefox, and fpgen reports the string "undefined" # Locale is handled separately (locale:*) diff --git a/pythonlib/camoufox/sync_api.py b/pythonlib/camoufox/sync_api.py index cb0fc6d..4c7a897 100644 --- a/pythonlib/camoufox/sync_api.py +++ b/pythonlib/camoufox/sync_api.py @@ -174,7 +174,7 @@ def NewContext( Creates a new browser context with a unique fingerprint identity. Each context gets its own real fingerprint preset - with unique seeds for audio, canvas, and font spacing noise. All values are applied + with its own audio noise seed. All values are applied via addInitScript so they self-destruct before page scripts can detect them. Parameters: diff --git a/pythonlib/camoufox/utils.py b/pythonlib/camoufox/utils.py index 5b125ef..5338d77 100644 --- a/pythonlib/camoufox/utils.py +++ b/pythonlib/camoufox/utils.py @@ -994,7 +994,7 @@ def launch_options( _user_set_dnt = 'navigator.doNotTrack' in config _user_set_gpc = 'navigator.globalPrivacyControl' in config _user_set_accept_encoding = 'headers.Accept-Encoding' in config - _user_set_noise_seeds = {k for k in ('audio:seed', 'canvas:seed') if k in config} + _user_set_audio_seed = 'audio:seed' in config # The salt that makes every seeded draw belong to this identity (see # fingerprints.identity_salt): stable when the caller pinned the identity @@ -1274,16 +1274,14 @@ def launch_options( # on every measureText), which is a fingerprint no stock Firefox emits. # Pass fonts:spacing_seed explicitly to opt back in. set_into(config, 'fonts:spacing_seed', 0) - # audio/canvas noise seeds follow the identity: a returning "same device" - # must reproduce its audio and canvas hashes (#442/#765). Derived, not - # equal, so the two streams differ; never 0 (0 disables the noise). - # A preset draws its own random seeds; they are replaced here too so a - # pinned preset reproduces them, but a seed the caller set is kept. - _ident = identity_seed(config, _identity_salt) - if 'audio:seed' not in _user_set_noise_seeds: + # The audio noise seed follows the identity: a returning "same device" must + # reproduce its audio hash (#442/#765). Never 0 (0 disables the noise). A + # preset draws its own random seed; it is replaced here too so a pinned + # preset reproduces it, but a seed the caller set is kept. There is no + # canvas seed: the browser adds no canvas noise (#528). + if not _user_set_audio_seed: + _ident = identity_seed(config, _identity_salt) config['audio:seed'] = ((_ident * 2654435761 + 97) & 0xFFFFFFFF) or 1 - if 'canvas:seed' not in _user_set_noise_seeds: - config['canvas:seed'] = ((_ident * 40503 + 12345) & 0xFFFFFFFF) or 1 # Set geolocation if geoip: diff --git a/pythonlib/tests/test_config_schema.py b/pythonlib/tests/test_config_schema.py index bd00fc9..7ef3694 100644 --- a/pythonlib/tests/test_config_schema.py +++ b/pythonlib/tests/test_config_schema.py @@ -38,6 +38,18 @@ MASKCONFIG_READ = re.compile(r'MaskConfig::(?:Get|Has)\w*\(\s*"([^"]+)"') # Add here (with a reason) only when the read genuinely cannot name its key. ALLOWED_UNDECLARED: set = set() +# Declared keys the browser never reads, each with the reason it is declared +# anyway. Everything else in properties.json must be read by a patch or by +# Juggler: a key nothing reads does nothing, silently. +NOT_READ_BY_THE_BROWSER = { + "locale:script": "the launcher joins it with locale:language/region into the UI locale", + "navigator.doNotTrack": "the launcher applies it as privacy.donottrackheader.enabled (#760)", + "navigator.buildID": "declared ahead of the patch that reads it (#780)", +} + +# How Juggler and the patches name a key: a quoted string literal. +QUOTED = '"{key}"', "'{key}'" + def _sources(): for pattern in ("patches/**/*.patch", "additions/**/*"): @@ -97,6 +109,34 @@ def test_every_key_the_browser_reads_is_declared(): pytest.fail("\n".join(lines)) +def test_every_declared_key_is_read_by_the_browser(): + """The other direction: canvas:seed (#721) was declared, generated by both + launchers and sent on every launch for three releases after the patch that + read it was removed (#528).""" + text = "".join(path.read_text(errors="ignore") for path in _sources()) + unread = sorted( + key + for key in _declared_keys() + if key not in NOT_READ_BY_THE_BROWSER + and not any(form.format(key=key) in text for form in QUOTED) + ) + assert not unread, ( + "settings/properties.json declares keys that no patch or Juggler file " + f"reads, so setting them does nothing: {unread}. Remove them, or add them " + "to NOT_READ_BY_THE_BROWSER with the reason they are declared." + ) + + +def test_keys_not_read_by_the_browser_are_really_unread(): + """An exemption must lapse once the browser starts reading the key.""" + text = "".join(path.read_text(errors="ignore") for path in _sources()) + now_read = [ + key for key in NOT_READ_BY_THE_BROWSER + if any(form.format(key=key) in text for form in QUOTED) + ] + assert not now_read, f"remove from NOT_READ_BY_THE_BROWSER, the browser reads them now: {now_read}" + + @pytest.mark.parametrize("key", ["media:spoof_codecs"]) def test_known_previously_missing_keys_stay_declared(key): """Pin the specific keys this guard was written for, so a schema edit that diff --git a/pythonlib/tests/test_identity_salt.py b/pythonlib/tests/test_identity_salt.py index f371cea..c61c0d6 100644 --- a/pythonlib/tests/test_identity_salt.py +++ b/pythonlib/tests/test_identity_salt.py @@ -42,7 +42,7 @@ def launch(**kwargs): return config_of(utils.launch_options(**kwargs)) -DRAWN = ("canvas:seed", "audio:seed", "fonts", "voices", "webGl:renderer") +DRAWN = ("audio:seed", "fonts", "voices", "webGl:renderer") def drawn(config): @@ -51,7 +51,7 @@ def drawn(config): class TestUnpinnedLaunchesAreDistinct: def test_noise_seeds_do_not_collide(self): - seeds = [launch()["canvas:seed"] for _ in range(40)] + seeds = [launch()["audio:seed"] for _ in range(40)] # 40 draws from 2**32: any collision means the seed space collapsed. assert len(set(seeds)) == len(seeds) @@ -77,7 +77,7 @@ class TestPinnedIdentityIsStable: pytest.skip("no presets bundled") first = launch(os="windows", fingerprint_preset=preset) second = launch(os="windows", fingerprint_preset=preset) - assert (first["canvas:seed"], first["audio:seed"]) == (second["canvas:seed"], second["audio:seed"]) + assert first["audio:seed"] == second["audio:seed"] assert first["fonts"] == second["fonts"] @pytest.mark.parametrize("os_name", ["windows", "macos", "linux"]) @@ -101,9 +101,17 @@ class TestPinnedIdentityIsStable: assert config.get("webGl:parameters"), (os_name, i) sample_webgl(key, config["webGl:vendor"], config["webGl:renderer"]) - def test_caller_seeds_are_kept(self): - config = launch(config={"canvas:seed": 7, "audio:seed": 9}) - assert (config["canvas:seed"], config["audio:seed"]) == (7, 9) + def test_caller_seed_is_kept(self): + assert launch(config={"audio:seed": 9})["audio:seed"] == 9 + + +def test_no_canvas_seed_is_generated(): + """The browser adds no canvas noise (#528), and no patch reads canvas:seed + (#721). Generating one only sent the browser a value it ignored.""" + assert "canvas:seed" not in launch() + context = fp.generate_context_fingerprint(os="linux") + assert "canvas:seed" not in context["config"] + assert "setCanvasSeed" not in context["init_script"] def test_salt_of_equal_objects_is_equal(self): a = fp.generate_fingerprint(os="windows") diff --git a/settings/properties.json b/settings/properties.json index f51b27a..b55153d 100644 --- a/settings/properties.json +++ b/settings/properties.json @@ -1,21 +1,14 @@ [ { "property": "navigator.userAgent", "type": "str" }, { "property": "navigator.doNotTrack", "type": "str" }, - { "property": "navigator.appCodeName", "type": "str" }, - { "property": "navigator.appName", "type": "str" }, { "property": "navigator.appVersion", "type": "str" }, { "property": "navigator.oscpu", "type": "str" }, { "property": "navigator.language", "type": "str" }, - { "property": "navigator.languages", "type": "array" }, { "property": "navigator.platform", "type": "str" }, { "property": "navigator.hardwareConcurrency", "type": "uint" }, - { "property": "navigator.product", "type": "str" }, - { "property": "navigator.productSub", "type": "str" }, { "property": "navigator.maxTouchPoints", "type": "uint" }, - { "property": "navigator.cookieEnabled", "type": "bool" }, { "property": "navigator.globalPrivacyControl", "type": "bool" }, { "property": "navigator.buildID", "type": "str" }, - { "property": "navigator.onLine", "type": "bool" }, { "property": "screen.availHeight", "type": "uint" }, { "property": "screen.availWidth", "type": "uint" }, { "property": "screen.availTop", "type": "uint" }, @@ -47,9 +40,6 @@ { "property": "headers.Accept-Encoding", "type": "str" }, { "property": "webrtc:ipv4", "type": "str" }, { "property": "webrtc:ipv6", "type": "str" }, - { "property": "webrtc:localipv4", "type": "str" }, - { "property": "webrtc:localipv6", "type": "str" }, - { "property": "pdfViewerEnabled", "type": "bool" }, { "property": "battery:charging", "type": "bool" }, { "property": "battery:chargingTime", "type": "double" }, { "property": "battery:dischargingTime", "type": "double" }, @@ -57,7 +47,6 @@ { "property": "fonts", "type": "array" }, { "property": "fonts:spacing_seed", "type": "uint" }, { "property": "audio:seed", "type": "uint" }, - { "property": "canvas:seed", "type": "uint" }, { "property": "geolocation:latitude", "type": "double" }, { "property": "geolocation:longitude", "type": "double" }, { "property": "geolocation:accuracy", "type": "double" }, @@ -78,17 +67,13 @@ { "property": "webGl:supportedExtensions", "type": "array" }, { "property": "webGl2:supportedExtensions", "type": "array" }, { "property": "webGl:parameters", "type": "dict" }, - { "property": "webGl:parameters:blockIfNotDefined", "type": "bool" }, { "property": "webGl2:parameters", "type": "dict" }, - { "property": "webGl2:parameters:blockIfNotDefined", "type": "bool" }, { "property": "webGl:shaderPrecisionFormats", "type": "dict" }, { "property": "webGl:shaderPrecisionFormats:blockIfNotDefined", "type": "bool" }, { "property": "webGl2:shaderPrecisionFormats", "type": "dict" }, { "property": "webGl2:shaderPrecisionFormats:blockIfNotDefined", "type": "bool" }, { "property": "webGl:contextAttributes", "type": "dict" }, { "property": "webGl2:contextAttributes", "type": "dict" }, - { "property": "canvas:aaOffset", "type": "int" }, - { "property": "canvas:aaCapOffset", "type": "bool" }, { "property": "voices", "type": "array" }, { "property": "voices:blockIfNotDefined", "type": "bool" }, { "property": "voices:fakeCompletion", "type": "bool" }, @@ -111,7 +96,6 @@ { "property": "disableTheming", "type": "bool" }, { "property": "disableInstantAnimations", "type": "bool" }, - { "property": "memorysaver", "type": "bool" }, { "property": "addons", "type": "array" }, { "property": "certificatePaths", "type": "array" }, { "property": "certificates", "type": "array" }, diff --git a/tests/patches/config-overrides.py b/tests/patches/config-overrides.py index 2c51b62..83ae176 100644 --- a/tests/patches/config-overrides.py +++ b/tests/patches/config-overrides.py @@ -122,19 +122,18 @@ async def test(): print(f" init_script has setFontSpacingSeed({val}): FAIL") # --- Test 4: other seeds are NOT affected by a font-only override --- - print("\n=== Test 4: audio/canvas seeds unaffected by font override ===") + print("\n=== Test 4: the audio seed is unaffected by a font override ===") preset4 = get_random_preset(os="macos") fp4 = generate_context_fingerprint( preset=preset4, config_overrides={"fonts:spacing_seed": 0}, ) audio = fp4["config"]["audio:seed"] - canvas = fp4["config"]["canvas:seed"] - if audio != 0 and canvas != 0: - print(f" audio:seed={audio}, canvas:seed={canvas} (both non-zero): PASS") + if audio != 0: + print(f" audio:seed={audio} (non-zero): PASS") else: - failures.append(f"Other seeds affected: audio={audio}, canvas={canvas}") - print(f" audio={audio}, canvas={canvas}: FAIL") + failures.append(f"audio seed affected: {audio}") + print(f" audio={audio}: FAIL") # --- Summary --- print("\n" + "=" * 50)