The difference between C++ and JavaScript fingerprint spoofing is where the new value lives. A JavaScript spoof replaces what one page realm sees, so every other realm, every worker and every request header still has the real machine. A value changed in C++, inside the browser, is the only value there is. We shipped the JavaScript kind first, on stock Chrome, and every way it failed is below.

How JavaScript spoofing works, and the first thing it gets wrong

The usual method is a script that runs before the page and overwrites getters: navigator.hardwareConcurrency, the WebGL renderer string, the canvas readback, getClientRects. Inside that page it works. Our first version also rewrote Function.prototype.toString, so that asking one of our functions for its source printed [native code] like the real thing. It answered from a WeakMap of our own functions.

That was airtight in the realm it was installed in, and worthless one realm over. A page gets a second realm for the price of an iframe: create one, take the iframe's own Function.prototype.toString, and call it on our userAgent getter. The iframe has its own map, our getter is not in it, and the fallback prints the getter's real source. We measured it on one profile: thirty-three patched functions across Navigator, Screen, canvas, WebGL, audio and client rects handed their bodies to a four-line probe.

We fixed that by making every patch a Proxy over the real native, because V8 renders a proxy as native code in every realm without anything being shared.

The fix that broke a real page for months

Function.prototype.toString is the one function that must not be a proxy over the function it replaces. Installed that way, it recursed until the stack ran out on deviceandbrowserinfo.com/are_you_a_bot: four Maximum call stack size exceeded rejections, the site's detection script dead, and its verdict box empty. Our own detector catalogue had been filing that row as "unread, raise the settle time". The site had rendered nothing to wait for.

We found it by running the same page in an untouched Chrome, which answered in five seconds, and then bisecting one variable per run. The cause was the identity of the proxy's target, and the fix was a target in the exact shape of the real method. All of that bought one function that looked like the original.

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

A spoof has to return the platform's own object

The next class of fault was objects, not functions. Four APIs in our script handed back plain object literals where Chrome returns a branded object:

  • getClientRects returned an Array, so Object.prototype.toString said [object Array] where every browser says [object DOMRectList].
  • getBattery resolved with an object carrying eleven own keys. A real BatteryManager has none; its values sit on the prototype.
  • enumerateDevices built its own entries instead of MediaDeviceInfo objects.
  • navigator.plugins rebuilt Plugin objects with no MimeType entries, so plugin[0] read undefined.

Each one is three tells at once: the wrong type tag, a failed instanceof, and own properties where the real class has none. A literal is quick to write and fails the first brand check it meets.

Workers never ran the script

The structural problem is that injection reaches documents, and a page can ask questions from places a document script does not run. A dedicated worker, a service worker, or a request header built by the network stack each answers from the real machine. navigator.deviceMemory and the Sec-CH-Device-Memory header are one number with two routes, and only one of them goes through JavaScript.

Measured on an engine-level Chromium build we tested before our own, version 148, one launch, page against a dedicated worker, on a sixteen-core Apple Silicon Mac:

page    platform Win32  ua Chrome/148 (Windows NT 10.0)  cores 8
worker  platform Win32  ua Chrome/148 (Windows NT 10.0)  cores 8

Nothing disagreed, because both realms asked the same C++. (Claiming Windows on a Mac was the test case here. It still matches less well than claiming macOS, because the installed fonts come from the real machine and no switch reaches them.)

A Mosaic profile's fingerprint tab: GPU, screen, fonts, timezone and language drawn for its exit
The values a profile claims, handed to the engine at launch

C++ vs JavaScript fingerprint spoofing: what moves, and what does not

Tesseract Chrome, our engine, is stock Chromium source with a patch series applied. Canvas noise is added in the one function every readback goes through. The device memory claim sits in the shared function that both the accessor and the client-hint header call. The GPU strings, screen geometry, timezone and audio latency each have a patch in the file that produces them. There is no replaced function for a page to stringify and no literal for it to brand-check, because the browser's own objects carry the claimed values.

It also changed a rule. When the engine moves a value, a JavaScript spoof on top of it stops being a second layer of disguise and becomes a second layer of noise over values that were already noised. Two noises do not cancel. So on Mosaic's own engine nothing is spoofed with injected JavaScript. The launcher stopped sending those scripts to any engine that moves the values itself.

C++ does not make a contradiction impossible. It moves the work to finding every route to a value, and a patch can miss one: our screen patch once changed screen.width while the device-width media query still read the real panel. The difference is that the fix is one more patch in the browser, not one more wrapper a page can find.

The reasons we built the engine at all, including what plain Chromium gets wrong compared with Chrome, are in why we built our own Chromium. The values themselves come from real hardware records, drawn together so they agree before the engine ever reads them, and what an anti-detect browser is covers the category these choices sit in.