Skip to content

fix(runtime): lazy-loaded globals should shadow on inherited [[Set]]#34405

Merged
nathanwhit merged 3 commits into
denoland:mainfrom
nathanwhit:nathanwhit/fix-34403-lazy-global-shadow
May 27, 2026
Merged

fix(runtime): lazy-loaded globals should shadow on inherited [[Set]]#34405
nathanwhit merged 3 commits into
denoland:mainfrom
nathanwhit:nathanwhit/fix-34403-lazy-global-shadow

Conversation

@nathanwhit

Copy link
Copy Markdown
Member

Summary

Fixes #34403.

Lazy-loaded global accessors (Response, Request, ReadableStream, WebSocket, CompressionStream, ...) installed via propNonEnumerableLazyLoaded / propWritableLazyLoaded stored assigned values in a shared closure and ignored the receiver. Assigning through an object whose prototype chain includes globalThis (the whatwg-fetch / cross-fetch polyfill idiom) clobbered the global instead of creating a shadowing own property on the receiver, breaking x instanceof Response for real fetch responses and tripping up wasm-bindgen loaders such as @swc/wasm-web.

The setter now uses Object.defineProperty(this, name, ...) to define an own data property on the receiver, matching the pre-2.8 data-property [[Set]] semantics. A new core.defineGlobalProperties helper stamps the property name onto each lazy descriptor at install time so the setter knows which name to define.

After the first set, the accessor is replaced by a regular data property on whichever object received the set, so subsequent reads on that object skip the lazy loader entirely:

  • Direct assignment to globalThis: descriptor becomes a data descriptor with the assigned value (same as 2.7.x).
  • Assignment via an inheriting object: creates a shadowing own property on the inheritor; globalThis is untouched.

Test plan

  • New spec test tests/specs/run/lazy_global_polyfill_shadowing covers the polyfill pattern, the instanceof invariant, and direct globalThis assignment.
  • cargo test --test specs -- lazy_global passes.
  • Related spec tests (fetch, window) still pass.

@fibibot fibibot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. ObjectDefineProperties is still destructured in runtime/js/90_deno_ns.js, but this PR replaces its only remaining uses with core.defineGlobalProperties. deno lint runtime/js/90_deno_ns.js reports no-unused-vars, matching the red lint debug linux-x86_64 CI shard. Drop it from the primordial destructuring.
  2. core.defineGlobalProperties is exported from libs/core/01_core.js but is not declared in libs/core/core.d.ts next to propWritableLazyLoaded. Please add the declaration so the internal core type stays in sync with the exported API.

Lazy-loaded global accessors (`Response`, `Request`, `ReadableStream`,
`WebSocket`, ...) installed via `propNonEnumerableLazyLoaded` /
`propWritableLazyLoaded` used to store assigned values in a shared closure
and ignore the receiver. Assigning through an object whose prototype chain
includes `globalThis` (the `whatwg-fetch` / `cross-fetch` polyfill idiom)
clobbered the global instead of creating a shadowing own property on the
receiver, which broke `x instanceof Response` for real fetch responses.

Restore data-property [[Set]] semantics: the setter now uses
`Object.defineProperty(this, name, ...)` to define an own data property on
the receiver. A new `core.defineGlobalProperties` helper stamps the
property name onto each lazy descriptor at install time so the setter
knows what to define.

Fixes denoland#34403.
@nathanwhit
nathanwhit force-pushed the nathanwhit/fix-34403-lazy-global-shadow branch from b9d2dd0 to c2d2acb Compare May 26, 2026 23:47
Drop unused `ObjectDefineProperties` primordial destructuring in
`runtime/js/90_deno_ns.js` and add the missing `defineGlobalProperties`
type declaration in `libs/core/core.d.ts`.
@nathanwhit
nathanwhit force-pushed the nathanwhit/fix-34403-lazy-global-shadow branch from c2d2acb to ad84316 Compare May 26, 2026 23:48

@fibibot fibibot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. propNonEnumerableLazyLoaded now shadows inherited assignments with enumerable: false. That does not match ordinary [[Set]] on an inherited writable data property: g.Response = ... should create an enumerable own data property on g, while direct assignment to the own global should keep the global descriptor non-enumerable. Please branch on whether this already owns desc[lazyNameSym] (or equivalent) and add that descriptor assertion to lazy_global_polyfill_shadowing.

The previous blockers are fixed: ObjectDefineProperties is no longer unused in runtime/js/90_deno_ns.js, and core.defineGlobalProperties is declared in core.d.ts.

Inherited [[Set]] of a writable data property creates a new enumerable own
data property on the receiver. Direct [[Set]] on the holder updates the
existing descriptor in place, preserving its enumerable attribute.

The lazy global setter now branches on whether `this` already owns the
property name: if it does, the replacement descriptor keeps the original
`enumerable: false`; if not, the new own property on the receiver is
created with `enumerable: true`. Extend the spec test to assert both
descriptor shapes.

@fibibot fibibot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current head fixes my prior blocker: propNonEnumerableLazyLoaded now branches on ObjectHasOwn(this, name), so inherited g.Response = ... creates an enumerable own data property while direct globalThis.WebSocket = ... preserves the global descriptor's non-enumerability. core.defineGlobalProperties is declared in core.d.ts, and the stale ObjectDefineProperties destructuring is gone. Holding approval until CI is green.

@fibibot fibibot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI is green now, promoting prior review to APPROVE.

@fibibot

fibibot commented May 27, 2026

Copy link
Copy Markdown
Contributor

@bartlomieju this is ready to merge

@nathanwhit
nathanwhit merged commit 3cdfe7e into denoland:main May 27, 2026
136 checks passed
littledivy pushed a commit to crowlKats/deno that referenced this pull request Jun 10, 2026
…enoland#34405)

## Summary

Fixes denoland#34403.

Lazy-loaded global accessors (`Response`, `Request`, `ReadableStream`,
`WebSocket`, `CompressionStream`, ...) installed via
`propNonEnumerableLazyLoaded` / `propWritableLazyLoaded` stored assigned
values in a shared closure and ignored the receiver. Assigning through
an object whose prototype chain includes `globalThis` (the
`whatwg-fetch` / `cross-fetch` polyfill idiom) clobbered the global
instead of creating a shadowing own property on the receiver, breaking
`x instanceof Response` for real fetch responses and tripping up
wasm-bindgen loaders such as `@swc/wasm-web`.

The setter now uses `Object.defineProperty(this, name, ...)` to define
an own data property on the receiver, matching the pre-2.8 data-property
`[[Set]]` semantics. A new `core.defineGlobalProperties` helper stamps
the property name onto each lazy descriptor at install time so the
setter knows which name to define.

After the first set, the accessor is replaced by a regular data property
on whichever object received the set, so subsequent reads on that object
skip the lazy loader entirely:
- Direct assignment to `globalThis`: descriptor becomes a data
descriptor with the assigned value (same as 2.7.x).
- Assignment via an inheriting object: creates a shadowing own property
on the inheritor; `globalThis` is untouched.

## Test plan

- [x] New spec test `tests/specs/run/lazy_global_polyfill_shadowing`
covers the polyfill pattern, the `instanceof` invariant, and direct
`globalThis` assignment.
- [x] `cargo test --test specs -- lazy_global` passes.
- [x] Related spec tests (`fetch`, `window`) still pass.
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.

Lazy-loaded global accessors (Response, Request, ReadableStream, …) get clobbered when assigned through an object inheriting from globalThis

2 participants