Repository navigation
fix(signals): a removed own __proto__ reads undefined, not Object.prototype (#4672) - #4676
Conversation
… not the prototype (#4672) A store whose own `__proto__` (or `constructor`) data was removed told the key's node the backing's inherited value, so the hot read served `Object.prototype` wrapped as a store (or `Object`) to tracked and untracked readers. Core R30: inherited pollution keys are never served. Now the node is told `undefined`, the same answer a store that never had the key gives: - notifyWrites (setter delete, a projection's draft delete): an absent pollution key's node gets `undefined`. An own key pays nothing, because the existing hasOwn result is reused. - reconcile's removed-key branch (reconcile, and a derive's returned value): a pollution key diffs against an empty record. - the get trap's existing pollution-key branch: a tracked reader subscribes like any absent key (R12), and its node is born absent. Own data that lands later reaches the reader (F-10). `__proto__` assignment stays inert (R30). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0117BoWNSSgtemsSYsaX2hXE
Size (brotli, eager entry chunk)
❌ Fails the gate
Bundled with Rolldown (what Vite ships), brotli q11, decimal KB. A scenario fails only when it is over its brotli cap and its minified size is more than 20 B over the minified recorded with the cap; over the cap within that allowance is brotli layout noise and passes with a warning. Caps and their recorded minified in |
Coverage Report for CI Build 41742835001Coverage remained the same at 78.249%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
Merging this PR will not alter performance1 warning
Comparing Footnotes
|
…-removal # Conflicts: # packages/signals/docs/RULES-INDEX.md
With #4674 on next, a plain read of an inherited `constructor` returns the source's class, and a removed own `constructor` already reads it, as an object that never had one does. Ruling: narrow the fix to `__proto__`. - notifyWrites and reconcile's removed-key branch tell a removed `__proto__` node `undefined`. An own key does no extra work in notifyWrites (the existing hasOwn result is reused), and the reconcile check sits inside the existing removed-key branch. - Dropped: the get-trap subscription and getNode's `absent` parameter (the reverse direction is pinned `it.fails`), and every constructor-specific change and test, apart from one pin that a removed own `constructor` reads the inherited class, and a pin that `prototype` reads undefined. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0117BoWNSSgtemsSYsaX2hXE
🦋 Changeset detectedLatest commit: 4fbe2a7 The changes in this PR will be included in the next version bump. This PR includes changesets to release 15 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
The size Fix PRs don't edit The CodSpeed report on the earlier head (f2c311d) flagged projection-root-write NESTED and dbmon partial tick. Both are known noisy benches, and that head also touched the get trap. The current head leaves the get trap untouched. Generated by Claude Code |
Claude-Session: https://claude.ai/code/session_0117BoWNSSgtemsSYsaX2hXE Co-authored-by: Claude <noreply@anthropic.com>
Fixes #4672.
Summary
A store can carry own
__proto__data, for example rows parsed from JSON. After that data was removed,store.__proto__readObject.prototypewrapped as a store, with 12 own keys, both tracked and untracked. The removal had told the key's node the backing's inherited value (pb.__proto__/incoming.__proto__), and the get trap's hot path serves node values.Scope follows the ruling after #4674, which is now on next. The fix covers
__proto__only. With #4674, a removed ownconstructoralready reads the inherited class, the same answer as an object that never had one. Ownprototypedata never leaked. Both keys get one pin test each, and no code.The removed
__proto__node is now toldundefinedat the two places a removal is notified:notifyWrites(a setterdelete, a projection's draft delete). It reuses thehasOwn(pb, key)result the accessor probe already computes, so an own key does no extra work. Only a key that is not own is compared against"__proto__".reconcile, and a projection's or derived store's returned value). Inside the existing!hasOwn(incoming, key)branch, a removed__proto__diffs against an empty null-prototype record.The get trap is unchanged, so ordinary-key reads do no extra work.
Rules checked
__proto__assignment inert): an inherited__proto__is never served. After removal the read isundefined, andObject.prototypeis never wrapped or served. Assigning__proto__stays inert after removal, and a later write pollutes nothing.undefinedin the same flush that untracked reads see it.__proto__ = undefinedis inert under R30, so the own data stays.__proto__data lands): the untracked read shows the data. A tracked reader is not notified, because the get trap answers an absent__proto__before it subscribes. This was already the case on next. Fixing it costs about +43 B minified (rows below), so it is pinnedit.failsand left out of this PR.Sizes (
node size.mjs --no-gate, against next d5cf231)node gate.mjsfails on createStore, compiled CSR and compiled hydrating. next already sits over the brotli cap on those three rows (after #4674) and passes only on the minified allowance: 15 B, 7 B and 15 B. Every variant below exceeds that allowance, so this PR needs a cap re-record whichever form ships. The hello-world compiled floor is unchanged at 9,977 B.Variants measured on the same base. Brotli swings with mangling, so minified is the stable figure:
__proto__only (kept separate from the existing check)Perf
The change adds no work to the get or set trap. In
notifyWritesan own key takes the same path as before: thehasOwnthat was already computed is reused. The local A/B on built dist for the first revision (4 alternating rounds, every store bench includingreconcile-dbmon-ab) showed no consistent regression (median Δ −2.1%, noise about ±15% on a shared machine). That revision also touched the get trap, so it was a superset of this one. It was not re-run for this narrowed form.How did you test this change?
packages/signals/tests/store/proto-key-removal-4672.test.ts, 19 tests: 18 pass and 1 is an expected fail (the reverse-tracked pin). 7 fail on next. Covered:undefinedis inert, and a write after removal is inert with no pollution;constructorreadsObject, and a removed ownprototypereadsundefined.server.fs.allowoverride because the worktree's symlinkednode_modulesresolves outside the vite root.RULES-INDEX.mdregenerated, patch changeset updated.🤖 Generated with Claude Code
https://claude.ai/code/session_0117BoWNSSgtemsSYsaX2hXE