The hidden tell

When a script runs Math.tanh(0.8) the result varies by OS: Linux‑Chrome returns 0.6640367702678491, macOS‑Chrome 0.664036770267849, and Windows‑Chrome 0.6640367702678489 [1][2]. The difference is a single unit in the last place (1 ULP) and stems from the platform’s math library (glibc, libsystem_m, or UCRT) [3][4].

Until Chrome 148 V8 used a bundled fdlibm implementation, producing identical bits on every system [5]. A V8 commit switched Math.tanh to std::tanh, which forwards the call to the host libm. The change ships in V8 14.8.57 and appears in Chrome 148‑150 [6]. Consequently, a single tanh probe can confirm whether the browser is truly running on the claimed OS, breaking naïve User‑Agent spoofing.

Where the leak propagates

The host library also powers other surfaces:

  • CSS trig functions (sin(), cos(), atan2()) call the platform std::sin after a degree‑to‑radian conversion, exposing the same OS‑specific bits [7].
  • Web Audio on macOS uses Apple’s Accelerate DSP for FFTs and scalar libsystem_m for the DynamicsCompressor, leaking both the OS and the CPU architecture (ARM vs x86) [8][9].
  • WASM remains safe because it lacks transcendental opcodes and relies on hardware arithmetic only.

The fingerprint map can be summarised:

Surface Leak source
Math.tanh host libm (glibc, libsystem_m, UCRT)
CSS sin, cos, tan, asin, acos, atan, atan2 host libm
Web Audio FFT (macOS) Accelerate (vDSP)
Web Audio compressor (macOS) scalar libsystem_m

Only these three entry points need to be normalised for a scraper to appear genuine.

Why naïve spoofing fails

Re‑implementing the math functions sounds easy, but four traps exist [10]:

  1. Partial coverage – most V8 math is still bundled; spoofing only Math.tanh creates a mismatch across the other functions.
  2. Different code paths – CSS and JS use separate libraries, so a single stub cannot satisfy both.
  3. Dual macOS libraries – Apple ships scalar libsystem_m and vector Accelerate; choosing the wrong one yields a systematic 1 ULP shift.
  4. Architecture differences – fused‑multiply‑add (FMA) behaviour varies between ARM and x86, affecting rounding.

Any mismatch is detectable because anti‑bot systems compare the probe against a table of known OS outputs; a value that does not match any real OS is flagged as synthetic.

Scrapfly’s mitigation strategy

  1. Exact reproduction – We reverse‑engineered the minimax coefficients, lookup tables and reduction constants from each target libm and transcribed them to portable C. The code mirrors the original bit‑for‑bit, including explicit fma() calls and -ffp-contract=off to prevent compiler‑added fusions, guaranteeing identical results on both FMA and non‑FMA CPUs [11].
  2. Dynamic library lifting – For Windows we map the genuine ucrtbase.dll into the Linux process and call its exports via the MS‑ABI calling convention, forcing the same SIMD dispatch flags that a real Windows binary would enable [12].
  3. Selective gating – The single choke point (the call that reaches std::tanh or std::sin) is patched to select the appropriate library based on the claimed OS. CSS and Web Audio paths are routed to the correct scalar or Accelerate implementation.
  4. Performance parity – Early prototypes that emulated fma in software were 2‑6× slower, exposing timing tells. Enabling hardware FMA narrowed the gap and made the reproduced functions faster than native glibc on comparable hardware.