# Detect canvas noise injection with one offset probe

> To detect canvas noise injection, draw one shape twice at two offsets and diff them. A real browser gives 0 bytes; our old noise gave 76.

Source: https://mosaicbrowser.com/blog/canvas-offset-probe

## The four-line probe that catches position-keyed canvas noise

To detect canvas noise injection, draw one shape twice at two offsets and diff them. A real browser gives 0 bytes; our old noise gave 76.

![76: Same drawing, two places, 76 bytes apart, beside a screenshot: a Mosaic leak test: every surface a site reads, checked from inside the profile page](https://mosaicbrowser.com/covers/canvas-offset-probe.webp)

To detect canvas noise injection, draw the same thing twice on one canvas at two different offsets and compare the two regions byte by byte. A real browser renders identical input to identical pixels, so the difference is 0. Noise keyed on a pixel's position cannot pass, because the two copies sit at different coordinates and get different stamps. When we ran it against our own engine's first canvas noise, the two copies differed by 76 bytes.

## The probe

It is short enough to paste into a console:

```
const c = Object.assign(document.createElement('canvas'), { width: 200, height: 120 }), g = c.getContext('2d')
for (const y of [0, 60]) { g.fillStyle = '#f60'; g.fillRect(10, y + 5, 80, 30); g.fillStyle = '#069'; g.font = '16px sans-serif'; g.fillText('Cwm fjord 1234', 12, y + 28) }
const a = g.getImageData(0, 0, 200, 60).data, b = g.getImageData(0, 60, 200, 60).data
let diff = 0; for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) diff++

```

The two halves of the canvas received the same instructions, shifted down by 60 whole pixels. On an untouched browser `diff` is 0. Anything else means something changed the pixels after they were drawn, and changed them differently depending on where they landed.

## Why stable noise still fails it

The usual tests for injected canvas noise are known pixels (draw a solid colour, read it back, check nothing moved) and repeat reads (render twice, compare the hashes). Noise that changes on every read fails the second test at once, so better noise is made deterministic, and keying it on the pixel's coordinates is the obvious way to do that.

Our engine's first version did exactly that, and it passed both of the usual tests. Measured on one profile, one canvas, the same text and rectangle drawn at y=0 and y=60:

```
identical regions differ    76 bytes      (a real browser: 0)
same region read twice      0 bytes
toDataURL stable            true

```

Every read agreed with every other read. The canvas still contradicted itself, because a real browser draws the same thing the same way wherever it is.

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)

## What it looked like from outside

Pixelscan's fingerprint card was reading this, and that is why its verdict on our profiles looked random. It only fired when a stamped pixel happened to land inside a region it compared. Four fresh profiles came back consistent, inconsistent, consistent, inconsistent. One profile, bisected field by field, passed twice with canvas noise off and failed six times with it on, while audio, client rects and every other surface made no difference.

## Keying the noise on colour

So the noise now keys on what was drawn instead of where. Every fully opaque pixel of a given colour gets the same low bit, chosen once per profile from the profile's seed. The key reads bits 1 to 7 of each channel and the stamp writes only bit 0, so reading a stamped pixel back computes the same key and writes the same value. Identical content is then identical at any offset, through `getImageData`, `toDataURL`, a sub-rectangle or a second canvas, and still different from what the bare machine would produce.

The first colour-keyed version touched every opaque pixel, and it failed Pixelscan five times out of five, worse than the noise it replaced. Changing every pixel does not read as two GPUs rounding differently; it reads as a filter over the image. Only about one distinct colour in 32 is touched now. Measured after that change, eight fresh profiles: 8 of 8 passed, with 8 distinct canvas hashes.

Three older rules made it safe to write. We never write to the page's own canvas, only to a copy we encode from. We stamp a bit rather than flip it, because XOR applied twice cancels, and a page that decodes its own data URL onto a second canvas would apply it twice. And we stamp fully opaque pixels only, since a transparent pixel's colour does not survive PNG encoding.

## Bromite had the same bug, and we nearly shipped it

Tesseract Chrome, our engine, carries some patches from other Chromium projects. Bromite's canvas function, `ShuffleSubchannelColorData`, scans for edge pixels and stamps them keyed on where they are. That is the same algorithm the probe catches, and it fails the probe the same way. Carrying it as it was would have put a known Pixelscan failure into a browser built to pass Pixelscan.

Our patch `0125` replaces that function's body, which all six of its call sites go through, with the colour-keyed stamp described above. The lesson we wrote down is about vendored code in general: a patch from another project tells you what that project chose, not what is correct. Read what it does before you carry it.

## How to detect canvas noise injection in your own browser

Paste the probe into the console of any profile and read `diff`. Zero is what a real browser says. A non-zero result with stable repeat reads is position-keyed noise; a result that changes between runs is per-read noise, which repeat reads catch on their own. The hash a site would compute from your canvas is on the [canvas fingerprint](https://mosaicbrowser.com/tools/canvas-fingerprint) page, and the [fingerprint checker](https://mosaicbrowser.com/tools/fingerprint-checker) shows the rest of what a page reads.

How engine-level noise differs from the extension kind is covered in [what an anti-detect browser is](https://mosaicbrowser.com/guides/what-is-an-anti-detect-browser), and [our launch post](https://mosaicbrowser.com/blog/introducing-mosaic) has why Mosaic moves these values in C++ at all.

## Questions

### How do sites detect canvas noise injection?

Three ways that we know of: draw known pixels and check they come back unchanged, read the same canvas twice and compare, and inspect the canvas functions for signs they were replaced. The offset probe is a fourth: draw the same thing at two places on one canvas and compare the two regions.

### Why does a real browser give identical pixels at two offsets?

The same text and shapes, drawn at whole-pixel offsets on one canvas, go through the same rasteriser with the same input. A real browser produced 0 differing bytes in our measurement, every time.

### Does canvas noise that is stable across reads pass this probe?

Not if it is keyed on coordinates. Our old noise read back identically every time and toDataURL was stable, yet the two copies of one drawing differed by 76 bytes, because each copy sat at different coordinates.

### What does Mosaic's canvas noise key on now?

The pixel's own colour. Every fully opaque pixel of a given colour gets the same low bit, chosen once per profile from its seed, and only about one distinct colour in 32 is touched, so identical content stays identical wherever it is drawn.

## Related

[What is an anti-detect browser?](https://mosaicbrowser.com/guides/what-is-an-anti-detect-browser): An anti-detect browser gives each profile its own fingerprint, proxy and cookies, so sites see separate visitors. What it changes, how, and its limits.

[Introducing Mosaic, an anti-detect browser that lives on your machine](https://mosaicbrowser.com/blog/introducing-mosaic): Mosaic runs its own Chromium, draws each profile from a real machine and matches it to the proxy exit. Profiles stay on disk. One plan, no per-profile fees.



[Same fonts, different measurements: per-profile glyph advances](https://mosaicbrowser.com/blog/glyph-advances-per-profile): Font fingerprint randomization can hide fonts or change how wide glyphs measure. Hiding read as masking; per-profile glyph advances did not.

[Every profile on one Mac drew the same WebGL image](https://mosaicbrowser.com/blog/webgl-hash-same-every-profile): A WebGL fingerprint is the same in every browser profile on one machine because the GPU draws it. Ours was too, until per-profile noise moved the hash.

[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.

[The canvas method nobody noised: what a worker test found](https://mosaicbrowser.com/blog/the-canvas-method-nobody-noised): Our canvas noise reached toDataURL and missed toBlob, so a page calling toBlob got a PNG byte-identical to stock Chrome's. A worker test caught it.



## 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.
