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 platformstd::sinafter 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_mfor 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]:
- Partial coverage – most V8 math is still bundled; spoofing only
Math.tanhcreates a mismatch across the other functions. - Different code paths – CSS and JS use separate libraries, so a single stub cannot satisfy both.
- Dual macOS libraries – Apple ships scalar
libsystem_mand vector Accelerate; choosing the wrong one yields a systematic 1 ULP shift. - 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
- Exact reproduction – We reverse‑engineered the minimax coefficients, lookup tables and reduction constants from each target
libmand transcribed them to portable C. The code mirrors the original bit‑for‑bit, including explicitfma()calls and-ffp-contract=offto prevent compiler‑added fusions, guaranteeing identical results on both FMA and non‑FMA CPUs [11]. - Dynamic library lifting – For Windows we map the genuine
ucrtbase.dllinto 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]. - Selective gating – The single choke point (the call that reaches
std::tanhorstd::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. - Performance parity – Early prototypes that emulated
fmain 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.


