[core] stop Intercept refusing declarations without a checkReceiver

`Intercept` threw for any declaration that converts IDL arguments and
does not pass a `checkReceiver`. No call site has ever passed one - 19
declarations on develop, zero with a receiver check - so the refusal
fired for every such interface, and `loadModules` swallowed the throw
into a `dbg.error`. Because it aborts the module function, everything
declared after the offending member went with it.

10 of the 19 declarations on develop were being refused: CookieStore,
Document.cookie, History, all five of performance.ts, StorageManager
and FileSystemHandle behind it, and Worklet. The runway failures are a
readout of exactly that set - ckjar-*, cookies-*, the pushstate tests,
platform-performance-observer, platform-resource-timing-own-entries -
none of which fail on 060552a4, before #110 landed.

The ordering it guarded is real: Web IDL brand-checks the receiver
before converting arguments, and conversion runs page code. But the
predicate asked whether a member declares IDL arguments at all, not
whether its conversion can run any page code, so `@Arguments()` with no
types was refused too. And a promise-only interface like Cache or
CookieStore has no member that can satisfy the contract at all, since
they reject rather than throw. Refusing to install is much the worse
outcome: for the storage-shaped modules it means no origin scoping,
which is a cross-origin leak traded for a fingerprint.

#117 tracks the real fix - a predicate that asks whether a conversion
can run page code, an async brand check, and a build-time failure.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_018Zn7Pst62DZXqzGyrkQ68o
This commit is contained in:
velzieandClaude Opus 5 committed 2026-09-22 13:22:44 -04:00
1 parent 785b81ca32
commit e438c4db13
1 file changed
+20 -24
@@ -1177,11 +1177,17 @@ return { apply, construct };
/**
* `checkReceiver` must synchronously invoke a side-effect-free native getter
* or method on the receiver, throwing for an invalid receiver. It is required
* for instance members with IDL arguments: Web IDL checks the receiver before
* converting arguments. For example, Headers can use the saved `has` method
* with a fixed valid name; Blob can use its saved `size` getter. Merely
* constructing a `client.native` wrapper does not check anything.
* or method on the receiver, throwing for an invalid receiver. Web IDL checks
* the receiver before converting arguments, and conversion runs page code, so
* an instance member with IDL arguments wants one. For example, Headers can
* use the saved `has` method with a fixed valid name; Blob can use its saved
* `size` getter. Merely constructing a `client.native` wrapper does not check
* anything.
*
* Wanted, not required: omitting it leaves the ordering imprecise rather than
* refusing the declaration, because a promise-only interface has no member
* that can satisfy the contract and the interfaces that do are better off
* installed. See #117.
*/
Intercept(handler: any, checkReceiver?: (receiver: any) => void): void {
const foreignbaseclass = Object_getPrototypeOf(handler);
@@ -1198,25 +1204,15 @@ return { apply, construct };
Object_getOwnPropertyDescriptors(handler.prototype);
const staticDescs: Record<string | symbol, PropertyDescriptor> =
Object_getOwnPropertyDescriptors(handler);
// Refuse an unsafe declaration before installing any of its members.
if (!checkReceiver) {
for (const descs of isglobal
? [prototypeDescs, staticDescs]
: [prototypeDescs]) {
for (const key of Reflect_ownKeys(descs)) {
const desc = descs[key];
if (isConstructorMember(desc.value)) continue;
if (
memberValidator(this.box, desc.value) ||
memberValidator(this.box, desc.set, true)
) {
throw new Error(
`Intercept(${classname}.${String(key)}) requires a native receiver check before IDL conversion`
);
}
}
}
}
// A declaration that converts IDL arguments without a `checkReceiver`
// used to be refused here. Nothing has ever passed one, so the refusal
// threw for every such interface and `loadModules` swallowed it into a
// `dbg.error` - silently uninstalling cookie, CookieStore, history,
// performance and opfs, which is a far worse outcome than the argument
// conversion ordering it was guarding. See #117 for the real fix: a
// predicate that asks whether a conversion can run page code at all, an
// async brand check for the promise-only interfaces, and a build-time
// failure rather than a runtime one.
// create a fake parent prototype for the handler, so that `super.method()` calls resolve to the native store versions
const fakePrototype = {};