Property enumeration order is a browser fingerprint in its own right: the order in which Object.getOwnPropertyNames(window), or a for...in over navigator, lists its names. Every comparison we had written against Google Chrome sorted those lists first, and a sorted list cannot show order. When we compared them unsorted, the only differences in order were the twenty field-trial entries we already knew about. With those switched off, our engine matched branded Chrome name for name and position for position.

That took one family of suspects off the list while we were chasing IPHey's verdict. It also taught us something about our own instruments, which is the more useful half.

Why the order of names says anything

JavaScript defines the order of an object's own string keys as the order they were created in. Integer-like keys come first, ascending; symbols come last. For a browser object that means the list reflects the order in which the engine installed each interface's attributes and methods. That order is fixed when the browser is built.

So a page can do this and hash the result:

Object.getOwnPropertyNames(window)
Object.getOwnPropertyNames(Navigator.prototype)
Object.getOwnPropertyNames(Document.prototype)

Two browsers with the same set of APIs installed in the same order give identical lists. A browser that enables one extra API puts a name somewhere in the middle, and every hash of the list changes. It moves without any single property's value changing, so a check that compares values one by one doesn't see it.

The comparison most of us write first is this one:

Object.getOwnPropertyNames(window).sort()

Sorting answers "which names exist". It discards "where each one sits". Two lists with the same names in a different order are equal after .sort(), so the diff reports nothing, and nothing looks a lot like a match.

Every profile is drawn from one real computer, not from separate listsIn Mosaic: Machines that exist. GPU, screen, CPU and fonts from one real device. Free holds 3 profiles. Pro is $49 a month for any number.

How it works: Machines that exist

What we had been comparing

Our check check:stock launches the engine beside a stock browser of the same major and diffs what a page can read. It compared sorted name lists, and so did every one-off probe we wrote while working on IPHey's "inconsistent browser fingerprint" signal. Every one of them could find a missing name or an extra one. None could find a name in the wrong place.

The first unsorted run was against branded Google Chrome 153 on the same Mac, with our engine launched without --disable-field-trial-config. The order differences it found were exactly the twenty field-trial entries:

window                 ModelContext between ServiceWorkerContainer
                       and NotRestoredReasonDetails
Navigator.prototype    protectedAudience missing
Document.prototype     modelContext where browsingTopics should be

These are the experiments an unbranded Chromium runs from its compiled-in testing config at every launch, and Google Chrome doesn't. Twenty APIs Chrome doesn't have has the whole list. What the unsorted diff added was the confirmation that they were the only thing out of order.

The run with the switch on

With --disable-field-trial-config, which every real profile launch sends, the lists matched branded Chrome exactly, in order. That held across window, Navigator, navigator's own properties, Screen, Document, Element, Performance, CanvasRenderingContext2D, WebGL2RenderingContext, MediaDevices, window.chrome, CSSStyleDeclaration, and for...in over both navigator and window.

And IPHey still scored the profile MX 90 where Chrome scored 100.

At the time that looked conclusive. Values, descriptors, names and now order all matched, so we wrote that the signal was not in the JavaScript surface at all and turned to the layer below. The TLS ClientHello and the HTTP/2 preamble turned out identical too, each about a minute's probe with no rebuild.

What we took it to rule out

We took it as ruling out a patch bisect. A bisect costs a rebuild per step. Our patches act on surfaces a page reads, the surfaces we had compared now matched, so stepping through the series for a difference no page could see seemed pointless.

That reasoning was only as good as the list of surfaces behind it. The speech voice list had been read, compared and excused, on the grounds that it loads asynchronously. The probe read it before it had populated, got 0 in both browsers, and the excuse explained the 0. Branded Chrome lists 199 voices and any unbranded build 180, because 19 Google voices ship only with Chrome's branding (the voice list story has the numbers). The other half of IPHey's verdict was a Widevine device certificate sent in the clear, from a module our own series bundles in patch 0134, so a bisect would have found that half. It is in the (whale) signal write-up.

So "the JavaScript surface matches" was true for the parts our probes actually read, and one of the parts we had excused was the one that mattered. The order result itself stands: in order, our names and Chrome's agree.

How to compare enumeration order properly

  1. Keep the unsorted list. Compare it as a sequence and report the first index where the two differ, plus the neighbours on each side.
  2. Compare the sorted list as well. It tells you which names are missing or extra, so you can tell an order difference from a set difference.
  3. Read both browsers in the same world. A list read in an isolated world differs from one read in the main world: we once nearly reported window.chrome.app as missing because one probe read each browser from a different world.
  4. Use a browser people actually run as the control. Our baseline had been Chrome for Testing, which carries its own differences from Chrome.
  5. Treat every exemption in the diff as a measurement you've decided not to make, and re-read its reason. A value that loads late needs a probe that waits for it.

Order is one input among the many that make up a browser fingerprint, and in our case it came out clean. It still deserves a place in the comparison, because it is the one difference a sorted diff can never report.