Skip to content

Fix Proxy getPrototypeOf/setPrototypeOf traps with null prototypes#2668

Merged
lahma merged 1 commit into
sebastienros:mainfrom
lahma:proxy-null-prototype
Jul 13, 2026
Merged

Fix Proxy getPrototypeOf/setPrototypeOf traps with null prototypes#2668
lahma merged 1 commit into
sebastienros:mainfrom
lahma:proxy-null-prototype

Conversation

@lahma

@lahma lahma commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Two latent bugs in the prototype traps, both around JS null vs CLR null:

  1. Engine crash: JsProxy.GetPrototypeOf cast the trap result straight to ObjectInstance, so a spec-legal null-returning trap crashed with InvalidCastException instead of returning null:
    Object.getPrototypeOf(new Proxy({}, { getPrototypeOf: () => null })) // crash → now: null
  2. Spurious TypeError: the non-extensible-target invariant compared the trap result (JsNull) against _target.Prototype (CLR null) with ReferenceEquals, so a null-prototype non-extensible target + null trap result wrongly threw; same pattern in SetPrototypeOf broke Object.setPrototypeOf(proxy, null) for non-extensible null-proto targets. Per spec the comparison is SameValue, which succeeds when both are null.

Fix: normalize the JS value to ObjectInstance? once after the object-or-null validation, then compare/return that. Trap fetch order, invariant semantics and messages unchanged.

test262 never exercises a null trap result in these paths (its getPrototypeOf suite only returns objects — verified file-by-file), hence latent. Expectations cross-checked against node 24 / V8. Negative validation: without the fix, the new tests reproduce the exact InvalidCastException.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EsHuKuE4UihKZp5HYapDHE

@lahma
lahma enabled auto-merge (squash) July 13, 2026 12:53
@lahma
lahma force-pushed the proxy-null-prototype branch from d6983fa to 99edcc8 Compare July 13, 2026 13:23
Two latent bugs in JsProxy's prototype traps when JS null is involved:

1. [[GetPrototypeOf]]: after correctly validating that the trap result
   is object-or-null, the code did `(ObjectInstance) handlerProto` --
   when a spec-legal trap returns null (`getPrototypeOf: () => null`),
   handlerProto is the JsNull singleton (not an ObjectInstance), so the
   extensible-target path crashed with InvalidCastException. On the
   non-extensible path, `ReferenceEquals(handlerProto, _target.Prototype)`
   compared JsNull against a CLR-null Prototype -> false -> spurious
   TypeError even though both are conceptually null (SameValue true).

2. [[SetPrototypeOf]]: same ReferenceEquals shape --
   `Object.setPrototypeOf(proxy, null)` on a non-extensible target whose
   prototype is already null compared JsNull against CLR null and threw
   a spurious TypeError where the spec (SameValue(V, targetProto))
   requires success.

Fix: normalize the JsNull sentinel to CLR null once, then compare/return
that. All script-visible callers of SetPrototypeOf (Object.setPrototypeOf,
Reflect.setPrototypeOf, __proto__ setter, __proto__ in object literals)
already validate object-or-null before calling, so the `as` cast is safe.

test262 does not cover trap-returns-null on these paths, which is why
this stayed latent. All five expectations cross-checked against Node
v24.18.0 (null / null / true null / true / TypeError on non-extensible
mismatch). New tests verified to fail (3/5, incl. the
InvalidCastException repro) without the fix; regression tests confirm
the non-extensible mismatch invariant still throws.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EsHuKuE4UihKZp5HYapDHE
@lahma
lahma force-pushed the proxy-null-prototype branch from 99edcc8 to d55d58b Compare July 13, 2026 13:51
@lahma
lahma merged commit 355f439 into sebastienros:main Jul 13, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant