# scrapfly-browser-math-os-fingerprint-2026-07-12

## Veille

Engineering article published on **July 12, 2026** by **Scrapfly Engineering**, on a little-known browser *fingerprinting* channel: **the last bits of a floating-point number betray the operating system**. **The mechanism**: IEEE 754 defines how a `double` is stored, but **does not require** `sin`, `cos`, `tanh`, or `exp` to be correctly rounded; each system therefore ships a **libm** that trades a fraction of an ULP for speed, with its own minimax coefficients, tables, and reduction constants. As a result, `Math.tanh(0.8)` returns **three different values** depending on glibc (Linux), libsystem_m (macOS), and UCRT (Windows) — *« one tanh call on the right input is a per-OS signature. Claim macOS, return Linux math bits, and you have contradicted your own User-Agent. »* **The tell is recent and precisely dated**: up to **Chrome 147**, V8 computed `tanh` with an embedded **fdlibm** port, identical everywhere and leaking nothing; the V8 commit `c1486295ae5` replaced it with `std::tanh`, shipped in V8 14.8.57, i.e. **Chrome 148** — 148, 149, and 150 leak, 147 and earlier do not. **Three surfaces concentrate the leaks**: `Math.tanh` (the **only** `Math.*` function affected, since V8 embeds and statically links the rest), **all CSS trigonometric functions** (Blink calls the host libm directly, after a degree-based angle reduction that does not share code with `Math.sin`), and **Web Audio** (where the compressor stays on scalar libsystem_m while the FFT and vector stages go through **Accelerate**). **Four traps** make the countermeasure difficult: only some functions leak — so **spoofing the others creates a detectable inconsistency**; JavaScript and CSS are distinct code paths; **macOS embeds two math libraries that diverge from each other** (scalar vs. Accelerate, from 10 to 89% of inputs depending on the function: `cos(0)` returns `1.0` on one side, `0.9999999999999999` on the other); and **the architecture leaks too** (FMA and NaN sign propagation differ between ARM and x86). **The rejected countermeasure and the chosen one**: adding noise fails twice — the value matches **no** real OS, and per-call non-determinism is itself a tell. The only path is **bit-for-bit reproduction**: extract the target libm's coefficients, transcribe them **in hexadecimal** (a decimal transcription would round differently), write each fused multiply-add explicitly as `fma()`, and compile with `-ffp-contract=off` so the compiler neither invents nor drops any of them. **Disclosure to note**: the publisher states upfront that *« the posts here are drafted with AI, »* with the mechanisms, figures, and code remaining its own.

## Titre Article

Your Browser Does Math Differently on Every OS, and Anti-Bot Systems Read the Bits

## Date

2026-07-12

## URL

https://scrapfly.dev/posts/browser-math-os-fingerprint/

## Keywords

fingerprinting, browser fingerprint, anti-bot, automation detection, IEEE 754, correct rounding, ULP, unit in the last place, libm, glibc, libsystem_m, UCRT, ucrtbase, fdlibm, llvm-libc, dbl-64, V8, Blink, Chrome 148, V8 commit, Math.tanh, CSS trigonometric functions, degree-based angle reduction, Web Audio, DynamicsCompressor, Accelerate, vDSP, vvsin, vvtanh, FFT, biquad, Apple Silicon, ARM vs x86, FMA, fused multiply-add, NaN propagation, WASM, minimax coefficients, exponent tables, reduction constants, bit-for-bit reproduction, hexadecimal transcription, ffp-contract, determinism, noise as a countermeasure, spoofing inconsistency, contradictory User-Agent, detection surface, browser agents, Scrapfly

## Authors

**Scrapfly Engineering** — équipe d'ingénierie de **Scrapfly**, fournisseur d'infrastructure de collecte web. Le texte annonce sa position d'intérêt sans détour : *« Scrapfly ships a browser that has to match a real one across hundreds of signals, and math is one of the harder ones. »* On lit donc un **attaquant du problème de détection**, qui documente le canal parce qu'il doit le neutraliser.

**Divulgation éditoriale explicite**, à consigner parce qu'elle est rare : *« A note on how these are written: the posts here are drafted with AI. That lets our engineers put research and findings out quickly and spend their effort on the technical substance instead of the prose. The mechanisms, the numbers, and the code are ours. »* La rédaction est assistée, les mesures et le code sont revendiqués par l'équipe.

## Ton

**Profile**: extremely dense engineering article, in the *engineering blog* register of an infrastructure vendor. Twelve minutes announced, no pedagogical concessions: it opens with a console snippet and closes with C code and hexadecimal coefficients.

**Style**: **the demonstration precedes the explanation**. The text opens with three lines to paste into a console — `Math.tanh(0.8)` and its three outputs annotated by OS — before explaining why. Then a table of real measurements across three physical machines, with the precision that separates an anecdote from a protocol: *« Measured over the DevTools protocol on Chrome 150: Linux (glibc), macOS 26 on Apple Silicon (libsystem_m), Windows 11 (ucrtbase.dll). »*

**Two methodological traits**:

1. **The test input is chosen, not found.** *« tanh(0.5) is one of the roughly three-in-four inputs where everyone agrees, which is exactly why it makes a useless probe. tanh(0.8) is one that separates all three at once. »* The text explains **how a probe is constructed**, not merely that it exists.
2. **Ambiguity is resolved experimentally, not by reading code.** Facing two competing Apple libraries: *« We resolved it by driving real Chrome on a real Mac over the debugging protocol and reading the exact double. »*

**Argumentative register**: that of **orderly refutation**. The "Four traps" section dismantles the obvious solutions one by one — *« "Just reimplement the Mac functions" breaks on contact, for four reasons »* — and the following section rules out the most intuitive countermeasure (noise) before proposing its own.

**Marker phrases**: *« There is a quieter signal, and it lives in the last bits of a number »*, *« a detector needs no math, only a table »*, *« Claim macOS, return Linux math bits, and you have contradicted your own User-Agent »*, *« Pick the wrong library for a given call site and you land 1 ULP off on most inputs, worse than not spoofing »*, *« CSS is a tell everywhere »*.

## Pense-betes

- **The principle, in one line**: IEEE 754 mandates the **storage** of a `double`, not the **correct rounding** of transcendental functions. Correct rounding is expensive, so each system ships its own libm with its minimax coefficients, tables, and reduction constants. **The rounding difference is a signature.**
- **The probe, worth knowing**: `Math.tanh(0.8)` returns `0.6640367702678491` on Linux (glibc), `0.664036770267849` on macOS (libsystem_m), and `0.6640367702678489` on Windows (UCRT) — **all three differ, by 2 ULP**. Linux and macOS diverge on roughly **a quarter of all inputs**, typically by **1 ULP**; Windows diverges from both on a few percent. → *« A detector needs no math, only a table. »*
- **The tell is dated down to the commit, and that is what makes the article useful**: up to **Chrome 147**, V8 computed `tanh` with an embedded **fdlibm** port — same bits everywhere, **no leak**. V8 commit **`c1486295ae5`** replaced it with `std::tanh`, which reads the host libm; shipped in **V8 14.8.57 = Chrome 148**. **148, 149, 150 leak. 147 and earlier do not.** → **A privacy regression introduced by a mundane implementation choice**, and a precise version window to date a fingerprint.
- **The leak map — the most reusable part**: | Operation | `Math.*` (JS) | CSS `calc()` | Web Audio | |---|---|---|---| | `sin cos tan` | V8 embedded | **host libm** | Accelerate (FFT), scalar in the compressor | | `asin acos atan atan2` | V8 embedded | **host libm** | not used | | `tanh` | **host libm** | — | not used | | `exp`, `log*`, `pow` | V8 embedded | **host libm** | scalar in the compressor | | vector, FFT | — | — | **Accelerate (vDSP)** on Mac | | `sqrt`, arithmetic | hardware | hardware | hardware | → **V8 routes almost everything through its own embedded math: JavaScript leaks at only one point, `Math.tanh`. CSS leaks everywhere. Web Audio touches three libraries within a single graph.**
- **WASM does not leak the OS**: no transcendental opcode, `sin` comes from whichever libm the module embedded, and arithmetic (`f64.sqrt`, `f64.mul`) is hardware-based. **Its only fingerprinting axis is ARM vs. x86** (NaN canonicalization, a few SIMD rounding differences).
- **The four traps — the real design lesson, transposable well beyond this topic**: 1. **Only some functions leak.** Spoofing the functions that **don't** leak creates an inconsistency, and *« that asymmetry is itself checkable. »* → **Overcorrecting is as detectable as undercorrecting.** 2. **JS and CSS are distinct code paths.** CSS trigonometric functions reduce the angle **in degrees** then call `std::sin` on the reduced value — a different result from a `sin()` in radians. The team reproduced *« the degree reduction and the radians-to-degrees step bit-for-bit, not just the leaf function. »* 3. **macOS has two math libraries that contradict each other.** Scalar `libsystem_m` and Accelerate's vector routines diverge on **10 to 89%** of inputs depending on the function. `cos(0)`: `1.0` in scalar, `0.9999999999999999` in Accelerate. → *« "Reproduce Apple's math" is undefined until you know which library the browser calls, at which site. »* Answer established experimentally: scalar for `Math.tanh`, CSS trig, and the compressor's per-sample transcendentals; Accelerate for Web Audio DSP (FFT, vector math, biquad filters). **Picking the wrong library costs 1 ULP on most inputs — worse than doing nothing.** 4. **The architecture leaks.** ARM and x86 differ on fused multiply-add and on NaN sign propagation; a reproduction that is correct on paper drifts if the compiler fuses on one side and not the other.
- **Why noise does not work — an argument generalizable to any fingerprinting countermeasure**: *« Perturbing the output fails twice. A reference comparison sees a value that matches no real OS, and per-call randomness breaks determinism, which is its own tell. »* → **A defense that produces an impossible value is just as identifying as a true one.** The target is not "noise," it's **the exact value of the claimed system**.
- **The chosen countermeasure, and its three requirements**: (1) **retrieve** minimax coefficients, exponent tables, and reduction constants from the target libm and **transcribe them into portable C**; (2) **copy the bit patterns in hexadecimal** — *« a decimal transcription would round differently »*; (3) **write each fusion as explicit `fma()`** and compile with **`-ffp-contract=off`**, so that the fused operations are exactly the ones Apple fuses and the result is identical on CPUs with and without FMA, and **between the emulated ARM machine and the x86 fleet doing the execution**. When reproduction isn't worth the effort: *« lift the original »* — since Windows UCRT is x86-64 and position-independent code.
- **What the article implies for browsing agents** — not stated in the text, but direct: an agent driving a browser to read the web crosses exactly these checks. Harnesses that **escalate to the user's real browser** rather than imitating one (cf. the browser path of [[skill-gibbs-hyperresearch-2026-08-03]], which drives the authenticated real Chrome and sets as a hard boundary that *« CAPTCHAs, 2FA, and logins are never solved automatically »*) sidestep the problem by construction: **there is nothing to falsify when the browser genuinely is what it claims to be.** Two opposing strategies facing the same wall.
- **Usage framing**: the article documents a **detection-evasion** technique, written by a web-scraping vendor. It is usable defensively (understanding what one's anti-bot system actually reads, and knowing that an isolated `Math.tanh` signal dates the browser as much as it identifies the OS) as well as for evasion. **The text does not address the consent of scraped sites**, nor terms of service — that is outside its stated scope.
- **AI-assisted drafting, disclosed**: *« the posts here are drafted with AI… The mechanisms, the numbers, and the code are ours. »* An honest and rare disclosure, worth mentioning when citing the text as a primary source. The measurements come with their protocol (DevTools protocol, three named machines with their versions), which makes them independently checkable regardless of how the prose was produced.
- **Meta / related**: on browser-driving agents, [[skill-gibbs-hyperresearch-2026-08-03]] (browser path, escalation, CAPTCHA boundary); on attack surface and web content treated as hostile data, [[valente-zalewski-beyond-zero-enterprise-security-ai-era-2026-07-20]]; on AI browsers and automation, [[mody-browser-company-arc-dia-ai-native-2025-11-23]], perplexity-chrome-integration-browser-ai-search-2025-10-22, and mcp-replaces-browser-logrocket-2025-09-15.

## RésuméDe400mots

Article by **Scrapfly Engineering** (July 12, 2026) on a *fingerprinting* channel lodged **in the last bits of a number**.

**The mechanism.** IEEE 754 defines how a `double` is stored but **does not require** correct rounding of transcendental functions. Since correct rounding is expensive, each platform ships a **libm** with its own minimax coefficients, tables, and constants. As a result, `Math.tanh(0.8)` returns three distinct values depending on glibc, libsystem_m, and UCRT. Linux and macOS diverge on roughly a quarter of inputs, typically by **1 ULP**. *« A detector needs no math, only a table. »* And the inconsistency is immediately exploitable: claiming macOS while returning Linux bits **contradicts its own User-Agent**.

**The tell is recent and dated.** Up to **Chrome 147**, V8 computed `tanh` with an embedded fdlibm, identical everywhere. Commit `c1486295ae5` replaced it with `std::tanh`, which reads the host libm, shipped with **Chrome 148**.

**Three surfaces leak.** `Math.tanh` is the **only** `Math.*` function affected — V8 embeds and statically links everything else. All **seven CSS trigonometric functions** leak, with Blink calling the host libm after a degree-based angle reduction that does not share code with `Math.sin`. And **Web Audio** touches three libraries within a single graph: Accelerate for the FFT and vector stages, scalar libsystem_m for the compressor's transcendentals. WASM, meanwhile, does not leak the OS — only the architecture.

**Four traps** make the countermeasure difficult: only some functions leak, so **spoofing the others creates a detectable asymmetry**; JavaScript and CSS are separate code paths; **macOS embeds two math libraries that diverge from each other** by 10 to 89% depending on the function, so "reproducing Apple's math" makes no sense until one knows which is called at which site; and ARM and x86 differ on fused multiply-add and NaN propagation.

**Noise does not work**: it produces a value that matches **no** real OS, and its non-determinism is itself a signal. The only path is **bit-for-bit reproduction** — coefficients extracted from the target libm and transcribed in hexadecimal, each fusion written as explicit `fma()`, compiled with `-ffp-contract=off`.

The publisher states that its posts are **drafted with AI assistance**, with the mechanisms, figures, and code remaining its own.

## GrapheDeConnaissance

- Scrapfly —publie→ Your Browser Does Math Differently on Every OS (DOCUMENT, 0.97)
- IEEE 754 —s_applique_à→ le stockage d'un double, sans exiger l'arrondi correct des fonctions transcendantes (AFFIRMATION, 0.96)
- libm de plateforme —permet→ d'identifier le système d'exploitation d'un navigateur par la différence d'arrondi d'une fonction transcendante (AFFIRMATION, 0.96)
- Math.tanh —mesure→ trois valeurs distinctes sur glibc, libsystem_m et UCRT pour l'entrée 0.8 (MESURE, 0.95)
- Chrome 148 —remplace→ le calcul embarqué de tanh par un appel à la libm hôte, introduisant la fuite (AFFIRMATION, 0.94)
- fonctions trigonométriques CSS —s_oppose_à→ les fonctions Math de JavaScript, dont elles ne partagent pas le chemin de code (AFFIRMATION, 0.93)
- Web Audio —utilise→ trois bibliothèques mathématiques distinctes dans un même graphe audio sur macOS (AFFIRMATION, 0.9)
- Accelerate —s_oppose_à→ libsystem_m scalaire, les deux divergeant sur 10 à 89 % des entrées selon la fonction (MESURE, 0.92)
- spoofing partiel d'une empreinte —s_oppose_à→ la cohérence de l'empreinte, l'asymétrie créée étant elle-même vérifiable (AFFIRMATION, 0.94)
- ajout de bruit —s_oppose_à→ la dissimulation d'une empreinte, en produisant une valeur ne correspondant à aucun système réel (AFFIRMATION, 0.95)
- Scrapfly —recommande→ de reproduire bit à bit l'algorithme de la libm cible plutôt que de perturber sa sortie (AFFIRMATION, 0.94)
- transcription hexadécimale des coefficients —résout→ l'erreur d'arrondi qu'introduirait une transcription décimale (AFFIRMATION, 0.93)
- option de compilation ffp-contract=off —permet→ de rendre déterministes les fusions multiplication-addition entre architectures (AFFIRMATION, 0.92)
- WebAssembly —réduit→ la surface d'empreinte, n'exposant que la distinction ARM contre x86 (AFFIRMATION, 0.9)
- Scrapfly —affirme_que→ ses billets sont rédigés avec l'aide de l'IA, les mécanismes, chiffres et code restant les siens (CITATION, 0.95)

---
Canonical: https://www.thekb.eu/en/fiches/scrapfly-browser-math-os-fingerprint-2026-07-12/
