Skip to content

fix(hir,runtime): late-assigned url.URL reads undefined; non-special URL href gains a trailing slash - #11325

Merged
proggeramlug merged 3 commits into
mainfrom
fix/11322-url-let-assign
Sep 25, 2026
Merged

proggeramlug merged 3 commits into
mainfrom
fix/11322-url-let-assign

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #11322

Root causes

1. let u; u = new URL(s) reads undefined for every property (with import { URL } from "url").
Native-instance tagging for new C(...) existed twice: once for let/const/var initializers
(destructuring/var_decl/native_new.rs) and once for plain assignment (lower/expr_assign.rs).
The initializer copy deliberately excludes the heap-object classes (url URL/URLSearchParams,
util TextEncoder/TextDecoder) and the handle-backed constructors (StringDecoder,
DiffieHellman*), and resolves aliased imports. The assignment copy tagged any new of an
imported native-module export. u was therefore registered as a url native instance, and
u.hostname lowered to a receiver-bound NativeMethodCall { module: "url", method: "hostname" }.
That call returns undefined on the plain heap URL object. The const form lowers to a plain
PropertyGet and works. This also broke let u = new URL(a); u = new URL(b) (reassignment),
var, let u: any, typed let u: URL, branch assignment, and module-level let x; x = new URL().

Fix: one shared classifier, native_instance_for_new(ctx, &NewExpr), now in native_new.rs.
The initializer arm, the await new arm and the assignment arm all use it, so the two binding
forms can no longer disagree. Side effects:

  • The await new C() arm now gets alias resolution, the new mod.Class() callee form, and the
    DisposableStack fallback, matching the non-await arm.
  • Assignment now also handles the new mod.Class() callee and the global-name fallback
    (EventEmitter, DisposableStack, …), exactly as declarations do.

2. new URL("iLoveJS://127.0.0.1:1234").href gets a trailing /.
parse_url seeded the pathname with / for every authority-bearing URL that has no path. Under
WHATWG, only special schemes (http, https, ws, wss, ftp; file has its own branch) do
that. A non-special scheme such as foo://a:5 or mongodb://db:27017 has an empty path:
pathname === "", and href/toString() have no trailing slash. The search/hash setters
rebuild href from the same fields, so they are fixed too (foo://a:5?z=1, not foo://a:5/?z=1).

Tests

  • test-files/test_gap_url_late_assign_non_special_path.ts has two parts. The first is a
    late-assignment matrix: URL via let/var/let: any/let: URL/reassign/try/branch/namespace
    import/module-level, plus URLSearchParams, TextEncoder/Decoder, StringDecoder, Map, Date, RegExp,
    Buffer, a class ctor local and a class field. The second is a URL serialization matrix of 22
    special and non-special inputs plus the setters.
    • With Node 26.5.1 as the oracle, the real harness (PERRY_SKIP_BUILD=1 PERRY_NO_AUTO_OPTIMIZE=1)
      gives PARITY_FAIL on the branch point (182e375) and PASS with the fix.
    • Auto-optimize mode, compiled by hand with the fix, is byte-identical to Node.
  • crates/perry-hir/tests/late_assigned_native_new.rs (new suite): the late-assigned URL,
    URLSearchParams and TextEncoder must not lower to url/util NativeMethodCalls. It fails on
    the branch point and passes with the fix. A control checks that a genuine native class (aliased
    net.BlockList) is still tagged.
  • perry-runtime url:: unit tests: 17 passed, including the new pathname special/non-special test.
  • A/B, same perry-dev build and same -p set for both arms: 35 related non-ext-routed gap tests
    (url, assign/uninit/late, disposable, async_local/asyncresource, …). Only the new test changed
    (DIFF → PASS); the other 34 pass on both arms. EventEmitter late assignment (plain and events.
    namespace) was checked by hand against Node on the fix build.
  • cargo test -p perry-hir: everything passes except unimplemented_api_check's two
    every_supported_module_rejects_bogus_* tests (sqlite/test/sea). Those fail identically with
    the branch-point HIR sources, so they are pre-existing and unrelated.
  • cargo check -p perry-hir -p perry-runtime --all-targets: no warnings. cargo fmt --check passes.
  • SKIP_COMPILE_GATES=1 scripts/run_lint_gates.sh (Linux): 89 of 91 script gates pass; the
    compile tier was not run. The 2 failures:
    • public-baseline freshness (grandfathered red);
    • cargo xwin check: cargo-xwin is not installed on the Linux host. This diff has no
      cfg(windows) code.

Not run

  • The full gap sweep.
  • The ext-routed (events/net/http/…) gap tests under the harness: its prebuilt-archive mode
    can't serve them for a baseline arm.
  • The Windows xwin type-check.
  • An instruction-count A/B. No codegen hot path changed: the change is HIR classification plus
    the URL constructor's parse.

Summary by CodeRabbit

  • Bug Fixes
    • Corrected URL parsing so authority-based URLs without an explicit path have an empty pathname for non-special schemes. Special schemes such as HTTP and HTTPS continue to receive /.
    • Fixed handling of built-in values assigned after their variable is declared, including URL-related classes, so their properties and methods behave as expected.

@proggeramlug

Copy link
Copy Markdown
Contributor Author

Ready to merge once CI is clean. Fixes #11322, which unblocks mongodb 7.0.0. x = new C() assignments used a second copy of the native-instance classifier that tagged plain-object classes like URL as native, so let u; u = new URL() read undefined for every property. The declaration, await new and assignment paths now share one classifier. The URL parser also no longer adds a / path for non-special schemes (WHATWG). The gap test fails on main and matches Node; the HIR test and a 35-test A/B show no other changes.

@coderabbitai

coderabbitai Bot commented Sep 25, 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: 4946fa34-45ef-4e67-bad6-7231bd6ea9b6

📥 Commits

Reviewing files that changed from the base of the PR and between 3c50eba and 44391ce.

📒 Files selected for processing (9)
  • changelog.d/11325-url-late-assign-non-special-path.md
  • crates/perry-hir/src/destructuring/mod.rs
  • crates/perry-hir/src/destructuring/var_decl.rs
  • crates/perry-hir/src/destructuring/var_decl/native_new.rs
  • crates/perry-hir/src/lower/expr_assign.rs
  • crates/perry-hir/tests/late_assigned_native_new.rs
  • crates/perry-runtime/src/url/mod.rs
  • crates/perry-runtime/src/url/parse.rs
  • test-files/test_gap_url_late_assign_non_special_path.ts

Included review availability: Your plan provides up to 8 included reviews per hour; 2 remain after this review.


📝 Walkthrough

Walkthrough

The change shares native-instance classification across initializers and later assignments. It also changes parsing for authority URLs without paths: only special schemes receive a default /.

Changes

Late-assigned native instances

Layer / File(s) Summary
Shared native-instance classification
crates/perry-hir/src/destructuring/...
Direct and awaited new initializers use a shared classifier. It resolves imported aliases, excludes specified classes, and recognizes identifier and module-member constructors.
Assignment registration and regression tests
crates/perry-hir/src/lower/expr_assign.rs, crates/perry-hir/tests/late_assigned_native_new.rs, test-files/test_gap_url_late_assign_non_special_path.ts
Assignments from new expressions use the shared classifier. Regression tests cover late-assigned URL and util values and retained native dispatch for net.BlockList. The TypeScript test exercises additional built-in values and URL serialization.

Authority URL paths

Layer / File(s) Summary
Authority URL path handling
crates/perry-runtime/src/url/parse.rs, crates/perry-runtime/src/url/mod.rs, changelog.d/11325-url-late-assign-non-special-path.md
Authority URLs without an explicit path receive / for http, https, ws, wss, and ftp. Other schemes receive an empty path. Tests cover both cases and explicit / paths.

Priority: ⬆️ High

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix · Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to 44391

The described URL fixes are mergeable after normal checks; no actionable issue is established by the supplied evidence.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to 44391

The URL change is narrowly scoped, but newly recognized constructor assignments can retain a native identity after the variable receives a different value. That could send later method calls through the wrong dispatch path.

Retained concerns

  • Medium · security · inferred: Newly recognized constructor assignments inherit binding-level native tags that persist after the binding’s value changes. A later property or method access can therefore select native dispatch using an identity that no longer describes its receiver.
Security review details

Security Blast Radius

  • inferred — The widened native-tagging surface is compiled code using newly recognized constructor assignment forms. The available evidence does not establish an independently reachable service endpoint or a cross-tenant scope.

Security Findings and Attack Paths

  • inferred — After a recognized native-constructor assignment, a non-native reassignment or divergent branch can leave a stale binding tag and direct subsequent accesses toward the former native class. Whether a particular program turns that mismatch into a privileged operation or exploitable failure is unverified.

Trust Boundaries and Controls

  • observed — The constructor classifier checks imported identity, excludes specified plain-object and handle-backed classes, and limits member-form tagging to recognized native classes. Those controls narrow classification, but do not invalidate a tag when the assigned value later changes.

Resilience and Maintainability Implications

  • inferred — Binding-keyed registration contains same-name scope collisions, but without value-state invalidation it cannot by itself preserve correct native dispatch across repeated assignments or branch joins.

Hardening Proposals

  • proposed — Make native dispatch identity valid across non-native reassignment and branch joins, and exercise newly recognized constructor forms followed by replacement, divergence, and caught construction failure.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.59% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 29 functions across 8 files. (1 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR addresses the coding requirements in [#11322]. native_instance_for_new is shared by declaration, await new, and assignment lowering, including aliased imports. The assignment path therefore…
Out of Scope Changes check ✅ Passed The changed files support [#11322]. The classifier refactoring, re-exports, HIR tests, URL parser change, runtime test, gap test, and changelog entry all implement or verify the two linked objectives.…
Title check ✅ Passed The title clearly summarizes both primary fixes: consistent late-assigned native-instance handling and removal of unwanted trailing slashes for non-special URLs.
Description check ✅ Passed The description is comprehensive. It explains the root causes, implementation, linked issue, test coverage, verification results, and known unrelated failures. It does not use every template heading o…
Full details: Docstring Coverage

Explanation

Docstring coverage is 27.59% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 29 functions across 8 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

Copy link
Copy Markdown
Contributor Author

Merge queue: this head has two CI runs. The cancelled duplicate (36141941822) accounts for the unexpanded gc-stress/gap-suite matrix names; the real run (36141945922) failed only the owner-grandfathered lint step plus pr-gate. Stacked on current main with #11331: lint, a strict workspace compile, tokio inventory and the perry suite (1203/0) are clean.

@proggeramlug
proggeramlug merged commit 801716c into main Sep 25, 2026
73 of 104 checks passed
@proggeramlug
proggeramlug deleted the fix/11322-url-let-assign branch September 25, 2026 16:41
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.

new URL() from import { URL } from "url" assigned to a let declared without initializer reads undefined properties (breaks mongodb 7.0.0 HostAddress)

1 participant