Skip to content

fix(runtime): a #private method or [Symbol.iterator] alias is not 'deleted' after an unrelated prototype mutation (#11692) - #11697

Merged
proggeramlug merged 3 commits into
mainfrom
fix/11692-private-and-symbol-method-deleted
Sep 30, 2026
Merged

proggeramlug merged 3 commits into
mainfrom
fix/11692-private-and-symbol-method-deleted

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Closes #11692

Cause

#11667 deleted CLASS_DELETED_KEYS and replaced it with class_proto_key_deleted: a declared prototype member counts as deleted when the per-name prototype guard is invalidated and the member's string key is missing from the materialized decl prototype object. Two kinds of declared member never have that string key:

  • a #private method (#validateOptions). It is not a property at all.
  • the synthetic @@iterator / @@asyncIterator / @@toPrimitive / dispose / inspect alias of a well-known-symbol method. It lives under its symbol key.

So once any Object.setPrototypeOf ran anywhere, every guard was invalidated and these members were treated as deleted.

  • redis: node-redis's this.#validateOptions(...) in new RedisClient threw #<perry:private-member:…> is not a function. It is called through the class extends BaseClass {} subclass that attachConfig builds.
  • mongodb: mongodb-connection-string-url re-parents whatwg-url's URLSearchParams with Object.setPrototypeOf. After that, Array.from(impl) threw @@iterator is not a function in parseOptions / new MongoClient.

The issue suspected static methods and a static iterator. The actual trigger is the prototype mutation, not statics.

Reduced (fails on main, passes here):

class Impl { _l = ["x"]; [Symbol.iterator]() { return this._l[Symbol.iterator](); } }
class SP {} class CI extends SP {}
Object.setPrototypeOf(new SP(), CI.prototype);
Array.from(new Impl()); // main: TypeError: @@iterator is not a function

Fix

class_proto_key_deleted now answers "not deleted" for members that have no string key on the prototype:

  • #-prefixed names;
  • the internal symbol-dispatch aliases, unless the class registered a string-member order for that name. A source method literally named "@@iterator" is a real key and can still be deleted, and the test covers that.

#11667's design is kept: deletion is still derived from the object, and no table comes back.

Tests

  • test_gap_class_private_method_after_prototype_mutation.ts
  • test_gap_class_symbol_iterator_after_prototype_mutation.ts

Both fail on pristine main 23d5634 and pass with the fix. Output matches Node 26.5.1 (/opt/node-v26.5.1-linux-x64).

Validation (perrymaster, Linux x86_64, --release cgu16, PERRY_SKIP_BUILD=1 PERRY_NO_AUTO_OPTIMIZE=1)

From-source tests through run_parity_tests.sh --filter:

test pristine main this PR
test_gap_node_redis_from_source PARITY_FAIL PASS
test_gap_mongodb_from_source PARITY_FAIL PASS

Gap subsets vs pristine main (the base arm is main 23d5634):

subset main this PR notes
class 133/136 135/136 the one remaining failure, test_gap_2159_defineproperty_class_prototype, fails on both arms
static 48/48 48/48
private 12/13 13/13
iterator 24/25 25/25

Zero regressions. #11667's own fixtures (test_gap_class_static_*, delete_redefine, computed_static_method, same_name_scopes) pass. After the rebase onto 6a50907, the two new tests were re-run and pass.

Other checks:

Harness note

Run by hand, the harness can report these from-source tests as a compile error for two reasons that are separate from this bug:

Not run

  • Codegen and HIR tests: those crates are untouched.
  • An instruction-count A/B: the changed function is reached only after the prototype guards have been invalidated, not on a guard hit, and it adds one byte compare and one matches!.
  • A full gap sweep.
  • Windows (cargo xwin).

Summary by CodeRabbit

  • Bug Fixes
    • Private instance and static methods, along with well-known-symbol methods such as iterators, are no longer incorrectly treated as deleted after an unrelated prototype change.
    • Ordinary prototype methods can still be deleted, and a source method named "@@iterator" remains distinct from the symbol-based iterator.
    • Fixed errors affecting class behavior in scenarios encountered with node-redis and mongodb.

@coderabbitai

coderabbitai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 6f06af72-0a7c-48be-9cf6-bbeae445ac7e

📥 Commits

Reviewing files that changed from the base of the PR and between 6a50907 and 6dea03f.

📒 Files selected for processing (4)
  • changelog.d/11697-private-and-symbol-method-not-deleted.md
  • crates/perry-runtime/src/object/class_registry/state.rs
  • test-files/test_gap_class_private_method_after_prototype_mutation.ts
  • test-files/test_gap_class_symbol_iterator_after_prototype_mutation.ts

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

The runtime no longer treats private names and unregistered symbol-dispatch aliases as deleted string-keyed prototype members. Regression tests cover private methods, iterator lookup, and deletion of ordinary and literal "@@iterator" methods after prototype mutations.

Changes

Prototype key lookup

Layer / File(s) Summary
Prototype key classification
crates/perry-runtime/src/object/class_registry/state.rs, changelog.d/11697-private-and-symbol-method-not-deleted.md
class_proto_key_deleted now returns false when a member has no reflective string key. Private names and unregistered symbol-dispatch aliases are excluded; registered aliases remain eligible as literal string keys.
Mutation regression coverage
test-files/test_gap_class_private_method_after_prototype_mutation.ts, test-files/test_gap_class_symbol_iterator_after_prototype_mutation.ts
Tests cover private instance and static methods, construction and error behavior, iterator lookup before and after prototype mutation, and deletion of ordinary and literal string-keyed methods.

Priority: ➖ Normal

Estimated code review effort: 2 (Simple) | ~12 minutes

Change: Bug fix · Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to 6dea0

The change restores private-method and iterator lookup after prototype mutation while preserving ordinary string-method deletion. No actionable merge-blocking issue is identified; merge after normal checks pass.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 6dea0

The fix restores valid private and symbol method calls, but its name-based exception may also leave an ordinary deleted method callable. No broader privilege gain or cross-service exposure was established.

Retained concerns

  • Low · architecture · inferred: The unconditional # prefix exception does not distinguish a private member from an ordinary string-keyed method such as "#m". After deletion removes the reflective property, the changed predicate can still permit lookup of its retained method-table entry. This weakens deletion as a means of removing an exposed operation; application-specific security impact was not established.
Security review details

Security Blast Radius

  • inferred — The supported exposure is method dispatch within a running program: a caller able to supply a matching method-name string may reach a retained entry. The inspected path does not establish additional operating-system authority, tenant access, or service reachability.

Security Findings and Attack Paths

  • inferred — A possible failure path is deletion of an ordinary "#m" prototype property followed by public method-name dispatch: the retained entry is found, the prefix exception reports it as not deleted, and invocation proceeds. No sensitive application sink or executable reproduction was established.

Trust Boundaries and Controls

  • observed — Public prototype dispatch and dedicated private dispatch share inherited method lookup. The public branch shown invokes without the dedicated private-brand call path. Whether private storage names are excluded before entering that public lookup remains unresolved; the base predicate already allowed lookup while guards were intact, so this is not established as a newly introduced private-access bypass.

Resilience and Maintainability Implications

  • observed — Generic symbol deletion clears symbol-property storage, while class symbol registration also creates separate class-method and synthetic-alias entries. The inspected deletion route does not establish cleanup of those entries. Representation-specific terminal behavior and its base-to-head security significance remain unresolved.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 3 files. (1 skipped: 1… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the runtime fix for private methods and well-known-symbol aliases after prototype mutation.
Description check ✅ Passed The description provides the cause, fix, related issue, regression tests, validation results, and known limitations. It does not use every template heading or include the checklist, but it contains th…
Linked Issues check ✅ Passed The PR meets the coding requirements in [#11692]. class_proto_key_deleted now ignores missing string keys for #private members and synthetic well-known-symbol aliases. It still detects deletion fo…
Out of Scope Changes check ✅ Passed The changes stay within [#11692]. The runtime change fixes the reported deleted-key classification. The added tests reproduce the private-method and iterator regressions and protect deletion behavior.…
Full details: Docstring Coverage

Explanation

Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 3 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@proggeramlug
proggeramlug merged commit 5ac0972 into main Sep 30, 2026
53 of 60 checks passed
@proggeramlug
proggeramlug deleted the fix/11692-private-and-symbol-method-deleted branch September 30, 2026 07:52
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.

regression from #11667: redis and mongodb from-source gap tests fail (static #private method / static iterator lookup)

1 participant