Skip to content
All fingerprint checksError subsystem

a stack trace names the function it was thrown in

Check id error-stack-liveness

What an ordinary browser yields

The name of a function created microseconds ago appears in the stack of an Error thrown inside it.

What a detector infers

A stack trace is not a string, it is a measurement of the present moment — a function of who called whom, from what URL, at what column, right now. PerimeterX throws a deliberate null dereference and keeps the wreckage, and its captcha path collects four stacks from four error types, because in the study's own words one stack is a bot taking a shortcut. The forgery is in that repo verbatim: a helper that returns four frozen template strings with a per-Chrome-major lookup table, which the authors themselves document breaking on every Chrome bump. So this check asks the engine a question it could not have known the answer to in advance. It mints a function name at runtime and throws inside it: a canned stack has nowhere to hide, because it cannot contain a name that did not exist when it was written. Two implementation details are load-bearing. The positive control reads its own identifier off Function.name rather than hardcoding it, because this bundle is minified and a hardcoded name would never match — the check would silently self-disable and pass everything forever. And the control itself is the guard that keeps us from flagging WebKit: if this engine does not name an ordinary declared function in its stacks at all, we have no business asserting anything and the check reports N/A. A V8 detail worth knowing: stack is an own accessor on each Error instance and Error.prototype has no stack at all, so a naive prototype-level shim is shadowed and never runs. What the check deliberately does NOT do is assert stack SHAPE — shape needs a version table that rots, exactly like the heap lattice this release retires. Liveness needs nothing but the browser itself.

How to resolve it

Error.stack cannot be replayed, only re-derived. Returning a fixed string from a patched stack getter answers a question the caller has not asked yet, and a runtime-minted name exposes it instantly. If a stack must be shaped, it has to be generated from the real call chain rather than replayed from a template — which is the same amount of work as not patching it.

Why does Function.toString() reveal a hooked function?

Read in the wild by

Kasada

Kasada provokes deliberate throws through Error and Function stringification — `new Function("Error.toString.call(1)")()` and `new Function("Function.__proto__.toString.call()")()` — then serializes the caught error's live `.message` and `.stack` and ships them in its encrypted payload, under its own recovered field names `patchedErrStringErrMsg`/`patchedErrStringErrStack` and `patchedFuncStringErrMsg`/`patchedFuncStringErrStack`. It scrubs its own script URL out of the stack before sending, which is a good indication of what it expects to find in there. A separate probe throws `class kekko extends Function.toString {}` and reads the resulting stack in the page, scanning for the string `[object Function]` and keeping a slice of the raw text around the hit; the sibling that scans a `class kek extends JSON.stringify {}` stack navigates the page to `about:blank` when it matches. What the artifact shows is the collection: a throw provoked through a stringification path, and the live stack and message that come back off it. The runtime-minted-name liveness test this check runs is ours, not Kasada's.

2.txt:40048 `new Function("Error.toString.call(1)")()` -> catch -> 2.txt:40076/40102 `patchedErrStringErrMsg`/`patchedErrStringErrStack`; 2.txt:46655 `new Function("Function.__proto__.toString.call()")()` -> catch -> 2.txt:46687/46712 `patchedFuncStringErrMsg`/`patchedFuncStringErrStack`; serializer func_026f90 reads the caught error's live `.message` (2.txt:40297) and `.stack` (2.txt:40299), path-scrubbed to "[path]", stored at 2.txt:40326. Client-side stack inspection: 2.txt:42820 `new Function("class kekko extends Function.toString {}")()` -> 2.txt:42843 `r4.stack.indexOf("[object Function]")` -> `verdict` plus a raw slice of the live stack; 2.txt:13825 the same scan on a `class kek extends JSON.stringify {}` throw, navigating to "about:blank" on a hit (2.txt:13830). Corroborated in a second script version at 3.txt:25288, 3.txt:3093, 3.txt:32705. Probe list: probes.txt lines 253-256.

PerimeterX / HUMAN

Throws a deliberate null dereference purely to harvest the resulting Error.stack, and ships the string as an EV2 field — the captured value in the study's own field dump begins "TypeError: Cannot read properties of nu…", so this fires in production. One collection path truncates the stack to its last 20 lines before sending; other call sites ship it raw. The Grubhub build additionally carries a dedicated "TypeError stack" EV2 field of its own.

main.min.js (iFood build): `function nk(){try{null[0]}catch(t){return t.stack||""}}`; truncation helper `function kE(t){var n=t.split("\n");return n.length>kj?n.slice(n.length-kj,n.length)[hN(93)]("\n"):t}` with `,kj=20`, applied at its single call site `e["XGhpYhoEY1Q="]=kE(nk())` — while other sites assign the stack raw (`t["XGhpYhoEY1Q="]=nk()`, `n["XGhpYhoEY1Q="]=nk()`, and a click handler). all_fields_map.json:1496-1503 (bug_report/sdk_drift_cases/2026-05-19_ifood/) records: key "XGhpYhoEY1Q=", src "EV2", val "TypeError: Cannot read properties of nu", ctx "\"XGhpYhoEY1Q=\"] = kE(nk())" — note the record's own `"line": 4391` refers to the beautified SDK, not to this JSON, which is 1963 lines long. EV1_EV2_UNIFIED_REFERENCE.md:165, under table "G. Platform-Specific Fields" (columns: Field | iFood only | Grubhub only | Common): `WGRubh4IZ1g=` (TypeError stack) | - | ✓ | EV2.

Every attribution traces to a published artifact. See the sources and their limits.

Other checks in Error subsystem

Who builds this test

Clearcote is a browser built for fingerprint coherence

It is a Chromium fork, maintained by the same people who wrote this reference. It ships as a compiled browser rather than as a stealth script injected into someone else's — which is a description of how it is built, and is not an argument about how it behaves on this check.

This audit takes no position on how Clearcote scores on Error subsystem checks, on this one, or anywhere else. It has no baseline corpus of other people's fingerprints to rank you against and no vendor scoreboard — nearly every check is self-referential, asking one browser the same question through two independent APIs and reporting whether both answers can be true at once. It runs identically on any browser, including ours. Run it on yours and read the result yourself.

See the other checks in Error subsystem — the family error-stack-liveness belongs to.