Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/webgl/sample.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,13 @@ export async function sampleWebGL(
query = `SELECT vendor, renderer, data, ${os} FROM webgl_fingerprints WHERE vendor = ? AND renderer = ?`;
params = [vendor, renderer];
} else {
query = `SELECT vendor, renderer, data, ${os} FROM webgl_fingerprints WHERE ${os} > 0`;
// Software rasterizers must never win the weighted draw. WAFs flag
// them far more reliably than any GPU/screen mismatch (see
// daijro/camoufox#743, which fixed the same bias in the python
// generator, and #276's dataset analysis). An explicit
// vendor/renderer pair below still selects them on purpose -- this
// only stops the random sampler from handing them out.
query = `SELECT vendor, renderer, data, ${os} FROM webgl_fingerprints WHERE ${os} > 0 AND renderer NOT LIKE '%llvmpipe%' AND renderer NOT LIKE '%SwiftShader%' AND renderer NOT LIKE '%Basic Render%' AND renderer NOT LIKE '%Generic Renderer%'`;
}

return new Promise<WebGLData>((resolve, reject) => {
Expand Down
64 changes: 64 additions & 0 deletions test/webgl-software-renderers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { describe, expect, test } from "vitest";
import { getPossiblePairs, sampleWebGL } from "../src/webgl/sample";

// Software rasterizers are legitimate dataset rows (the db is built from real
// user submissions) but must never win the weighted draw: pages can read the
// renderer string and a GPU-less software renderer is one of the strongest
// "this is a bot" signals there is. daijro/camoufox#743 applied the same rule
// to the python generator; #276 documents the dataset contamination.

const SOFTWARE_RENDERER_PATTERNS = [
/llvmpipe/i,
/SwiftShader/i,
/Basic Render/i,
/Generic Renderer/i,
];

function isSoftwareRenderer(renderer: unknown): boolean {
return typeof renderer === "string" && SOFTWARE_RENDERER_PATTERNS.some((p) => p.test(renderer));
}

// The sampled blob is the camoufox config fragment: the renderer strings live
// under the spoofing keys, not top-level.
function fingerprintRenderer(fp: { renderer?: unknown; "webGl:renderer"?: unknown }): string {
return typeof fp["webGl:renderer"] === "string" ? fp["webGl:renderer"] : String(fp.renderer);
}

describe("sampleWebGL weighted draw", () => {
test("never samples a software rasterizer", async () => {
const seen = new Set<string>();
for (let i = 0; i < 500; i++) {
const fp = await sampleWebGL("lin");
const renderer = fingerprintRenderer(fp);
expect(isSoftwareRenderer(renderer)).toBe(false);
seen.add(renderer);
}
// The filtered draw still covers the real hardware devices, it has not
// collapsed onto a single row.
expect(seen.size).toBeGreaterThan(2);
}, 60_000);

for (const os of ["win", "mac", "lin"] as const) {
test(`no software renderer wins the draw on ${os}`, async () => {
for (let i = 0; i < 300; i++) {
const fp = await sampleWebGL(os);
expect(isSoftwareRenderer(fingerprintRenderer(fp))).toBe(false);
}
}, 60_000);
}

test("explicit pairs still resolve, including software rows", async () => {
const pairs = await getPossiblePairs();
const softwarePair = pairs.lin?.find((p) => isSoftwareRenderer(p.renderer));
// The shipped dataset does contain llvmpipe rows for linux.
expect(softwarePair).toBeDefined();
const fp = await sampleWebGL("lin", softwarePair!.vendor, softwarePair!.renderer);
expect(fingerprintRenderer(fp)).toBe(softwarePair!.renderer);
}, 30_000);

test("unknown explicit pairs still report the full (unfiltered) pair list", async () => {
const pairs = await getPossiblePairs();
expect(Array.isArray(pairs.lin)).toBe(true);
expect(pairs.lin!.length).toBeGreaterThanOrEqual(11);
Comment on lines +59 to +62

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exercise the unknown-pair error path

This test never supplies an unknown explicit pair to sampleWebGL; it only calls the independent getPossiblePairs() API. Consequently, it would remain green if the error path’s SELECT DISTINCT query were filtered or its reported pair list were otherwise broken, so the behavior named by the test is not actually protected. Invoke sampleWebGL with a nonexistent pair and assert that the rejected error contains a known software-renderer pair.

Useful? React with 👍 / 👎.

});
});