Skip to content
On this page

An engine or OS oracle is a small, incidental observable whose value is fixed by the JavaScript engine or the host operating system, and which no User-Agent string can change. The canonical example is eval.toString().length: V8 returns 33 because it serialises the function as function eval() { [native code] }, while SpiderMonkey and JavaScriptCore return 37 for their own formatting. Oracles are valuable to detection systems precisely because they are side effects of implementation rather than declared identity - nobody sets them, so nobody remembers to keep them consistent with a claimed browser.

Quick facts

DefinitionAn incidental constant that leaks the real engine or OS
Classic engine oracleeval.toString().length - 33 on V8, 37 on SpiderMonkey and JavaScriptCore
Classic OS oracleRounding of transcendental math (the platform libm) and hyphenation dictionaries
CPU oracleThe sign bit of a canonical NaN differs between architectures
Why it mattersA UA claiming Chrome on Windows must match V8 and Windows oracles simultaneously

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 constant 20100101. Blink and WebKit return 20030107. 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 null produces 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 window tracks 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, and Math.expm1 are 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: auto uses 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.

Code example

javascript
// Engine and OS oracles: derived values that no User-Agent string can move.

// --- Engine family ---
const evalLen = eval.toString().length;         // V8: 33 | SpiderMonkey & JSC: 37
const engine  = evalLen === 33 ? 'v8' : 'spidermonkey-or-jsc';
const gecko   = navigator.productSub === '20100101';   // Gecko hardcodes this

// --- CPU architecture: canonical NaN byte layout ---
const nanBytes = new Uint8Array(new Float64Array([NaN]).buffer).join(',');

// --- OS: platform libm rounding on transcendental functions ---
const mathSig = [Math.tanh(1), Math.sinh(1), Math.expm1(1), Math.acosh(1e300)]
  .map(n => n.toString(36))
  .join('|');

// --- Coherence: does the claim match the oracles? ---
const claimsChrome = /Chrome\/\d+/.test(navigator.userAgent) && !/Firefox/.test(navigator.userAgent);
const coherent = !claimsChrome || (engine === 'v8' && !gecko);

console.log({ evalLen, engine, gecko, nanBytes, mathSig, coherent });
// A UA claiming Chrome while evalLen is 37 is reporting an engine it is not running.

Next in Coherence and lie detection · 6 of 8

A third class needs no comparison at all - the spec defines the legal answer.

What Are Spec Invariants in Fingerprinting?

Related terms

Concept map

Concept map

How Engine/OS Oracle Fingerprinting connects

The terms most directly tied to this one. Hover a node to see its neighbours, click to preview, drag to rearrange.

0 terms · 0 connections
You are here · Anti-Bot
Building map…

Frequently asked questions

Why is eval.toString().length exactly 33 in Chrome?

V8 serialises native functions as the literal string "function eval() { [native code] }", which is 33 characters long. SpiderMonkey and JavaScriptCore use a slightly different layout with extra whitespace and newlines, giving 37. The number is a side effect of the serialiser, not a value anyone chose as an identifier, which is exactly why it is useful for identifying the engine.

Can an engine oracle be changed from JavaScript?

Only by replacing the function that produces it, which creates a bigger problem. Overriding Function.prototype.toString to return a different string means the override itself is visible to native function integrity checks, because the replacement is not a native function. The oracle is derived from engine internals, so moving it convincingly requires a modified engine build.

Do OS oracles still work if the browser runs in a container or VM?

Yes, because they read the operating system inside the container, not the host. A Linux container reports Linux math rounding and Linux hyphenation regardless of what the host machine runs. This is why a claimed Windows identity served from Linux infrastructure fails these checks unless the claim matches the actual guest OS.

Last updated: 2026-07-30 · Facts last verified: 2026-07-30