Font fingerprint randomization comes in two kinds. You can change which fonts a page finds, by hiding or swapping families, or you can leave the font list alone and change how wide the text measures. We shipped both in our engine at first. Measured, the first kind was what a detector called masking, and the second was not. So we removed the font hiding and kept per-profile glyph advances, which is how Tesseract Chrome handles fonts today.

How a page fingerprints fonts

There is no API that lists installed fonts, so a page measures. It draws a test string in a fallback font such as monospace and records the width, then draws the same string as "Some Font", monospace. If the width changes, Some Font is installed. Run that over a few hundred family names and the page has a font list, built entirely from measurements.

The same widths are a fingerprint of their own. How wide a string comes out depends on the font files, the version of the operating system that drew them and the way it rasterises text. Two browsers on one machine produce identical numbers for every string, because they are using the same files on the same system. So on a computer running several profiles, text measurement is shared entropy: every profile answers the same question the same way.

That gives a randomizer two different things to change, and they behave very differently.

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

Hiding fonts: what we started with

The font patch our engine started from filtered font lookups in font_cache.cc. For a profile claiming the machine's own OS, it hid a seeded 2% or so of families, so each profile was missing a different handful. For a profile claiming another OS, it hid 95% of the families that platform would not have and substituted the host's fonts for the claimed platform's names.

Each profile got a different list, which sounds like the goal. What it actually produced was a Mac missing two families every Mac ships with. A macOS font list with random gaps is not a rare Mac. It is a Mac nobody owns, the same mistake as a fast laptop chip paired with the wrong screen, made two per cent at a time.

Pixelscan said so. On the first detector run of our finished engine it read one fresh profile as "Masking detected". We bisected that profile in place, one surface at a time, with the unchanged profile run first and last as a control, and only one surface flipped the verdict: fonts. Canvas, WebGL, audio and client-rect noise were all still on in the row that passed. The full bisect, with every row, is in Pixelscan masking, bisected.

The cross-OS branch went too, and it was already dead code: the engine made every profile claim the host's own OS at that point, so the substitution never ran. It would not have helped anyway. A Windows font name answered by a Mac's font still measures like the Mac's font, which is why a Mac makes the best Mac profiles.

Font fingerprint randomization by glyph advance

The second patch, 0115-font-metric-jitter, touches one function in simple_font_data.cc: the one that returns how far the pen moves after a glyph, its advance width. It adds a small amount to that width, and nothing else changes. The same fonts exist, the same fallback rules apply, and each font draws the same shapes.

The rules it follows are the ones that make it look like a font rather than a filter:

  • Advances only, never the x or y offsets. Offsets are how combining marks sit on their base letters, and moving them breaks Thai, Arabic and Devanagari text.
  • A glyph with zero advance stays at zero. Those are combining marks and joiners, and giving one a width pushes the mark off its letter.
  • The amount is a pure function of the profile's seed and the glyph id. A glyph measures the same every time within a profile, so measuring one string twice gives one answer. Noise that re-rolled per call would be caught by exactly that test, which is a stronger signal than any width.
  • At most a twentieth of a pixel per glyph. That is enough to move the width of a measured string, and small enough to stay inside what two machines drawing one font could plausibly differ by. More noise is not safer. Past a point it stops looking like a different machine and starts looking like a randomizer, which is the lesson canvas noise taught us first.

The seed comes from the profile, through --fingerprint-font-spacing-seed, so it holds across launches. A profile measures text the same way next week as today, and a second profile on the same Mac measures it slightly differently.

How we knew the jitter was innocent

Both font patches sat behind one --disable-spoofing=font token, so the first bisect turned them off together, and we nearly deleted both. The jitter has its own off switch by accident: a seed that is not a number fails to parse, and the function returns the real width. That let us hold the two apart. With the jitter off and the hiding on, Pixelscan still failed, twice. With both off it passed. With both on it failed. So the hiding was the signal, and the jitter stayed.

Because this happens in C++ inside Blink's own font code, there is no script on the page for a detector to find and no wrapped function whose source gives it away. A JavaScript wrapper around one measuring API would leave the others reporting the real value, and why that split is worse than no change at all is in C++ versus JavaScript spoofing.

The cost, stated

Two Mosaic profiles on one computer now report the same font list. Profiles drawn for this computer's own system already share its operating system and its GPU vendor, because those are true of the machine. A shared truth is ordinary; real offices are full of identical laptops. A private contradiction, a list that no real install of that system would have, is what a scanner is looking for. Where text measurement fits among the other values a page reads is on browser fingerprint.