20 KiB
Bucket 2: bugs in the new code (not regressions against main)
These are problems in code develop added or rewrote, where main was no better (or the feature didn't exist on main). Chrome, default flags. Ordered by impact. Repro paths are relative to packages/scramjet/packages/runway/src/tests/review/.
-
localStorage.key(i)/.lengthloops are O(n²) across every proxied site's keys. Each call runsObject.keysover the whole shared area and filters it. 1500 keys took about 0.8-12.8s depending on the run and load (main timed out outright). This hits localforage's localStorage driver, "clear keys with prefix" loops and analytics storage sweeps, and it gets slower the more the user browses. Cause:client/dom/storage.ts:56-66. CONFIRMED. Repro: a5/misc.ts ::rv5-misc-storage-perf -
CSSGroupingRule.insertRule(@media/@supports/@layer/@container) andCSSKeyframesRule.appendRuledon't rewriteurl(). #109 claims to cover the CSSOM but only hooksCSSStyleSheet.insertRule/addRule/replace/replaceSync, so Twind, Stitches, JSS nested media and UnoCSS runtime images and fonts hit the proxy origin. CONFIRMED (same on main). Repro: a7/css-gaps.ts ::rv7-css-gaps(grouping_insert,keyframes_append) -
Line-1 columns are off by hundreds of characters in stack traces and
error.colno. The pst prelude, which carries the whole base64 sourcemap for SW-rewritten scripts, is spliced onto line 1 without shifting the map. Line numbers are now right (main was off by lines), but minified one-line bundles report columns off by the prelude's length to Sentry and Bugsnag. CONFIRMED. Repro: a6/columns.ts ::rv6-col-oneline -
Opaque-origin documents get a fresh random storage scope on every load and write to persistent storage. This covers data: documents and about:blank frames with no creator. The data is unreachable afterwards but consumes shared quota forever and slows the scans in #1. Browsers refuse storage for opaque origins. CONFIRMED. On develop each
data:frame writes real keys under a new randomabout-opaque://…@prefix (main writes under a shared@key; Chrome throws SecurityError). Repro: a6/qa.ts ::rv6-qa-opaque-storage -
box.scriptrealms/box.scripthashesgrow without bound. Every script,evalandnew Functionbody registers a permanent record that holds its client strongly, and each registration does a stack capture. Eval/template-heavy pages leak per call, and eval/new Functiongot about 20% slower. Cause:client/shared/incumbency.ts:195-217,client/singletonbox.ts:144-147. CONFIRMED. After 2000 distinct evals,scriptrealms/scripthashesgo from 1 to 2001 entries (main grows onlysourcemaps). That's 580 B/iteration vs 211. Repro: a6/memcount.ts ::rv6-memcount; a6/memory.ts (RV6_SCEN=evals) -
The rewriter and the client pick the incumbency mode independently. If
siteFlagsmake them disagree, every script calls an undefined$scramjet$registerrealmand dies. Default flags are safe; the fix is atypeofguard in the prelude, or always defining the global. CONFIRMED through the stale-$toppath (bucket 1 #16 and #19). An iframe'starget=_toplink to a page that a siteFlagsincumbencyoverride matches gives$scramjet$registerrealm is not defined, and none of that page's scripts run. Repro: a6/qa.ts ::rv6-qa-modemismatch-stale-top -
Named SharedWorkers still see the scoped name (
"<origin>@name"). develop's newSharedWorkerGlobalScope.prototype.nameinterceptor never takes effect, becausenameis an own property of the worker global. CONFIRMED (same on main for named workers). Repro: a5/workers.ts ::rv5-sharedworker-string-name,-dict-name,-blob -
Import-map URL-like keys and scopes only match for inline modules or same-origin importers (correction: prefix scopes never match a static import on develop, even from same-origin importers; see the root-scope item in bucket 1). A key is compared against the fully rewritten URL, including an importer-specific
$io, so static imports from CDN modules never match. This is new functionality, and main never mapped URL-like keys. CONFIRMED. Page-levelimport()is mapped, but a static import inside a CDN module is not. Repro: a6/qa.ts ::rv6-qa-importmap-cdn-key -
$topcopies the top page's full URL, including its#fragment, into every URL rewritten in a subframe. That bloats URLs and leaks fragments to subresource requests; it's the same parameter behind bucket 1 #16. CONFIRMED (the fragment only appears inside proxy URLs; the origin never sees$top). Repro: a6/qa.ts ::rv6-qa-top-fragment -
Resource-timing reads are about 4-10x slower (about 7x in QA), and RUM agents poll them. scramjet's own
/scramjet/scramjet.jsis still visible ingetEntriesByType('resource'), as it was on main. CONFIRMED. Repro: a7/css-perf-probe.ts ::rv7-perfprobe(getEntriesByType_res_ms); a7/misc-probe.ts (perf_resource_masked) -
history.pushState(null, '', 'http://user:pw@<same host>/x')is accepted and puts the userinfo intolocation. Native throws SecurityError. It has the same root cause as bucket 1 #39 (comparing origins instead of URL components). CONFIRMED. See a7 findings, "Minor". -
The demo FlagEditor renders
incumbencyas a checkbox. Toggling it stores a boolean, and every rewrite then throws "incumbency was not string"; the value persists in localStorage. CONFIRMED, and worse than it sounds: withallowInvalidJson by default, a failed rewrite means scripts run unrewritten, with the raw proxylocation, instead of throwing. Where:demo/src/components/FlagEditor.tsx:108-122. -
class X extends WebSocket(and WebSocketStream) instances don't get the subclass prototype.new X()has no subclass methods,instanceof Xis false, andReflect.constructignoresnewTarget. Main was worse (its constructor threw). Fix: usenewTarget.prototypeinWebSocket.ts:364-376andWebSocketStream.ts:177. CONFIRMED. Repro: a7/subclass-probe2.ts ::rv7-subclass2-probe(ws_subclass_method,reflect_construct_ws,wss_subclass) -
WebSocket.close(1000.9)throws InvalidAccessError.[Clamp]rounds the code to 1001 where Chrome truncates it to 1000. Main'sclose()was broken for every call. CONFIRMED. See a3 findings, "Pass 2". -
Minor IDL fidelity issues.
localStorage.setItem("k")reports "Illegal invocation" instead of "2 arguments required": the validator fallback calls the native with the proxy asthis.fetch/Requestinit members are read out of IDL order. Also note: when the argument check atclient/client.ts:1321-1327rejects a call, the original native runs with the raw arguments and URL rewriting is skipped. No over-strict declaration was found, but any future one would let URLs through unrewritten. CONFIRMED. Repro: a3/rv3-idl.ts; a6/qa.ts ::rv6-qa-mutationobserver-script(setItem-1arg) -
bytesToBase64looks up.callon the page-writableFunction.prototypeat call time. It runs for every script source the text layer stores, so a page that replacesFunction.prototype.call(anti-debug and tracing libraries) breaks script-source bookkeeping. develop calls the page'scallonce per inline script insert, where main called it 25 times; a throwingcallbreaks script insertion on both builds. Fix:Function_call(bytesToBase64Native, bytes). Cause:shared/util.ts:47-50. CONFIRMED. Repro: a6/qa.ts ::rv6-qa-b64-call -
saveNativeseagerly reads every property of the global during client construction, includinglocalStorage,sessionStorageandindexedDB. Where one of those getters throws (storage blocked by browser settings, or third-party-storage restrictions in some embed contexts), building the client could throw and leave the realm unhooked. It also forces layout and instantiates lazy objects (see bucket 1 #27). Cause:client/client.ts:384-408(#90). Mechanism CONFIRMED; the natural trigger (browser-blocked storage) stays PLAUSIBLE, because those settings also stop the SW from registering. It's also a widened sandbox escape. A page can reach a fresh iframe throughwindow.frames[i](not intercepted) before the firstcontentWindowaccess and plant a throwing own getter. Client construction then throws and the frame stays unhooked, leaving a raw same-origin realm. On main only a throwinglocalStoragedid this; on develop any property does. Repro: a6/qa.ts ::rv6-qa-savenatives-variants -
webkitMatchesSelectorbypasses the selector rewriting.matches("a[href^='/foo']")now works, butwebkitMatchesSelectorwith the same selector doesn't: it's a separate IDL operation in Blink and isn't intercepted. Old jQuery/Sizzle and polyfills pickwebkitMatchesSelectorfirst. Cause:client/dom/element.ts:55. CONFIRMED. Repro: a2/consistency.ts ::rv2-cons-selectors(label "matches") (run withoutRUNWAY_FAST) -
removeAttribute("nonce")on an element with no nonce attribute clears thenonceslot (toggleAttributeis fine on develop; main fails all three variants differently).AttributeLayer.removeruns its change steps whether or not the attribute existed, sos.nonce = 'abc'; s.removeAttribute('nonce')leaves''. The same unconditionalchanged()re-runs a full JS rewrite (text.sync) on everyremoveAttribute('type')of a script. Cause:client/attributes.ts:430. CONFIRMED. Repro: a2/nonce.ts ::rv2-nonce-remove-absent(run withoutRUNWAY_FAST) -
el.attributes.valuesexists on the wrapper, thoughNamedNodeMaphas novalues:"values" in el.attributesis false whiletypeof el.attributes.values === "function". This is a new tell on develop. On both builds,attributes[Symbol.iterator] !== Array.prototype.values. Cause:client/dom/attr.ts:127. CONFIRMED. Repro: a6/qa.ts ::rv6-qa-mutationobserver-script(attrs-values) -
Cleaned stacks show the site's original URL but keep line:column positions from the rewritten code. For example, TikTok's error is reported at column 656688 when the real call is at 544238. Source-mapping error reporters (Sentry, Bugsnag) then resolve to the wrong code, which is worse than main's obvious proxy URL. This compounds #3. Cause:
client/shared/error.ts(thecleanErrorsformatter, now default). CONFIRMED (live on tiktok.com; see a1 findings, "Pass 2"). -
Calling the
isTrustedgetter directly on a wrapped event throws. Every normal read ofisTrustedon a wrapped event returns true, including fake WebSocket events,window.eventand handleEvent objects. The one tell isObject.getOwnPropertyDescriptor(e, 'isTrusted').get.call(e)-style access, which throws. It affects every wrapped event kind. CONFIRMED. Repro: a6/qa-ws.ts ::rv6-qa-ws-istrusted(run withoutRUNWAY_FAST) -
Storage.prototype.clearskips the brand check, soclear.call({})doesn't throw. CONFIRMED. Repro: a12/rv12-misc.ts ::rv12-misc -
Decorator metadata (
Symbol.metadata) is treated as an interface static. That's 104 wasted lookups per window realm, and it could turn noisy or unsafe once V8 ships decorator metadata. PLAUSIBLE. See a12 findings. -
Meta refresh set through
meta.contentormeta.httpEquivis never rewritten, so the frame navigates straight to the real origin (proxy escape). develop's new meta rule fixed thesetAttributeorders only. With an absolute URL, the refresh leaves the proxy; relative ones land on the proxy origin. CONFIRMED. Repro: a13/misc.ts ::rv13-misc-meta-refresh(labelsrefresh-a/d/e/f,refresh-j; run withoutRUNWAY_FAST) -
svgA.target.baseVal = "_top"navigates the real top window, replacing the browser.js UI (escape). develop's newSVGAnimatedString.prototype.baseValinterceptor passes every non-href animated string straight to the native, so thetargetrule never runs. main didn't navigate at all (also wrong). Cause:client/dom/reflect.ts:1442-1457. CONFIRMED. Repro: a13/targets.ts ::rv13-target-svg-a-target-baseVal -
Copying or adopting an
Attrnode leaks the rewritten value.attr.cloneNode()anddocument.importNode(attr)return the proxy URL or the rewritten JS/CSS, so the common idiomdst.setAttributeNode(a.cloneNode())stores the proxy URL as the page's value.adoptNode(attr)leaves a mirror behind, so the element still reports an attribute it no longer has. CONFIRMED. Repro: a13/attrnodes.ts ::rv13-attrnodes -
Minor mirror-layer mismatches:
- Mixed-case names through the
*NSmethods (setAttributeNS(null, "SRC", v),createAttributeNS(null, "SRC")) overwrite the lowercase attribute's mirror. [*|href…]and[|href…]selectors skip the mirror.- After native editing,
[style*=…]selectors answer from a stale mirror. - After any CSSOM write the style mirror holds absolutised URLs (
url(/a.png)reads back ashttp://site/a.png). form.actionreturns the raw attribute for unparseable values (http://[bad), where Chrome and main normalise it, and a relative value insidecreateHTMLDocument()documents, where Chrome returns"".
CONFIRMED. Repro: a13/edge.ts (
rv13-edge-el-form), a13/selectors.ts ::rv13-selectors, a13/equal.ts; see a13 findings, "Minor" - Mixed-case names through the
-
localStorage.clear()fires one StorageEvent per key instead of a singlekey === nullevent, and a genuine null-key event from the browser is dropped. Handlers that treat a null key as "logged out in another tab" never run. CONFIRMED. Repro: a19/storage.ts ::rv19-storage-event-clear -
Object.keys(localStorage)/JSON.stringify(localStorage)cost about 13µs per key, 2.7x main, because the scope prefix is re-derived for every key. CONFIRMED. Repro: a19/perf.ts ::rv19-perf-storage-hot -
develop's new
CSSRule.styleandinsertRulerewrites resolve relativeurl()s against the document, not the stylesheet, andnew CSSStyleSheet({baseURL})is ignored. So develop loads the wrong path; main loaded nothing. CONFIRMED. Repro: a18/cssmatrix.ts ::rv18-cssmatrix(w_rulestyle_ext*,w_insertRule_ext,w_constructed_baseURL) -
The new typed-OM rewriting misses
CSSUnparsedValueand other hand-built values. CONFIRMED. Repro: a18/cssmatrix.ts ::rv18-cssmatrix(w_typed_unparsed) -
Prefix-mapped modules get a different URL from the same module reached any other way, so they're instantiated twice. The special query-less URL used for prefix mappings differs from what a relative import, an exact entry or
import()produces. On the real three.js import map,new EffectComposer(r).copyPass instanceof ShaderPassis false. On main these imports failed outright. CONFIRMED. Repro: a16/mod4.ts ::rv16-prefix-map-identity-static,rv16-prefix-map-identity-mixed; a15/params.ts ::rv15-importmap-prefix-dup -
Script-inserted import maps resolve against the base URL at
import()time, not at registration, so afterpushStateor adding a<base href>, relative entries load the wrong module. CONFIRMED. Repro: a16/mod1.ts ::rv16-simap-pushstate-relative,rv16-simap-base-change-relative,rv16-simap-pushstate-relative-key -
Import maps inserted with
innerHTML, which the browser never registers, are registered by the client resolver (and double-rewritten). CONFIRMED. Repro: a16/mod1.ts ::rv16-simap-innerhtml-inert -
document.open()switches off develop's new postMessagetargetOrigincheck. The check is a window listener registered once, anddocument.open()removes all window listeners. So everydocument.write-built iframe (ad slots, editor frames) then receives messages addressed to other origins. main never had the check. CONFIRMED. Repro: a14/docopengate.ts ::rv14-docopen-gate(run withoutRUNWAY_FAST) -
URL-keyed import maps break after the stale reload (bucket 1 #21, SPA reload): the mapped file is never used and the original path 404s. main never supported these maps. CONFIRMED. Repro: a15/params.ts ::
rv15-pushstate2-importmap -
pushStatealone can flip the rewriter's incumbency mode away from the client's when asiteFlagspattern matches one route and not another, so scripts lose$scramjet$registerrealm. This is the same mechanism as the mode-mismatch item. PLAUSIBLE (code reading). -
Cloned OPFS root handles reveal the per-site directory name. develop disguises the root's
nameas""only for the exact handle object it returned, so a clone (posted to or from a worker, or stored in IndexedDB) showshttp%3A%2F%2Fsite. CONFIRMED. Repro: a20/opfs.ts ::rv20-opfs-handles -
Capability delegation through
postMessage(msg, {targetOrigin, delegate: "payment"|"fullscreen"})is silently dropped. The call succeeds but nothing is delegated, and Chrome's NotSupportedError/NotAllowedError validation is skipped. main broke this differently: forcing"*"made every real delegation throw. CONFIRMED. Repro: a21/options.ts ::rv21-opts-window-postmessage(run withoutRUNWAY_FAST) -
An origin header whose name starts with
x-scramjet-is shown to the page under the unprefixed name, merged into the real one. For example,X-Scramjet-Content-Type: text/htmlnext toContent-Type: text/plainmakesget("content-type")return"text/html, text/plain". CONFIRMED. Repro: a24/spoof.ts ::rv24-originprefixed -
Nonces aren't hidden when the CSP came in a header. After insertion, Chrome returns
""fromgetAttribute("nonce")(and inouterHTML); develop returns the real value. Low: webpack, Vite and emotion read.noncefirst, and develop fixes every.noncepropagation pattern main got wrong. CONFIRMED. Repro: a25/nonce.ts ::rv25-nonce-csphdr