libm Rounding Fingerprints Your OS for Anti-Bot Detection
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 requiresin, 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.
By **Scrapfly Engineering** — équipe d'ingénierie de **Scrapfly**// Source scrapfly.dev ↗/Reading 2 min/.md// Auto-verified translation
#fingerprinting#browser fingerprint#anti-bot#automation detection#IEEE 754#correct rounding#ULP#unit in the last place
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.
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.
— **Scrapfly Engineering** — équipe d'ingénierie de **Scrapfly** , scrapfly.dev
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 onlyMath.* 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.
Key takeaways
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.
Key figures
trois valeurs distinctes sur glibc, libsystem_m et UCRT pour l'entrée 0.8