Engine oracles: constants nobody sets on purpose
JavaScript engines differ in dozens of ways that are visible but were never designed as identity. Each becomes an oracle:
eval.toString().length- the serialised form of a native function differs by engine. V8 produces a 33-character string; Gecko's SpiderMonkey and WebKit's JavaScriptCore produce 37. A claimed Chrome that answers 37 is not running V8.navigator.productSub- Gecko hardcodes the constant20100101. Blink and WebKit return20030107. The value has been meaningless for two decades and is frozen per engine family, which makes it a perfect tell.- Error dialect - the exact wording of an engine's runtime error messages is engine-specific. Calling a method on
nullproduces different text in V8 than in SpiderMonkey, and the shape of the stack trace differs further still. See stack depth fingerprinting for the related recursion-limit signal. - Global surface - the exact set of properties on
windowtracks browser version closely. A UA claiming a specific Chrome release should expose the globals that release shipped and none that arrived later.
None of these are reachable by changing a header, and most are not reachable by property overrides either, because they are computed rather than stored.
OS and CPU oracles: the machine under the engine
A second family leaks the operating system and processor rather than the browser:
- Transcendental math rounding - functions like
Math.tanh,Math.sinh, andMath.expm1are not fully specified to the last bit. Engines delegate to the platform math library, and glibc, Apple's libm, and the Microsoft CRT round the final ulp differently. A handful of carefully chosen inputs produce an OS signature. This is the mechanism behind math fingerprinting. - Canonical NaN bit patterns - when a NaN is written into a typed array and read back as bytes, the sign bit and payload differ between processor architectures. Comparing the JavaScript result against the WebAssembly result adds a second, independent read of the same fact.
- Hyphenation dictionaries - CSS
hyphens: autouses the OS or bundled dictionary for the document language. Where a long word breaks is an observable that varies by platform. - System colors and default styles - CSS system color keywords resolve from the platform theme, and the user-agent stylesheet has small per-platform differences.
- Emoji and font advance widths - measuring the advance width of a glyph reveals which platform font served it, a mechanism shared with font fingerprinting.
Why oracles are the hardest layer to keep consistent
A declared value is stored somewhere and can be changed. An oracle is derived - it falls out of how the engine was compiled and which system libraries it links against. Changing eval.toString().length means changing the function serialiser. Changing Math.tanh rounding means shipping a different math library. These are build-time properties of the binary, not runtime properties of a session.
The consequence is a hard floor on what a claimed identity can be. A request that presents itself as Chrome on Windows has to answer as V8 and as the Windows CRT and as an x86-64 NaN layout, all at once, on every one of these incidental observables. Running Firefox and claiming Chrome fails at productSub before any fingerprint hash is computed. Running Chrome on Linux and claiming Windows fails at the math and hyphenation oracles.
In practice this pushes serious work toward matching the claim to the binary rather than the other way around: run a real engine on a real platform and let the declared identity describe what is actually there. Forks built for this reason patch at the engine level so that derived values move together with declared ones. For teams that only need the data, a managed web data API removes the problem from scope - the browser fleet already runs real engines on real operating systems, so the oracles agree with the identity by default.