# Pixelscan WebGL fingerprint: sixty fields and one string

> Pixelscan's WebGL fingerprint is a render hash plus about sixty driver fields beside the renderer string. A chip that never existed failed like a real one.

Source: https://mosaicbrowser.com/blog/pixelscan-sixty-webgl-fields

## Pixelscan reads sixty WebGL fields, and the renderer string is one

Pixelscan's WebGL fingerprint is a render hash plus about sixty driver fields beside the renderer string. A chip that never existed failed like a real one.

![Sixty honest fields and one string, beside a screenshot: a profile fingerprint in Mosaic: GPU, screen, fonts, timezone and language matched to the exit](https://mosaicbrowser.com/covers/pixelscan-sixty-webgl-fields.webp)

Pixelscan's WebGL fingerprint has two parts: a hash of one rendered triangle, and a record of about sixty driver fields sitting beside the renderer string. We decoded its collector and then measured it. A chip that never existed failed exactly like a real one, so the check is whether the string agrees with the record, not whether the chip is on a list.

## What the Pixelscan WebGL fingerprint collects

Pixelscan's collector is `pixelscan.net/assets/fptc.min.js`. It is obfuscated only with `\xNN` escapes, so four lines of Python turn it back into readable code, and it names what it gathers:

`audio`, `canvasHash`, `fonts`, `hardwareConcurrency`, `language`, `navigatorPlatform`, `screenResolution`, `secCh`, `timezone`, `ua`, `webDriver`, `webglHash`, `webglMeta`

Two of those are WebGL. `webglHash` compiles a fixed vertex shader and a fixed fragment shader, draws one triangle from three hard-coded vertices, and MD5s the framebuffer. `webglMeta` is the claim: `unMaskedVendor` and `unMaskedRenderer`, and next to them about sixty capability fields. Every `MAX_*` limit is there, with the shader precision formats, the bit depths, anisotropy, the extension list and the WebGL2 function list.

One cross-check is visible in the script itself. On `Win32` or `Win64` it looks for `D3D11` or `D3D9` in the renderer and compares that against `MAX_VERTEX_UNIFORM_VECTORS`. Everything else is decided on Pixelscan's server, so we can say what it reads and not exactly how it weighs each field.

Mosaic is a desktop app

Email yourself the link and open it on your computer.

[How it works: Machines that exist](https://mosaicbrowser.com/features/real-fingerprints)

## The theory we wrote down, and why it was wrong

The obvious reading is that the hash is the ground truth and the string is the claim: the GPU draws the triangle, the pixels say which GPU it really is, and a chip you don't own is arithmetic. We wrote exactly that into our notes. Three measurements took it apart.

First, we noise the WebGL framebuffer per profile (the colour-keyed stamp that makes `readPixels` and `toDataURL` agree is in [readPixels and toDataURL read a canvas from opposite ends](https://mosaicbrowser.com/blog/webgl-readpixels-vs-todataurl)). With the noise on, the hash matches no real device at all. The host's own chip still passed and every other chip still failed. The hash is not what decides it.

Second, we claimed `Apple M9 Ultra`, which has never been made. It failed exactly as `Apple M2 Pro` did. If Pixelscan were looking chips up in a table of known devices, an unknown string would have had nothing to contradict.

Third, the masked pair is not leaking the real chip. `getParameter(RENDERER)` returns `WebKit WebGL` and `VENDOR` returns `WebKit`, byte for byte what stock Chrome returns. (The unmasked constants have their own trap: they answer `null` until the page asks for the debug extension, covered in [UNMASKED_RENDERER is null until a page asks for it](https://mosaicbrowser.com/blog/webgl-debug-renderer-info-fresh-context).)

What is left is the capability record. We diffed 56 of its fields against a stock Google Chrome on the same Mac, and they came back identical. That is correct behaviour, and it is also why the claim fails: Pixelscan reads sixty honest driver fields and one string that disagrees with them.

## The measurements, one profile patched in place

Every row below comes from one profile on an M4 Max host, patched in place over several sweeps, with only the renderer string moving and the host's own chip run as the control in each sweep. This was a stock-browser build, where a claimed renderer sits on top of the record the real driver wrote.

| Claimed renderer | Pixelscan |
| --- | --- |
| Apple M4 Max (the host's chip) | pass, every round |
| Apple M4 Pro | fail |
| Apple M4 | fail |
| Apple M3 Max | fail |
| Apple M2 Pro | fail |
| Apple M1 | fail |
| Apple M9 Ultra (never existed) | fail |

It is the exact chip, not the family and not the generation. An M4 Pro on an M4 Max is as wrong as an M1. The thread count is a separate field and was free: M4 Max at 12 threads passed beside M4 Max at 16, and M2 Pro failed at both.

The cost was one headline. The same profile claiming M2 Pro still scored Trustworthy on IPHey and 90% on BrowserScan, the same as it did claiming its own chip. Only Pixelscan's verdict moved.

## What it takes to claim another GPU

The rule we already worked to says only change a surface if every route to it can be made to agree. For a GPU on a stock browser that means the whole record, not the string: the limits, the precision formats and the extension list of a real target device, kept coherent with each other. We don't have per-GPU capability data for that, and we will not write it from a hand-made table, because the record is fifty times more surface to get wrong than a renderer name.

Tesseract Chrome, our own Chromium, changes the renderer inside the engine, in C++, instead of laying a string over a driver's answer. The same sweep on it, one profile patched in place with the host's chip as the control at both ends of two runs, came back differently: M1, M2, M3, M2 Pro, M1 Max and the invented M9 Ultra all passed. Two freshly drawn profiles, an M3 Pro and an M2 on the M4 Max host, passed Pixelscan, IPHey and BrowserScan. Which values the engine draws together from real hardware is on [real fingerprints](https://mosaicbrowser.com/features/real-fingerprints), and the font half of Pixelscan's consistency check, which no renderer string touches, is in [Pixelscan's masking verdict, bisected to one surface](https://mosaicbrowser.com/blog/pixelscan-masking-bisected).

## Questions

### What does Pixelscan collect for WebGL?

Two things, by the names in its own collector script. webglHash is an MD5 of a fixed triangle drawn with a fixed shader pair. webglMeta is the unmasked vendor and renderer beside about sixty capability fields: MAX limits, precision formats, bit depths, anisotropy, the extension list and the WebGL2 function list.

### Does Pixelscan check the GPU against a list of real devices?

Our measurements say no. A renderer string for Apple M9 Ultra, a chip that has never existed, failed exactly the way Apple M2 Pro did on an M4 Max machine. A lookup would have had nothing to compare an unknown chip against.

### Is the WebGL render hash what decides Pixelscan's verdict?

Not in our runs. With the framebuffer noised, the hash matched no real device at all, and the host's own chip still passed while every other chip still failed.

### Why does changing the WebGL renderer string fail Pixelscan?

On a stock browser, the string moves and the capability record under it stays the real driver's. A 56-field diff against stock Chrome on the same Mac came back identical, so the record described one GPU and the string named another.

## Related

[Pixelscan's masking verdict, bisected to one surface](https://mosaicbrowser.com/blog/pixelscan-masking-bisected): We bisected Pixelscan's masking detected verdict on one profile. Canvas, WebGL, audio and client rects were fine. Hiding about 2% of font families caused it.

[readPixels and toDataURL read a canvas from opposite ends](https://mosaicbrowser.com/blog/webgl-readpixels-vs-todataurl): readPixels starts at the bottom row and toDataURL at the top, so position-keyed WebGL noise disagreed with itself. Keyed on colour, 4096 of 4096 pixels agree.

[UNMASKED_RENDERER is null until a page asks for it](https://mosaicbrowser.com/blog/webgl-debug-renderer-info-fresh-context): Chrome returns null for the unmasked WebGL renderer until the page calls getExtension, so a spoof that answers it earlier is a two-line tell.

[Machines that exist: fingerprints drawn from real hardware](https://mosaicbrowser.com/features/real-fingerprints): Mosaic draws each profile's GPU, screen, CPU, memory and fonts from one real device, so the parts of a fingerprint agree with each other and with the host.









## Mosaic, on your own computer

The browser this blog is about: its own Chromium, fingerprints from real machines and a proxy exit per profile. Free holds 3 profiles. Pro is $49 a month for any number.

Mosaic is a desktop app

Email yourself the link and open it on your computer.
