A canvas blocker extension changes toDataURL, toBlob and getImageData by replacing them in the page's JavaScript, so a site gets a blank image, an error, or pixels with noise in them. That hides your real canvas hash. It also leaves a replaced browser function where a native one should be, and a page can find that in four lines. You swap a common signal for a rare one.

What a canvas blocker extension does to the page

An extension cannot reach the code that draws a canvas. That code is C++ inside the browser. What it can reach is the page's JavaScript world, so it runs a script before the site's own and replaces the methods that read pixels back. Extensions such as CanvasBlocker offer a choice of modes: refuse the read, return a fake, or add a little random noise to the real pixels.

Each mode changes the value a site reads. None of them changes the fact that the method is no longer the one Chrome shipped, and fingerprinting scripts have long since stopped asking only for the value.

The iframe probe we ran against our own payload

We know how well this kind of layer hides because we wrote one. A JavaScript payload of ours replaced getters the same way an extension does, and it rewrote Function.prototype.toString so each replaced function would print [native code] when asked for its source. Inside the page it worked: our patched toString looked each function up in a private table and answered with the native string.

A page gets a second JavaScript world for free, though. Any iframe has its own copy of every built-in, including its own toString, and our table was not in it:

const f = document.createElement('iframe')
document.documentElement.appendChild(f)
f.contentWindow.Function.prototype.toString.call(
  Object.getOwnPropertyDescriptor(Navigator.prototype, 'userAgent').get)

The iframe's toString had never heard of our function, so it printed the real thing: our getter's JavaScript source. Measured on one profile, 33 patched functions across Navigator, Screen, canvas, WebGL, audio and client rects gave up their bodies to that probe. Canvas was not special. Every method the layer touched could be read the same way.

Every profile is drawn from one real computer, not from separate listsIn Mosaic: Machines that exist. GPU, screen, CPU and fonts from one real device. Free holds 3 profiles. Pro is $49 a month for any number.

How it works: Machines that exist

Why fixing the toString leak is not the end

We fixed that one. Each patch became a JavaScript Proxy wrapped around the real native function, which V8 prints as native code in every frame. That bought us agreement between the page and the iframe, and it found the next edge on its own.

Function.prototype.toString itself could not be wrapped that way. Installed as a proxy over the function it replaced, it recursed until the stack ran out on deviceandbrowserinfo.com's bot check: four Maximum call stack size exceeded errors, their detection script dead, and no verdict on the page. We had logged that row as "unread" for months, assuming the site was slow. One run in a stock Chrome with only our script added reproduced it, and the untouched control rendered its verdict in seconds.

That is the work an extension author is signing up for: every method, in every frame, in every worker, with every error thrown the way the native one throws it. CreepJS lists each place a replacement behaves unlike the native as a "lie", and what a CreepJS lie is walks through the checks it runs.

Noise has a second tell: read it twice

Suppose the extension's replacement were perfect. The noise itself can still be caught.

A real browser that draws the same shape twice gives the same pixels twice. A blocker that adds fresh random noise to every read gives two different hashes for one drawing on one page, and a script needs only to read twice and compare. Noise tied to each pixel's position fails a different test: draw one shape at two offsets, and the difference between the copies should be zero bytes. Ours once gave 76. The four-line offset probe has the code and the numbers.

Blocking outright is louder still. A canvas that comes back blank, or a read that throws, is something almost no ordinary browser produces, so it marks the browser as unusual on the first call.

What the real canvas hash was telling the site

The hash a blocker hides is less personal than it sounds. A canvas readback depends on the GPU, the driver, the fonts and how the operating system rasterises text, so it names a kind of machine that many people share. What a canvas hash reveals covers which parts of the stack move it.

So the trade is a hash shared by everyone with your kind of laptop, against a modified browser function that very few visitors carry. The second is easier to pick out of a crowd than the first. You can see what your own browser gives away on our canvas fingerprint page, and run it once with the extension and once without.

Where we put the canvas change instead

On Mosaic's own engine, Tesseract Chrome, nothing is spoofed with injected JavaScript. Canvas output is changed in C++, where the pixels are stored, so toDataURL and toBlob remain Chrome's own native methods and the page, its iframes and its workers read the same result. The probe above has nothing to catch, because no function was replaced.

That is not a promise that no site can tell the profile apart. It means the canvas value belongs to a machine that exists, drawn together with its GPU and fonts, which is what real fingerprints describes. In an ordinary Chrome, the choice a blocker offers is a common hash or a rare modification, and the probe above is how a site tells which one you picked.