Your Browser Does Math Differently on Every OS, and Anti-Bot Systems Read the Bits
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.
**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.