Back to Devoured - July 13, 2026
DEVOURED
Your Browser Does Math Differently on Every OS, and Anti-Bot Systems Read the Bits

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

Tech Scrapfly
Small discrepancies in how operating systems calculate floating-point math reveal a browser's true OS, providing anti-bot systems a reliable fingerprinting signal.
What: Engineers at Scrapfly discovered that functions like Math.tanh and CSS trigonometric operations route to different host libraries (glibc on Linux, libsystem_m on macOS, UCRT on Windows), resulting in bit-level differences that identify the underlying OS. Chrome 148+ leaks this information by abandoning the platform-agnostic bundled V8 math library in favor of host-native calls. Spoofing these requires bit-for-bit reproduction of specific vendor minimax coefficients and coefficients, or mapping and calling the actual UCRT DLLs directly.
Why it matters: Fingerprinting has moved beyond obvious surface-level signals like canvas or fonts into low-level execution environments, forcing scrapers and privacy-focused browsers to implement complex, hardware-accurate emulation to remain indistinguishable from real traffic.
Takeaway: If you are building a headless browser or scraper, you must verify your math output against real-device oracles or call native platform DLLs directly to avoid being fingerprinted by anti-bot services like Cloudflare.
Deep dive
  • Mathematical Leakage: Chrome 148 replaced portable fdlibm routines with native platform libm calls, causing non-deterministic outputs across OS environments.
  • Surface Areas: Leakage occurs in JavaScript's Math.tanh, CSS calc() trigonometric functions, and Web Audio DSP operations.
  • Vendor Discrepancies: Each OS (Linux, macOS, Windows) uses unique polynomial approximations for transcendental functions, creating distinct OS signatures.
  • Implementation Pitfalls: Using incorrect libraries (e.g., Apple's Accelerate vs. scalar libsystem_m) or failing to account for hardware FMA instructions leads to detectable inconsistencies.
  • Remediation: Correct spoofing requires either transcribing exact vendor coefficients with FMA contraction disabled or directly mapping and calling target DLLs (like UCRT) using platform-specific ABI conventions.
Decoder
  • ULP (Unit in the Last Place): The smallest possible increment between two representable floating-point numbers; used here to describe the tiny precision differences between math libraries.
  • libm: The C standard library module providing mathematical functions like sine, cosine, and tangent.
  • Minimax polynomial: An approximating polynomial designed to minimize the maximum error between a function and its approximation.
  • FMA (Fused Multiply-Add): A CPU instruction that performs (a
  • b) + c as one operation with a single rounding step, critical for maintaining bit-parity across different architectures.
Original article

Fingerprinting is usually about canvas, WebGL, fonts, audio. There is a quieter signal, and it lives in the last bits of a number.

Run this in any console:

Math.tanh(0.8)
// 0.6640367702678491   genuine Linux Chrome    (glibc)
// 0.664036770267849    genuine macOS Chrome     (libsystem_m)
// 0.6640367702678489   genuine Windows Chrome   (UCRT)

That output is an approximation, and its exact bits depend on the OS that computed it. A genuine Mac runs Math.tanh through Apple’s math library. Linux runs it through glibc. The two disagree on about a quarter of all inputs, usually by one unit in the last place (1 ULP). Windows, through the Universal C Runtime, disagrees with both on a few percent, and on the input above all three land on a different bit.

The same call, run on genuine Chrome 150 across three real machines:

Call Linux (glibc) macOS (libsystem_m) Windows (UCRT) Split
Math.tanh(0.5) 0.46211715726000974 0.46211715726000974 0.46211715726000974 all three agree
Math.tanh(0.7) 0.6043677771171636 0.6043677771171635 0.6043677771171635 Linux alone, 1 ULP
Math.tanh(0.8) 0.6640367702678491 0.664036770267849 0.6640367702678489 all three differ, 2 ULP spread
Math.tanh(0.9) 0.7162978701990245 0.7162978701990245 0.7162978701990244 Windows alone, 1 ULP

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.

This tell is recent. Until Chrome 148, V8 computed tanh itself with a bundled fdlibm port, so it returned the same bits on every OS and leaked nothing. V8 commit c1486295ae5 replaced it with std::tanh, which reads the host libm. It first shipped in V8 14.8.57, which is Chrome 148. Chrome 147 and earlier do not leak here. Chrome 148, 149, and 150 do.

Why one function returns different bits

IEEE 754 defines how a double is stored. It does not require sin, cos, tanh, or exp to be correctly rounded. Correct rounding is expensive, so every vendor ships a libm that trades a fraction of a ULP for speed, with its own minimax coefficients, lookup tables, and reduction constants.

The three implementations produce three sets of bits:

  • Linux: glibc
  • macOS: Apple libsystem_m
  • Windows: UCRT (ucrtbase.dll)

They agree almost everywhere and split just often enough to classify the OS. A detector needs no math, only a table: genuine macOS Chrome returns one pattern for cos(1), genuine Linux Chrome returns another, and a single comparison tells them apart.

Four traps

“Just reimplement the Mac functions” breaks on contact, for four reasons.

1. Only some math leaks. V8 ships its own math and links it statically: Math.exp, Math.pow, Math.atan, and most of the rest come from bundled llvm-libc, and Math.sin / Math.cos from a bundled glibc-derived dbl-64 routine. All of them are identical on every OS, so spoofing them creates an inconsistency. The exception is Math.tanh: since Chrome 148 V8 computes it with the platform std::tanh instead of the bundled routine it used before, so it now reads the host libm. It is the only Math.* that leaks the OS, and that asymmetry is itself checkable.

2. JavaScript math and CSS math are different code paths. CSS sin(), cos(), and atan2() do not share code with Math.sin. The layout engine reduces the angle in degrees, then calls platform std::sin on the reduced value. That gives a different result than a direct radian sin(), and it hits the host libm, so all seven CSS trig functions leak. We 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 disagree. Apple Silicon carries scalar libsystem_m and the Accelerate framework’s vector routines (vvsin, vvtanh). They are different code. Across a million inputs they diverge on 10 to 89 percent, depending on the function. Take cos(0): scalar returns exactly 1.0, Accelerate returns 0.9999999999999999. So “reproduce Apple’s math” is undefined until you know which library the browser calls, at which site. We resolved it by driving real Chrome on a real Mac over the debugging protocol and reading the exact double. Answer: scalar libsystem_m backs Math.tanh, CSS trig, and the audio compressor’s per-sample transcendentals. Accelerate backs Chrome’s Web Audio DSP on Mac, the FFT, the vector math, and the biquad filters.

4. 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 a multiply-add on one target and not the other.

The map: what leaks where

Operation V8 Math.* (JS) CSS calc() Web Audio
sin cos tan V8 bundled host libm Accelerate (osc FFT), sin scalar in the compressor
asin acos atan atan2 V8 bundled host libm not used
tanh host libm none not used
exp V8 bundled host libm scalar in the compressor
log log2 log10 pow V8 bundled host libm scalar log10f / powf in the compressor
vector add/mul/scale, FFT n/a n/a Accelerate (vDSP) on Mac
sqrt abs + - * / hardware hardware hardware

How to close it

No noise. 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. The target is a value identical to the OS you claim, which noise cannot produce.

Reproduce the algorithm exactly. Recover the target’s minimax coefficients, exponent tables, and reduction constants from its libm, and transcribe them to portable C. Match every bit, including the inputs where the target rounds the wrong way.

Make it deterministic. That explicit fma() matters. Compile with FMA contraction off (-ffp-contract=off) so the compiler never invents or drops a fusion of its own. Now the fused ops are exactly the ones Apple fuses, and the result is identical on FMA and non-FMA CPUs.

When reproduction is not worth it, lift the original. Windows UCRT is x86-64, the same ISA as a Linux server, and position-independent. Map the genuine ucrtbase.dll into memory at runtime and call its exports directly. The bits are genuine because the code is genuine, no reverse-engineering required.

Patch the chokepoint, gate it. Hook the single function that owns the value, where the engine calls libm. Gate on the claimed OS: Linux keeps glibc, Mac gets the reproduction.

Watch the clock. A perfect reproduction that runs slow is still a tell. Our first build lowered every fma() to a software call, because the default x86 baseline predates hardware FMA. That ran 2.5 to 6 times slower than native. A loop timing Math.tanh against Math.sin would show a ratio no real browser has. Turning on hardware FMA cut each fused op to one instruction: about 6 times faster, faster than glibc, and bit-identical.

Validation

None of this ships without proof. Our harness runs 871,000 inputs per release across every branch and domain: dense grids, interval boundaries, subnormals, signed zeros, infinities, NaNs. Two ground truths back it:

  1. A genuine-device oracle: a real Mac computing both scalar and Accelerate results for every input, so we know exactly where the two disagree.
  2. A genuine-browser anchor: real Chrome on a real Mac over the debugging protocol, computing Math.tanh and every CSS trig function at full precision. This is the surface a fingerprinter reads.

Why it matters

Math is deterministic, cheap to probe, hard to fake, and almost never on a spoofing stack’s radar. That makes it a strong signal for a defender and a liability for a scraper. Getting it right takes reverse-engineering vendor libm internals, mapping how three engines route math per call site, matching algorithms to the last bit, holding determinism across architectures, and proving it against real hardware.

Scrapfly’s browser carries all of it. Send a request through our API and ask to present as macOS, and the identity holds down to the rounding of a cosine.

Explore all 70 articles in today's digest

Fresh dev news, AI-digested daily. Never miss a trend at devoured.fyi