mirror of
https://github.com/daijro/camoufox.git
synced 2026-10-06 00:00:33 +00:00
feat(python): draw every identity's WebGL from fpgen
WebGL vendor, renderer, context attributes, extensions, parameters and shader precisions, for WebGL1 and WebGL2, now come from fpgen's recorded Firefox devices instead of webgl_data.db, which is deleted with the camoufox/webgl/ package. camoufox/webgl.py: - webgl_for_gpu() traces `webgl` given Firefox, the OS and the GPU, then `webgl2` given the chosen `webgl` too, and draws each with one seeded random.Random. The GPU and the webgl value are pinned by their fpgen lookup index: a dict condition is flattened into leaves that overwrite each other, so only the renderer applied and Linux "Mesa" and "AMD" Radeon HD 3200 devices came back mixed. - sample_webgl_for_screen() draws the GPU of a generated identity from fpgen's per-OS weights, filtering out software rasterisers, GPUs the OS cannot report, discrete GPUs behind a netbook screen and the resistFingerprinting "Mozilla" mask before the weighted choice, so there is no rejection loop. An empty pool raises. - The draft/host-dependent extension filter moves over unchanged. A preset's GPU and a caller's webgl_config pair are looked up as given; a pair fpgen has never seen from Firefox on that OS raises instead of falling back to another GPU. generate_context_fingerprint no longer falls back to the host GPU when the draw fails. For 10 of the 15 (GPU, OS) pairs the two sources share, one of fpgen's records converts to exactly the database row on every value the browser reads. The other five rows (Linux R9 200 and Radeon HD 3200, macOS Intel HD, and two software rasterisers) are devices fpgen does not carry; those GPUs now present fpgen's recorded devices instead. The Linux GTX 980 row is kept as a test fixture. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
40edebb925
commit
0d859b3a3d
@@ -0,0 +1,567 @@
|
||||
{
|
||||
"webGl:renderer": "NVIDIA GeForce GTX 980, or similar",
|
||||
"webGl:vendor": "NVIDIA Corporation",
|
||||
"webGl:contextAttributes": {
|
||||
"alpha": true,
|
||||
"antialias": true,
|
||||
"depth": true,
|
||||
"failIfMajorPerformanceCaveat": false,
|
||||
"powerPreference": "default",
|
||||
"premultipliedAlpha": true,
|
||||
"preserveDrawingBuffer": false,
|
||||
"stencil": false
|
||||
},
|
||||
"webGl:supportedExtensions": [
|
||||
"ANGLE_instanced_arrays",
|
||||
"EXT_blend_minmax",
|
||||
"EXT_color_buffer_half_float",
|
||||
"EXT_depth_clamp",
|
||||
"EXT_float_blend",
|
||||
"EXT_frag_depth",
|
||||
"EXT_shader_texture_lod",
|
||||
"EXT_sRGB",
|
||||
"EXT_texture_compression_bptc",
|
||||
"EXT_texture_compression_rgtc",
|
||||
"EXT_texture_filter_anisotropic",
|
||||
"OES_element_index_uint",
|
||||
"OES_fbo_render_mipmap",
|
||||
"OES_standard_derivatives",
|
||||
"OES_texture_float",
|
||||
"OES_texture_float_linear",
|
||||
"OES_texture_half_float",
|
||||
"OES_texture_half_float_linear",
|
||||
"OES_vertex_array_object",
|
||||
"WEBGL_color_buffer_float",
|
||||
"WEBGL_compressed_texture_etc",
|
||||
"WEBGL_compressed_texture_s3tc",
|
||||
"WEBGL_compressed_texture_s3tc_srgb",
|
||||
"WEBGL_debug_renderer_info",
|
||||
"WEBGL_debug_shaders",
|
||||
"WEBGL_depth_texture",
|
||||
"WEBGL_draw_buffers",
|
||||
"WEBGL_lose_context"
|
||||
],
|
||||
"webGl:parameters": {
|
||||
"2849": 1,
|
||||
"2884": false,
|
||||
"2885": 1029,
|
||||
"2886": 2305,
|
||||
"2928": [
|
||||
0,
|
||||
1
|
||||
],
|
||||
"2929": false,
|
||||
"2930": true,
|
||||
"2931": 1,
|
||||
"2932": 513,
|
||||
"2960": false,
|
||||
"2961": 0,
|
||||
"2962": 519,
|
||||
"2963": 4294967295,
|
||||
"2964": 7680,
|
||||
"2965": 7680,
|
||||
"2966": 7680,
|
||||
"2967": 0,
|
||||
"2968": 4294967295,
|
||||
"2978": [
|
||||
0,
|
||||
0,
|
||||
300,
|
||||
150
|
||||
],
|
||||
"3024": true,
|
||||
"3042": false,
|
||||
"3074": null,
|
||||
"3088": [
|
||||
0,
|
||||
0,
|
||||
300,
|
||||
150
|
||||
],
|
||||
"3089": false,
|
||||
"3106": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"3107": [
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
],
|
||||
"3314": null,
|
||||
"3315": null,
|
||||
"3316": null,
|
||||
"3317": 4,
|
||||
"3330": null,
|
||||
"3331": null,
|
||||
"3332": null,
|
||||
"3333": 4,
|
||||
"3379": 32768,
|
||||
"3386": [
|
||||
32768,
|
||||
32768
|
||||
],
|
||||
"3408": 8,
|
||||
"3410": 8,
|
||||
"3411": 8,
|
||||
"3412": 8,
|
||||
"3413": 8,
|
||||
"3414": 24,
|
||||
"3415": 0,
|
||||
"7936": "Mozilla",
|
||||
"7937": "NVIDIA GeForce GTX 980, or similar",
|
||||
"7938": "WebGL 1.0",
|
||||
"10752": 0,
|
||||
"32773": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"32777": 32774,
|
||||
"32823": false,
|
||||
"32824": 0,
|
||||
"32873": null,
|
||||
"32877": null,
|
||||
"32878": null,
|
||||
"32883": null,
|
||||
"32926": false,
|
||||
"32928": false,
|
||||
"32936": 1,
|
||||
"32937": 4,
|
||||
"32938": 1,
|
||||
"32939": false,
|
||||
"32968": 0,
|
||||
"32969": 1,
|
||||
"32970": 0,
|
||||
"32971": 1,
|
||||
"33000": null,
|
||||
"33001": null,
|
||||
"33170": 4352,
|
||||
"33901": [
|
||||
1,
|
||||
2047
|
||||
],
|
||||
"33902": [
|
||||
1,
|
||||
10
|
||||
],
|
||||
"34016": 33984,
|
||||
"34024": 32768,
|
||||
"34045": null,
|
||||
"34047": null,
|
||||
"34068": null,
|
||||
"34076": 32768,
|
||||
"34467": null,
|
||||
"34816": 519,
|
||||
"34817": 7680,
|
||||
"34818": 7680,
|
||||
"34819": 7680,
|
||||
"34852": null,
|
||||
"34853": null,
|
||||
"34854": null,
|
||||
"34855": null,
|
||||
"34856": null,
|
||||
"34857": null,
|
||||
"34858": null,
|
||||
"34859": null,
|
||||
"34860": null,
|
||||
"34877": 32774,
|
||||
"34921": 16,
|
||||
"34930": 32,
|
||||
"34964": null,
|
||||
"34965": null,
|
||||
"35071": null,
|
||||
"35076": null,
|
||||
"35077": null,
|
||||
"35371": null,
|
||||
"35373": null,
|
||||
"35374": null,
|
||||
"35375": null,
|
||||
"35376": null,
|
||||
"35377": null,
|
||||
"35379": null,
|
||||
"35380": null,
|
||||
"35657": null,
|
||||
"35658": null,
|
||||
"35659": null,
|
||||
"35660": 32,
|
||||
"35661": 192,
|
||||
"35723": null,
|
||||
"35724": "WebGL GLSL ES 1.0",
|
||||
"35725": null,
|
||||
"35738": 5121,
|
||||
"35739": 6408,
|
||||
"35968": null,
|
||||
"35977": null,
|
||||
"35978": null,
|
||||
"35979": null,
|
||||
"36003": 0,
|
||||
"36004": 4294967295,
|
||||
"36005": 4294967295,
|
||||
"36006": null,
|
||||
"36007": null,
|
||||
"36063": null,
|
||||
"36183": null,
|
||||
"36203": null,
|
||||
"36345": null,
|
||||
"36347": 1024,
|
||||
"36348": 32,
|
||||
"36349": 1024,
|
||||
"36387": null,
|
||||
"36388": null,
|
||||
"36392": null,
|
||||
"36795": null,
|
||||
"37137": null,
|
||||
"37154": null,
|
||||
"37157": null,
|
||||
"37440": false,
|
||||
"37441": false,
|
||||
"37443": 37444,
|
||||
"37444": null,
|
||||
"37445": "NVIDIA Corporation",
|
||||
"37446": "NVIDIA GeForce GTX 980, or similar",
|
||||
"37447": null,
|
||||
"38449": null
|
||||
},
|
||||
"webGl:shaderPrecisionFormats": {
|
||||
"35633,36336": {
|
||||
"rangeMin": 127,
|
||||
"rangeMax": 127,
|
||||
"precision": 23
|
||||
},
|
||||
"35633,36337": {
|
||||
"rangeMin": 127,
|
||||
"rangeMax": 127,
|
||||
"precision": 23
|
||||
},
|
||||
"35633,36338": {
|
||||
"rangeMin": 127,
|
||||
"rangeMax": 127,
|
||||
"precision": 23
|
||||
},
|
||||
"35633,36339": {
|
||||
"rangeMin": 24,
|
||||
"rangeMax": 24,
|
||||
"precision": 0
|
||||
},
|
||||
"35633,36340": {
|
||||
"rangeMin": 24,
|
||||
"rangeMax": 24,
|
||||
"precision": 0
|
||||
},
|
||||
"35633,36341": {
|
||||
"rangeMin": 24,
|
||||
"rangeMax": 24,
|
||||
"precision": 0
|
||||
},
|
||||
"35632,36336": {
|
||||
"rangeMin": 127,
|
||||
"rangeMax": 127,
|
||||
"precision": 23
|
||||
},
|
||||
"35632,36337": {
|
||||
"rangeMin": 127,
|
||||
"rangeMax": 127,
|
||||
"precision": 23
|
||||
},
|
||||
"35632,36338": {
|
||||
"rangeMin": 127,
|
||||
"rangeMax": 127,
|
||||
"precision": 23
|
||||
},
|
||||
"35632,36339": {
|
||||
"rangeMin": 24,
|
||||
"rangeMax": 24,
|
||||
"precision": 0
|
||||
},
|
||||
"35632,36340": {
|
||||
"rangeMin": 24,
|
||||
"rangeMax": 24,
|
||||
"precision": 0
|
||||
},
|
||||
"35632,36341": {
|
||||
"rangeMin": 24,
|
||||
"rangeMax": 24,
|
||||
"precision": 0
|
||||
}
|
||||
},
|
||||
"webGl2:contextAttributes": {
|
||||
"alpha": true,
|
||||
"antialias": true,
|
||||
"depth": true,
|
||||
"failIfMajorPerformanceCaveat": false,
|
||||
"powerPreference": "default",
|
||||
"premultipliedAlpha": true,
|
||||
"preserveDrawingBuffer": false,
|
||||
"stencil": false
|
||||
},
|
||||
"webGl2:supportedExtensions": [
|
||||
"EXT_color_buffer_float",
|
||||
"EXT_depth_clamp",
|
||||
"EXT_float_blend",
|
||||
"EXT_texture_compression_bptc",
|
||||
"EXT_texture_compression_rgtc",
|
||||
"EXT_texture_filter_anisotropic",
|
||||
"OES_draw_buffers_indexed",
|
||||
"OES_texture_float_linear",
|
||||
"WEBGL_compressed_texture_etc",
|
||||
"WEBGL_compressed_texture_s3tc",
|
||||
"WEBGL_compressed_texture_s3tc_srgb",
|
||||
"WEBGL_debug_renderer_info",
|
||||
"WEBGL_debug_shaders",
|
||||
"WEBGL_lose_context"
|
||||
],
|
||||
"webGl2:parameters": {
|
||||
"2849": 1,
|
||||
"2884": false,
|
||||
"2885": 1029,
|
||||
"2886": 2305,
|
||||
"2928": [
|
||||
0,
|
||||
1
|
||||
],
|
||||
"2929": false,
|
||||
"2930": true,
|
||||
"2931": 1,
|
||||
"2932": 513,
|
||||
"2960": false,
|
||||
"2961": 0,
|
||||
"2962": 519,
|
||||
"2963": 4294967295,
|
||||
"2964": 7680,
|
||||
"2965": 7680,
|
||||
"2966": 7680,
|
||||
"2967": 0,
|
||||
"2968": 4294967295,
|
||||
"2978": [
|
||||
0,
|
||||
0,
|
||||
300,
|
||||
150
|
||||
],
|
||||
"3024": true,
|
||||
"3042": false,
|
||||
"3074": 1029,
|
||||
"3088": [
|
||||
0,
|
||||
0,
|
||||
300,
|
||||
150
|
||||
],
|
||||
"3089": false,
|
||||
"3106": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"3107": [
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
],
|
||||
"3314": 0,
|
||||
"3315": 0,
|
||||
"3316": 0,
|
||||
"3317": 4,
|
||||
"3330": 0,
|
||||
"3331": 0,
|
||||
"3332": 0,
|
||||
"3333": 4,
|
||||
"3379": 32768,
|
||||
"3386": [
|
||||
32768,
|
||||
32768
|
||||
],
|
||||
"3408": 8,
|
||||
"3410": 8,
|
||||
"3411": 8,
|
||||
"3412": 8,
|
||||
"3413": 8,
|
||||
"3414": 24,
|
||||
"3415": 0,
|
||||
"7936": "Mozilla",
|
||||
"7937": "NVIDIA GeForce GTX 980, or similar",
|
||||
"7938": "WebGL 2.0",
|
||||
"10752": 0,
|
||||
"32773": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"32777": 32774,
|
||||
"32823": false,
|
||||
"32824": 0,
|
||||
"32873": null,
|
||||
"32877": 0,
|
||||
"32878": 0,
|
||||
"32883": 16384,
|
||||
"32926": false,
|
||||
"32928": false,
|
||||
"32936": 1,
|
||||
"32937": 4,
|
||||
"32938": 1,
|
||||
"32939": false,
|
||||
"32968": 0,
|
||||
"32969": 1,
|
||||
"32970": 0,
|
||||
"32971": 1,
|
||||
"33000": 1048576,
|
||||
"33001": 1048576,
|
||||
"33170": 4352,
|
||||
"33901": [
|
||||
1,
|
||||
2047
|
||||
],
|
||||
"33902": [
|
||||
1,
|
||||
1
|
||||
],
|
||||
"34016": 33984,
|
||||
"34024": 32768,
|
||||
"34045": 15,
|
||||
"34047": null,
|
||||
"34068": null,
|
||||
"34076": 32768,
|
||||
"34467": null,
|
||||
"34816": 519,
|
||||
"34817": 7680,
|
||||
"34818": 7680,
|
||||
"34819": 7680,
|
||||
"34852": 8,
|
||||
"34853": 1029,
|
||||
"34854": 0,
|
||||
"34855": 0,
|
||||
"34856": 0,
|
||||
"34857": 0,
|
||||
"34858": 0,
|
||||
"34859": 0,
|
||||
"34860": 0,
|
||||
"34877": 32774,
|
||||
"34921": 16,
|
||||
"34930": 32,
|
||||
"34964": null,
|
||||
"34965": null,
|
||||
"35071": 2048,
|
||||
"35076": -8,
|
||||
"35077": 7,
|
||||
"35371": 14,
|
||||
"35373": 14,
|
||||
"35374": 84,
|
||||
"35375": 84,
|
||||
"35376": 65536,
|
||||
"35377": 233472,
|
||||
"35379": 233472,
|
||||
"35380": 256,
|
||||
"35657": 4096,
|
||||
"35658": 4096,
|
||||
"35659": 124,
|
||||
"35660": 32,
|
||||
"35661": 192,
|
||||
"35723": 4352,
|
||||
"35724": "WebGL GLSL ES 3.00",
|
||||
"35725": null,
|
||||
"35738": 5121,
|
||||
"35739": 6408,
|
||||
"35968": 4,
|
||||
"35977": false,
|
||||
"35978": 128,
|
||||
"35979": 4,
|
||||
"36003": 0,
|
||||
"36004": 4294967295,
|
||||
"36005": 4294967295,
|
||||
"36006": null,
|
||||
"36007": null,
|
||||
"36063": 8,
|
||||
"36183": 32,
|
||||
"36203": 4294967295,
|
||||
"36345": null,
|
||||
"36347": 1024,
|
||||
"36348": 32,
|
||||
"36349": 1024,
|
||||
"36387": false,
|
||||
"36388": false,
|
||||
"36392": null,
|
||||
"36795": null,
|
||||
"37137": 1.8446744073709552e+19,
|
||||
"37154": 128,
|
||||
"37157": 128,
|
||||
"37440": false,
|
||||
"37441": false,
|
||||
"37443": 37444,
|
||||
"37444": null,
|
||||
"37445": "NVIDIA Corporation",
|
||||
"37446": "NVIDIA GeForce GTX 980, or similar",
|
||||
"37447": 1000000000,
|
||||
"38449": null
|
||||
},
|
||||
"webGl2:shaderPrecisionFormats": {
|
||||
"35633,36336": {
|
||||
"rangeMin": 127,
|
||||
"rangeMax": 127,
|
||||
"precision": 23
|
||||
},
|
||||
"35633,36337": {
|
||||
"rangeMin": 127,
|
||||
"rangeMax": 127,
|
||||
"precision": 23
|
||||
},
|
||||
"35633,36338": {
|
||||
"rangeMin": 127,
|
||||
"rangeMax": 127,
|
||||
"precision": 23
|
||||
},
|
||||
"35633,36339": {
|
||||
"rangeMin": 24,
|
||||
"rangeMax": 24,
|
||||
"precision": 0
|
||||
},
|
||||
"35633,36340": {
|
||||
"rangeMin": 24,
|
||||
"rangeMax": 24,
|
||||
"precision": 0
|
||||
},
|
||||
"35633,36341": {
|
||||
"rangeMin": 24,
|
||||
"rangeMax": 24,
|
||||
"precision": 0
|
||||
},
|
||||
"35632,36336": {
|
||||
"rangeMin": 127,
|
||||
"rangeMax": 127,
|
||||
"precision": 23
|
||||
},
|
||||
"35632,36337": {
|
||||
"rangeMin": 127,
|
||||
"rangeMax": 127,
|
||||
"precision": 23
|
||||
},
|
||||
"35632,36338": {
|
||||
"rangeMin": 127,
|
||||
"rangeMax": 127,
|
||||
"precision": 23
|
||||
},
|
||||
"35632,36339": {
|
||||
"rangeMin": 24,
|
||||
"rangeMax": 24,
|
||||
"precision": 0
|
||||
},
|
||||
"35632,36340": {
|
||||
"rangeMin": 24,
|
||||
"rangeMax": 24,
|
||||
"precision": 0
|
||||
},
|
||||
"35632,36341": {
|
||||
"rangeMin": 24,
|
||||
"rangeMax": 24,
|
||||
"precision": 0
|
||||
}
|
||||
},
|
||||
"webGl2Enabled": true
|
||||
}
|
||||
@@ -5,8 +5,6 @@ a page can see. Each site warns with a report block the user can paste into a
|
||||
GitHub issue, so the failure reaches us instead of shipping silently.
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
|
||||
import pytest
|
||||
from test_identity_salt import launch
|
||||
|
||||
@@ -56,7 +54,6 @@ def test_preset_voice_draw(monkeypatch):
|
||||
[
|
||||
("_generate_random_font_subset", OSError("fonts.json missing"), "fonts"),
|
||||
("_generate_random_voice_subset", ValueError("bad manifest"), "voices"),
|
||||
("sample_webgl_for_screen", sqlite3.OperationalError("no such table"), "webGl:renderer"),
|
||||
],
|
||||
)
|
||||
def test_context_draws(monkeypatch, target, error, key):
|
||||
@@ -83,14 +80,3 @@ def test_launch_font_draw(monkeypatch):
|
||||
with pytest.warns(FallbackWarning, match=REPORT):
|
||||
config = launch()
|
||||
assert config["fonts"]
|
||||
|
||||
|
||||
def test_preset_gpu_not_in_webgl_data():
|
||||
# Windows reports every GPU through ANGLE, so only an ANGLE string reaches the lookup.
|
||||
GPU = "ANGLE (Acme, Acme GPU 9000 Direct3D11 vs_5_0 ps_5_0)"
|
||||
preset = fp.load_presets("152")["presets"]["windows"][0]
|
||||
preset = {**preset, "webgl": {"unmaskedVendor": "Google Inc. (Acme)", "unmaskedRenderer": GPU}}
|
||||
with pytest.warns(FallbackWarning, match=REPORT) as record:
|
||||
config = launch(os="windows", fingerprint_preset=preset)
|
||||
assert GPU in _report(record)
|
||||
assert config["webGl:renderer"] != GPU
|
||||
|
||||
@@ -83,23 +83,13 @@ class TestPinnedIdentityIsStable:
|
||||
@pytest.mark.parametrize("os_name", ["windows", "macos", "linux"])
|
||||
def test_every_bundled_preset_launches(self, os_name):
|
||||
# The test above draws ONE preset at random, so a preset that cannot
|
||||
# launch shows up as a 1-in-11 flake rather than a failure -- which is
|
||||
# how it reached CI. 39 of the 435 bundled presets name a GPU that is
|
||||
# not among the 31 in webgl_data.db, and sample_webgl raises for those.
|
||||
# Every preset has to produce launch options; see the fallback in
|
||||
# utils.launch_options.
|
||||
from camoufox.webgl import sample_webgl
|
||||
|
||||
# launch would show up as a flake rather than a failure. Every preset
|
||||
# must launch with its own GPU and that GPU's recorded parameters.
|
||||
presets = fp.load_presets("150")["presets"][os_name]
|
||||
key = {"windows": "win", "macos": "mac", "linux": "lin"}[os_name]
|
||||
for i, preset in enumerate(presets):
|
||||
config = launch(os=os_name, fingerprint_preset=preset)
|
||||
# Whatever GPU survives, the renderer the page reads and the
|
||||
# parameters behind it must come from the SAME recorded device --
|
||||
# merge_into does not overwrite, so a fallback that forgets to drop
|
||||
# the preset's pair leaves one device's name on another's data.
|
||||
assert config["webGl:renderer"] == preset["webgl"]["unmaskedRenderer"], (os_name, i)
|
||||
assert config.get("webGl:parameters"), (os_name, i)
|
||||
sample_webgl(key, config["webGl:vendor"], config["webGl:renderer"])
|
||||
|
||||
def test_caller_seed_is_kept(self):
|
||||
assert launch(config={"audio:seed": 9})["audio:seed"] == 9
|
||||
|
||||
@@ -9,11 +9,10 @@ rules -- `scripts/clean-fingerprint-data.py --write` is what makes these pass.
|
||||
Dropped on 2026-09-17: 38 of 435 presets (26 with a GPU their OS cannot report,
|
||||
7 pairing Apple Silicon with a core count Apple never shipped, 4 with a colour
|
||||
depth their GPU contradicts, 3 with a phone viewport, 1 claiming 40 touch
|
||||
points), and 2 impossible macOS weights in webgl_data.db.
|
||||
points).
|
||||
"""
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
import sys
|
||||
from os.path import dirname, join
|
||||
from pathlib import Path
|
||||
@@ -24,7 +23,6 @@ sys.path.insert(0, join(dirname(__file__), ".."))
|
||||
|
||||
from camoufox import coherence # noqa: E402
|
||||
from camoufox.fingerprints import from_preset # noqa: E402
|
||||
from camoufox.webgl.sample import DB_PATH # noqa: E402
|
||||
|
||||
DATA = Path(__file__).parent.parent / "camoufox"
|
||||
PRESET_FILES = ("fingerprint-presets.json", "fingerprint-presets-v150.json")
|
||||
@@ -46,62 +44,12 @@ def test_every_bundled_preset_is_coherent_as_stored(filename):
|
||||
)
|
||||
|
||||
|
||||
def test_no_gpu_is_offered_to_an_os_that_cannot_report_it():
|
||||
connection = sqlite3.connect(DB_PATH)
|
||||
try:
|
||||
rows = connection.execute(
|
||||
"SELECT vendor, renderer, win, mac, lin FROM webgl_fingerprints"
|
||||
).fetchall()
|
||||
finally:
|
||||
connection.close()
|
||||
assert rows, "webgl_data.db is empty"
|
||||
for vendor, renderer, *weights in rows:
|
||||
for os_key, weight in zip(("win", "mac", "lin"), weights):
|
||||
if weight and weight > 0:
|
||||
assert coherence.gpu_fits_os(renderer, os_key), (
|
||||
f"{renderer!r} is offered to {os_key} at {weight}"
|
||||
)
|
||||
|
||||
|
||||
def test_each_os_still_has_gpus_to_draw_from():
|
||||
"""The filter must not empty a pool -- a single GPU per OS is its own tell."""
|
||||
connection = sqlite3.connect(DB_PATH)
|
||||
try:
|
||||
for os_key in ("win", "mac", "lin"):
|
||||
count = connection.execute(
|
||||
f"SELECT COUNT(*) FROM webgl_fingerprints WHERE {os_key} > 0" # nosec
|
||||
).fetchone()[0]
|
||||
assert count >= 2, f"{os_key} has {count} GPU(s) left"
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
|
||||
def test_every_gpu_row_can_be_drawn_on_some_os():
|
||||
"""A row no OS can draw is dead weight a reader would take as real data."""
|
||||
connection = sqlite3.connect(DB_PATH)
|
||||
try:
|
||||
rows = connection.execute(
|
||||
"SELECT vendor, renderer, win, mac, lin FROM webgl_fingerprints"
|
||||
).fetchall()
|
||||
finally:
|
||||
connection.close()
|
||||
undrawable = [
|
||||
(vendor, renderer)
|
||||
for vendor, renderer, *weights in rows
|
||||
if not any(
|
||||
weight > 0 and coherence.gpu_fits_os(renderer, os_key)
|
||||
for os_key, weight in zip(("win", "mac", "lin"), weights)
|
||||
)
|
||||
]
|
||||
assert undrawable == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("filename", PRESET_FILES)
|
||||
def test_every_preset_gpu_has_webgl_data(filename):
|
||||
"""A preset records only its GPU's name; the WebGL parameters behind it come
|
||||
from fpgen. A GPU fpgen has never seen Firefox report on that OS has none, so
|
||||
launching it would pair the name with another device's parameters."""
|
||||
from camoufox.fingerprints import firefox_gpus
|
||||
from camoufox.webgl import firefox_gpus
|
||||
|
||||
presets = json.loads((DATA / filename).read_text())["presets"]
|
||||
for os_name, entries in presets.items():
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
"""WebGL identities drawn from fpgen's recorded Firefox devices (camoufox.webgl)."""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from test_identity_salt import host, launch
|
||||
|
||||
from camoufox import coherence, utils
|
||||
from camoufox import fingerprints as fp
|
||||
from camoufox import webgl
|
||||
from camoufox.fingerprints import gpu_screen_is_plausible, is_software_renderer
|
||||
from camoufox.webgl import sample_webgl_for_screen, webgl_for_gpu
|
||||
|
||||
SEEDS = range(300)
|
||||
OSES = ("win", "mac", "lin")
|
||||
|
||||
_GTX_980_LINUX = ("NVIDIA Corporation", "NVIDIA GeForce GTX 980, or similar")
|
||||
_BASIC_RENDER_DRIVER = (
|
||||
"Google Inc. (Microsoft)",
|
||||
"ANGLE (Microsoft, Microsoft Basic Render Driver Direct3D11 vs_5_0 ps_5_0), or similar",
|
||||
)
|
||||
|
||||
# Limits WebGL1 and WebGL2 read from the same device: MAX_TEXTURE_SIZE,
|
||||
# MAX_VIEWPORT_DIMS, MAX_RENDERBUFFER_SIZE, MAX_CUBE_MAP_TEXTURE_SIZE,
|
||||
# MAX_VERTEX_ATTRIBS, MAX_TEXTURE_IMAGE_UNITS, MAX_VERTEX_TEXTURE_IMAGE_UNITS,
|
||||
# MAX_COMBINED_TEXTURE_IMAGE_UNITS and the three uniform/varying vector limits.
|
||||
_SHARED_LIMITS = ("3379", "3386", "34024", "34076", "34921", "34930", "35660", "35661", "36347", "36348", "36349")
|
||||
|
||||
|
||||
def _as_json(value):
|
||||
# JSON has one number type: 2**64 stored as an int and as a float is the
|
||||
# same value to the browser's parser.
|
||||
return json.loads(json.dumps(value), parse_int=float)
|
||||
|
||||
|
||||
def test_converter_reproduces_the_recorded_device():
|
||||
"""The fixture is what the retired webgl_data.db gave launch_options for
|
||||
this GPU. fpgen records the same device, so everything the browser reads
|
||||
must come out identical: parameters are compared where the old row had a
|
||||
value, less the UNMASKED_* strings, which the browser takes from
|
||||
webGl:vendor/renderer rather than the table."""
|
||||
old = json.loads((Path(__file__).parent / "data" / "webgl-gtx980-linux.json").read_text())
|
||||
new = webgl_for_gpu("lin", *_GTX_980_LINUX, seed=0)
|
||||
|
||||
assert new.keys() == old.keys()
|
||||
for key in old:
|
||||
if key.endswith(":parameters"):
|
||||
recorded = {
|
||||
pname: value
|
||||
for pname, value in old[key].items()
|
||||
if value is not None and pname not in ("37445", "37446")
|
||||
}
|
||||
assert _as_json({pname: new[key].get(pname) for pname in recorded}) == _as_json(recorded), key
|
||||
else:
|
||||
assert new[key] == old[key], key
|
||||
|
||||
|
||||
def test_same_seed_same_device():
|
||||
for target_os in OSES:
|
||||
for seed in (0, 1, 12345):
|
||||
assert sample_webgl_for_screen(target_os, 1920, 1080, seed) == sample_webgl_for_screen(
|
||||
target_os, 1920, 1080, seed
|
||||
)
|
||||
gpu = ("AMD", "Radeon R9 200 Series, or similar")
|
||||
assert webgl_for_gpu("lin", *gpu, seed=7) == webgl_for_gpu("lin", *gpu, seed=7)
|
||||
|
||||
|
||||
def test_seed_chooses_among_a_gpus_recorded_devices():
|
||||
# fpgen records several Linux R9 200 devices; one seed must not pin them all.
|
||||
drawn = {json.dumps(webgl_for_gpu("lin", "AMD", "Radeon R9 200 Series, or similar", seed=s)) for s in range(40)}
|
||||
assert len(drawn) > 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("target_os", OSES)
|
||||
def test_synthetic_draw_is_a_hardware_gpu_the_os_reports(target_os):
|
||||
renderers = {sample_webgl_for_screen(target_os, 1920, 1080, s)["webGl:renderer"] for s in SEEDS}
|
||||
for renderer in renderers:
|
||||
assert not is_software_renderer(renderer), renderer
|
||||
assert renderer != "Mozilla"
|
||||
assert coherence.gpu_fits_os(renderer, target_os), renderer
|
||||
# A single GPU per OS is its own tell.
|
||||
assert len(renderers) >= 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize("target_os", OSES)
|
||||
def test_netbook_screen_never_draws_a_discrete_gpu(target_os):
|
||||
for seed in SEEDS:
|
||||
renderer = sample_webgl_for_screen(target_os, 1024, 600, seed)["webGl:renderer"]
|
||||
assert gpu_screen_is_plausible(renderer, 1024, 600), renderer
|
||||
|
||||
|
||||
def test_no_coherent_gpu_raises(monkeypatch):
|
||||
# A pool with nothing that fits is a data defect; substituting a GPU the
|
||||
# filters rejected would present exactly what they exist to prevent.
|
||||
only_software = tuple(r for r in webgl._trace("gpu", "lin") if is_software_renderer(r.value["renderer"]))
|
||||
assert only_software
|
||||
monkeypatch.setattr(webgl, "_trace", lambda *a, **kw: only_software)
|
||||
with pytest.raises(ValueError, match="No recorded lin GPU"):
|
||||
sample_webgl_for_screen("lin", 1920, 1080, seed=0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("target_os", OSES)
|
||||
def test_webgl2_comes_from_the_same_device_as_webgl1(target_os):
|
||||
# webgl2 is pinned to the drawn webgl; drawn on the GPU alone, a Linux
|
||||
# Intel identity paired MAX_TEXTURE_SIZE 8192 with 16384.
|
||||
for seed in SEEDS:
|
||||
config = sample_webgl_for_screen(target_os, 1920, 1080, seed)
|
||||
for pname in _SHARED_LIMITS:
|
||||
assert config["webGl:parameters"][pname] == config["webGl2:parameters"][pname], (seed, pname)
|
||||
|
||||
|
||||
def test_gpu_is_pinned_by_vendor_and_renderer():
|
||||
# "Mesa" and "AMD" both report this renderer on Linux. A dict condition on
|
||||
# fpgen matches the renderer alone and mixed their devices.
|
||||
for seed in range(40):
|
||||
assert webgl_for_gpu("lin", "Mesa", "Radeon HD 3200 Graphics, or similar", seed)["webGl:vendor"] == "Mesa"
|
||||
|
||||
|
||||
def test_device_without_webgl2():
|
||||
config = webgl_for_gpu("win", *_BASIC_RENDER_DRIVER, seed=0)
|
||||
assert config["webGl2Enabled"] is False
|
||||
assert not any(key.startswith("webGl2:") for key in config)
|
||||
|
||||
with host():
|
||||
options = utils.launch_options(
|
||||
os="windows", webgl_config=_BASIC_RENDER_DRIVER, headless=True, i_know_what_im_doing=True
|
||||
)
|
||||
assert options["firefox_user_prefs"]["webgl.enable-webgl2"] is False
|
||||
|
||||
|
||||
def test_unknown_webgl_config_raises():
|
||||
with pytest.raises(ValueError, match="No recorded WebGL data"):
|
||||
launch(os="windows", webgl_config=("Apple", "Apple M1, or similar"))
|
||||
|
||||
|
||||
def test_preset_keeps_its_own_gpu():
|
||||
preset = fp.load_presets("152")["presets"]["linux"][0]
|
||||
gpu = (preset["webgl"]["unmaskedVendor"], preset["webgl"]["unmaskedRenderer"])
|
||||
config = launch(os="linux", fingerprint_preset=preset)
|
||||
assert (config["webGl:vendor"], config["webGl:renderer"]) == gpu
|
||||
pinned = (webgl._pin("gpu", {"vendor": gpu[0], "renderer": gpu[1]}),)
|
||||
recorded = [webgl.to_config(r.value, [], "lin")["webGl:parameters"] for r in webgl._trace("webgl", "lin", pinned)]
|
||||
assert config["webGl:parameters"] in recorded
|
||||
|
||||
|
||||
def test_preset_gpu_fpgen_has_never_seen_raises():
|
||||
preset = fp.load_presets("152")["presets"]["windows"][0]
|
||||
gpu = "ANGLE (Acme, Acme GPU 9000 Direct3D11 vs_5_0 ps_5_0)"
|
||||
preset = {**preset, "webgl": {"unmaskedVendor": "Google Inc. (Acme)", "unmaskedRenderer": gpu}}
|
||||
with pytest.raises(ValueError, match="Acme GPU 9000"):
|
||||
launch(os="windows", fingerprint_preset=preset)
|
||||
|
||||
|
||||
# -- extensions ---------------------------------------------------------------
|
||||
|
||||
|
||||
def _extensions(target_os, key):
|
||||
return [
|
||||
set(sample_webgl_for_screen(target_os, 1920, 1080, s).get(key) or ()) for s in range(100)
|
||||
]
|
||||
|
||||
|
||||
def test_windows_keeps_ovr_multiview2_on_webgl2():
|
||||
assert any("OVR_multiview2" in exts for exts in _extensions("win", "webGl2:supportedExtensions"))
|
||||
|
||||
|
||||
def test_linux_filters_ovr_multiview2():
|
||||
# fpgen records it on ~20% of Linux WebGL2 devices.
|
||||
assert not any("OVR_multiview2" in exts for exts in _extensions("lin", "webGl2:supportedExtensions"))
|
||||
|
||||
|
||||
def test_draft_extensions_filtered_on_every_os():
|
||||
recorded = {
|
||||
"vendor": "v",
|
||||
"renderer": "r",
|
||||
"contextAttributes": {},
|
||||
"params": {},
|
||||
"shaderPrecisionFormats": [],
|
||||
"supportedExtensions": ["ANGLE_instanced_arrays", "WEBGL_multi_draw", "WEBGL_compressed_texture_etc1"],
|
||||
}
|
||||
webgl2 = {**recorded, "supportedExtensions": ["EXT_texture_norm16", "WEBGL_clip_cull_distance", "OVR_multiview2"]}
|
||||
for target_os in OSES:
|
||||
config = webgl.to_config(recorded, webgl2, target_os)
|
||||
assert config["webGl:supportedExtensions"] == ["ANGLE_instanced_arrays"]
|
||||
assert config["webGl2:supportedExtensions"] == (["OVR_multiview2"] if target_os == "win" else [])
|
||||
@@ -1,60 +0,0 @@
|
||||
"""Which sampled WebGL extensions reach the page, per OS."""
|
||||
|
||||
import sqlite3
|
||||
|
||||
import orjson
|
||||
|
||||
from camoufox.webgl import sample
|
||||
from camoufox.webgl.sample import DB_PATH, _load_webgl_data
|
||||
|
||||
|
||||
def _row_with(ext, key="webGl2:supportedExtensions", os="win"):
|
||||
con = sqlite3.connect(DB_PATH)
|
||||
try:
|
||||
for (data,) in con.execute(f"SELECT data FROM webgl_fingerprints WHERE {os} > 0"): # nosec
|
||||
if ext in (orjson.loads(data).get(key) or []):
|
||||
return data
|
||||
finally:
|
||||
con.close()
|
||||
raise AssertionError(f"no {os} row carries {ext}")
|
||||
|
||||
|
||||
def test_windows_keeps_ovr_multiview2_on_webgl2():
|
||||
data = _load_webgl_data(_row_with("OVR_multiview2"), "win")
|
||||
assert "OVR_multiview2" in data["webGl2:supportedExtensions"]
|
||||
|
||||
|
||||
def test_linux_filters_ovr_multiview2():
|
||||
data = _load_webgl_data(_row_with("OVR_multiview2", os="lin"), "lin")
|
||||
assert "OVR_multiview2" not in data["webGl2:supportedExtensions"]
|
||||
|
||||
|
||||
def test_ovr_multiview2_never_on_webgl1_in_corpus():
|
||||
con = sqlite3.connect(DB_PATH)
|
||||
try:
|
||||
for (data,) in con.execute("SELECT data FROM webgl_fingerprints"):
|
||||
assert "OVR_multiview2" not in (orjson.loads(data).get("webGl:supportedExtensions") or [])
|
||||
finally:
|
||||
con.close()
|
||||
|
||||
|
||||
def test_draft_extensions_filtered_on_every_os():
|
||||
blob = orjson.dumps(
|
||||
{
|
||||
"webGl:supportedExtensions": ["ANGLE_instanced_arrays", "WEBGL_multi_draw"],
|
||||
"webGl2:supportedExtensions": ["EXT_texture_norm16", "WEBGL_clip_cull_distance", "OVR_multiview2"],
|
||||
}
|
||||
)
|
||||
for os in ("win", "mac", "lin"):
|
||||
data = _load_webgl_data(blob, os)
|
||||
assert data["webGl:supportedExtensions"] == ["ANGLE_instanced_arrays"]
|
||||
assert "EXT_texture_norm16" not in data["webGl2:supportedExtensions"]
|
||||
assert "WEBGL_clip_cull_distance" not in data["webGl2:supportedExtensions"]
|
||||
|
||||
|
||||
def test_sampled_windows_identities_can_carry_it():
|
||||
hits = sum(
|
||||
"OVR_multiview2" in (sample.sample_webgl("win", seed=s).get("webGl2:supportedExtensions") or [])
|
||||
for s in range(200)
|
||||
)
|
||||
assert hits > 0
|
||||
@@ -4,8 +4,8 @@ Tests for the WebGL <-> screen coherence helpers in camoufox.fingerprints.
|
||||
Run with:
|
||||
cd pythonlib && python -m pytest tests/test_webgl_screen_consistency.py -v
|
||||
|
||||
The regression these guard (daijro/camoufox#729): BrowserForge picks the
|
||||
screen, webgl_data.db picks the GPU, and nothing ties them together -- so the
|
||||
The regression these guard (daijro/camoufox#729): the generator picks the
|
||||
screen, camoufox.webgl picks the GPU, and nothing ties them together -- so the
|
||||
synthetic path can emit pairs no real machine ships (a discrete GPU behind a
|
||||
1024x600 netbook panel).
|
||||
|
||||
@@ -24,15 +24,14 @@ import pytest
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
from camoufox import fingerprints # noqa: E402
|
||||
from camoufox.fingerprints import ( # noqa: E402
|
||||
MODERN_SCREEN_FLOOR,
|
||||
_renderer_bucket,
|
||||
gpu_screen_is_plausible,
|
||||
is_software_renderer,
|
||||
raise_screen_to_modern_floor,
|
||||
sample_webgl_for_screen,
|
||||
)
|
||||
from camoufox.webgl import sample_webgl_for_screen # noqa: E402
|
||||
|
||||
# The three spellings Gecko emits for one discrete-NVIDIA bucket.
|
||||
_NV_ANGLE = "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0), or similar"
|
||||
@@ -153,64 +152,10 @@ def test_hardware_is_not_mistaken_for_software(monkeypatch):
|
||||
assert not is_software_renderer(renderer)
|
||||
|
||||
|
||||
def test_software_first_draw_is_resampled_to_hardware(monkeypatch):
|
||||
"""A presented llvmpipe / WARP / SwiftShader is the first thing every
|
||||
consumer-hardware check flags (measured 2026-09-14 with sundial), so a
|
||||
software first draw is retried until a hardware renderer that fits the
|
||||
screen comes up, and only kept when the pool offers nothing else."""
|
||||
draws = iter([{"webGl:renderer": _LLVMPIPE}, {"webGl:renderer": _INTEL}])
|
||||
monkeypatch.setattr(fingerprints, "sample_webgl", lambda *a, **kw: next(draws))
|
||||
|
||||
assert sample_webgl_for_screen("lin", 1024, 600)["webGl:renderer"] == _INTEL
|
||||
|
||||
only_software = iter([{"webGl:renderer": _LLVMPIPE}] * 40)
|
||||
monkeypatch.setattr(fingerprints, "sample_webgl", lambda *a, **kw: next(only_software))
|
||||
assert sample_webgl_for_screen("lin", 1920, 1080)["webGl:renderer"] == _LLVMPIPE
|
||||
|
||||
|
||||
def test_software_draws_are_skipped_when_resampling(monkeypatch):
|
||||
# A hardware first draw settles the class as hardware, so a software
|
||||
# candidate mid-loop is skipped instead of accepted -- otherwise the
|
||||
# rejection loop still leaks probability mass onto the rasterizers.
|
||||
draws = iter(
|
||||
[
|
||||
{"webGl:renderer": _NV_ANGLE}, # implausible at 1024x600
|
||||
{"webGl:renderer": _LLVMPIPE}, # plausible, but wrong class
|
||||
{"webGl:renderer": _INTEL}, # the coherent hardware answer
|
||||
]
|
||||
)
|
||||
monkeypatch.setattr(fingerprints, "sample_webgl", lambda *a, **kw: next(draws))
|
||||
|
||||
assert sample_webgl_for_screen("lin", 1024, 600)["webGl:renderer"] == _INTEL
|
||||
|
||||
|
||||
def test_falls_back_to_the_first_draw_when_nothing_is_coherent(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
fingerprints, "sample_webgl", lambda *a, **kw: {"webGl:renderer": _NV_ANGLE}
|
||||
)
|
||||
fp = sample_webgl_for_screen("win", 800, 600, attempts=4)
|
||||
assert fp["webGl:renderer"] == _NV_ANGLE
|
||||
|
||||
|
||||
def test_a_plausible_first_draw_costs_one_query(monkeypatch):
|
||||
# sample_webgl opens a fresh sqlite connection per call, and the common
|
||||
# case (any screen at or above the floor) must not pay for 32 of them.
|
||||
calls = []
|
||||
|
||||
def _counted(*args, **kwargs):
|
||||
calls.append(args)
|
||||
return {"webGl:renderer": _NV_ANGLE}
|
||||
|
||||
monkeypatch.setattr(fingerprints, "sample_webgl", _counted)
|
||||
sample_webgl_for_screen("win", 1920, 1080)
|
||||
assert len(calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("target_os", ["win", "mac", "lin"])
|
||||
def test_sampled_gpu_is_coherent_with_the_screen(target_os):
|
||||
# Against the real webgl_data.db pool.
|
||||
for _ in range(25):
|
||||
fp = sample_webgl_for_screen(target_os, 1280, 800)
|
||||
for seed in range(25):
|
||||
fp = sample_webgl_for_screen(target_os, 1280, 800, seed=seed)
|
||||
assert gpu_screen_is_plausible(fp.get("webGl:renderer"), 1280, 800)
|
||||
|
||||
|
||||
@@ -257,8 +202,8 @@ def test_screen_floor_is_a_no_op_without_screen_values():
|
||||
@pytest.mark.parametrize("target_os", ["windows", "macos", "linux"])
|
||||
def test_context_fingerprints_get_the_same_treatment(target_os):
|
||||
"""generate_context_fingerprint() is the per-context API #729 names, and
|
||||
build-tester drives the browser through it. It sampled the GPU with a bare
|
||||
sample_webgl() and never applied the floor, so the coherence fix reached
|
||||
build-tester drives the browser through it. It drew the GPU without the
|
||||
screen and never applied the floor, so the coherence fix reached
|
||||
launch_options() only."""
|
||||
from camoufox.fingerprints import generate_context_fingerprint
|
||||
|
||||
|
||||
Reference in New Issue
Block a user