Repository navigation
Finding the YouTube stop
Since 2026-09-23 a YouTube watch page played and then stopped at about 44 seconds of media, with the player's own "Something went wrong" screen. On 2026-10-02 the cause was fixed. This page records how, so the method can be reused.
The media stream was fed for about 30 seconds, with 62 seconds buffered. Then the video server answered with tiny control messages instead of media.
The site runs an attestation program in the page: a check of whether the environment is a real browser. The rule from the start is that this program and its protocol are a black box. Sashfold never reads, patches, traces or decodes it. The engine improves by the standards; measurement happens only on our side.
Over a week, each was ruled out with a control:
- A wrong or missing user-agent string, tested by A/B in the real window.
- Cookies and headers, identical by name to Brave's.
- The timing of the exchange: Brave throttled 8x and 20x still plays.
- The media pipeline. Both tracks buffered far past the stop.
- Missing web APIs. First 48 names, then 39. Deleting them from a real Brave did not stop it. Only one name,
window.trustedTypes, made Brave stop, and adding Trusted Types to Sashfold did not fix Sashfold.
Brave was driven over the Chrome DevTools Protocol, paused on every exception, caught or uncaught. Each was printed in the line shape of Sashfold's own throw trace, and the same page ran in Sashfold's real window with that trace on.
A script diffed the two sets by message, by message plus origin line, and by origin line. Over 60 seconds Brave raised 311 exceptions and Sashfold about 90. The differing rows were the engine's conformance list:
- performance.mark with a negative start time. Chrome throws; Sashfold accepted it. The counts were 0 against 27.
- "Illegal invocation" from a one-line eval'd probe: 1 in Sashfold against 5 in Brave, plus one rejected Permissions.query. Built-ins called on the wrong kind of object: a browser refuses, Sashfold answered.
- The player's storage reads never parsed plain strings in Sashfold.
- The Cache API: used 94 times in Brave, missing in Sashfold.
- Three Polymer binding texts reached JSON.parse only in Sashfold.
The attestation request and answer were compared by JSON structure only, with every string replaced by its length. Both sides send the same envelope: a 20-character string and one long opaque string. Brave's was about 2,000 characters. Sashfold's was 823.
The content was never read or printed; two Brave runs seconds apart differ byte for byte anyway. One more arm: Brave with all 39 remaining missing APIs deleted still sent about 2,020 characters and played. Absent features did not make the string short; measured behaviour did.
A probe of our own called every member of every prototype of every global interface with four wrong receivers: a plain object, the prototype itself, no object, and an object created from the prototype. The outputs from Brave and Sashfold were diffed line by line: about 7,200 lines against 3,400. The diff listed which members answered when they should have refused, which should reject a promise instead of throwing, and which attributes are lenient about this in Chrome.
The web platform tests had already named 25 of the affected getters in idlharness rows failing all along ("getting property on prototype object must throw TypeError"): 19 on Navigator, 3 on History, and BarProp, the track lists and CSSStyleDeclaration.parentRule.
WebIDL requires that a member defined on an interface's prototype refuse a receiver that does not implement the interface, with a TypeError that browsers word as "Illegal invocation". Sashfold's binding helpers defined getters and operations that ignored this.
Commit 965408c on main, 2026-10-02, "The members of an interface answer for its objects alone", writes the check once, in the two helpers that define every attribute and operation:
- The receiver must have the interface's prototype above it on its chain. A window's proxy is unwrapped; another realm's object is recognised by the interface's name.
- An undefined or null
thisstands for the window when the window implements the interface. - The global interfaces are exempt, and the receiver is judged before the argument count.
- A member of Promise type rejects instead of throwing.
- onmouseenter, onmouseleave and onreadystatechange are lenient, as in Chrome.
Natives shared between two prototypes broke under the rule and are now defined per interface, among them ParentNode on Document and the worker's location and navigator.
In the real window the opaque string in the request went from 823 to 1,603 characters, and the answer is token-shaped. The media kept flowing to 237 seconds buffered instead of starving at 62. The watch page plays through: 120 seconds on the real clock, the media clock at 115 seconds at the end, 30 pictures a second from the first, no reset and no error screen. The user confirmed it in their own window.
The gate holds: 1,314 binding checks and 113 worker checks pass, and the web platform test harness, run alone, gained 217 subtests and lost none (145,446 to 145,663).
- Differential testing on the real workload beats reading the other side's code: run the page in a real browser and in ours with the same instrument, and diff.
- Measure shapes and lengths, never contents, when the content belongs to someone else's security system.
- Delete-and-bisect in the real browser answers "which name matters". It cannot answer "which behaviour matters". The exception and receiver differentials do that.
- The standards' own test suite often already names the fault. Read its failing rows before writing probes.
- A meter you can read from your own side, here the length of the string we send, tells you whether a change moved toward the goal before the goal is reached.
Getting started
What works today
Measured
Design decisions
Testing and tools