Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions changelog.d/10634-asynclocalstorage-heritage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
### Fixed

- **`class X extends AsyncLocalStorage` threw at `super()` unless the
heritage was a bare `import { AsyncLocalStorage } from
"node:async_hooks"` binding** (#10625), the same defect #10621 fixed for
`AsyncResource` (#10453). A local alias (`const Alias =
AsyncLocalStorage`), a namespace member (`ah.AsyncLocalStorage`), a
default import (`import ahDefault from "node:async_hooks";
ahDefault.AsyncLocalStorage`), and a CJS destructured
`require('node:async_hooks')` all threw `Class constructor
AsyncLocalStorage cannot be invoked without 'new'`. Only the bare import
shape was recognized statically at HIR-lowering time
(`canonical_native_parent_name`,
`crates/perry-hir/src/lower_decl/class_decl.rs`), routing to
perry-stdlib's `js_async_local_storage_subclass_init` via a
codegen-declared extern symbol; every other shape fell through
`js_fetch_or_value_super`
(`crates/perry-runtime/src/object/global_this/fetch_globals.rs`) to a
plain CALL of the bound `async_hooks` export, which throws by design
without `new`. Unlike `AsyncResource`, whose implementation lives
entirely in perry-runtime, `AsyncLocalStorage`'s subclass-init helper
lives in perry-stdlib (it needs the stdlib `Handle` registry), and
perry-runtime cannot depend on perry-stdlib. Fixed by adding
`JS_NATIVE_ASYNC_LOCAL_STORAGE_SUBCLASS_INIT` /
`js_set_native_async_local_storage_subclass_init`
(`crates/perry-runtime/src/value/{tags,handle}.rs`), a registration hook
perry-stdlib installs at startup, matching the existing
`JS_NATIVE_ASYNC_HOOKS_CONSTRUCT` / `JS_NATIVE_EVENTS_CONSTRUCT` pattern
already used for this exact kind of cross-crate reach.
`bound_native_callable_module_and_method` needed no changes — it already
generically resolves any bound native export; only the per-consumer
match arm in `js_fetch_or_value_super` was missing for
`AsyncLocalStorage`.
12 changes: 6 additions & 6 deletions crates/perry-runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -361,12 +361,12 @@ pub use value::{
pub use value::{
js_set_handle_array_get, js_set_handle_array_length, js_set_handle_call_method,
js_set_handle_object_get_property, js_set_handle_to_string, js_set_handle_typeof,
js_set_native_async_hooks_construct, js_set_native_bun_tcp_dispatch,
js_set_native_crypto_dispatch, js_set_native_domain_dispatch, js_set_native_events_construct,
js_set_native_events_dispatch, js_set_native_http_dispatch, js_set_native_module_js_loader,
js_set_native_net_dispatch, js_set_native_querystring_dispatch, js_set_native_sqlite_dispatch,
js_set_native_tls_dispatch, js_set_native_webcrypto_dispatch, js_set_native_zlib_dispatch,
js_set_new_from_handle_v8,
js_set_native_async_hooks_construct, js_set_native_async_local_storage_subclass_init,
js_set_native_bun_tcp_dispatch, js_set_native_crypto_dispatch, js_set_native_domain_dispatch,
js_set_native_events_construct, js_set_native_events_dispatch, js_set_native_http_dispatch,
js_set_native_module_js_loader, js_set_native_net_dispatch, js_set_native_querystring_dispatch,
js_set_native_sqlite_dispatch, js_set_native_tls_dispatch, js_set_native_webcrypto_dispatch,
js_set_native_zlib_dispatch, js_set_new_from_handle_v8,
};

// Extension pump registration — allows extensions to register pump functions
Expand Down
31 changes: 31 additions & 0 deletions crates/perry-runtime/src/object/global_this/fetch_globals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,37 @@ pub unsafe extern "C" fn js_fetch_or_value_super(
crate::async_hooks::js_async_resource_subclass_init(this_box, type_value, options);
return undef;
}
// #10625: `class X extends AsyncLocalStorage` reached indirectly (local
// alias, namespace member, CJS destructured `require()`) hits the same gap
// #10453/#10621 fixed for AsyncResource: only the canonical bare
// `import { AsyncLocalStorage } from "node:async_hooks"` binding is
// recognized statically at HIR-lowering time
// (`crates/perry-hir/src/lower_decl/class_decl.rs`), which routes to
// perry-stdlib's `js_async_local_storage_subclass_init` via a
// codegen-declared extern symbol
// (`crates/perry-codegen/src/expr/this_super_call.rs`). Every other
// heritage shape resolves `parent_val` to the identical bound native
// export here, but this crate cannot call that stdlib helper directly —
// perry-runtime cannot depend on perry-stdlib, where the helper (and the
// `Handle` registry backing it) live — so route through the registration
// hook perry-stdlib installs at startup instead, exactly like the WASI arm
// above.
if bound_native_parent
.as_ref()
.is_some_and(|(module, method)| {
super::super::native_module::normalize_native_module_alias(module.as_str())
== "async_hooks"
&& method.as_str() == "AsyncLocalStorage"
})
{
let ptr = crate::value::JS_NATIVE_ASYNC_LOCAL_STORAGE_SUBCLASS_INIT
.load(std::sync::atomic::Ordering::SeqCst);
if !ptr.is_null() {
let dispatch: crate::value::JsNativeAsyncLocalStorageSubclassInitFn =
std::mem::transmute(ptr);
return dispatch(this_box);
}
}
// `class X extends Temporal.<Type>` (non-spread `super(a, b)`): a Temporal
// constructor returns a fresh NaN-boxed cell and does NOT mutate the
// implicit `this`, so the ordinary dispatch below would drop that cell and
Expand Down
11 changes: 11 additions & 0 deletions crates/perry-runtime/src/value/handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,17 @@ pub extern "C" fn js_set_native_async_hooks_construct(func: JsNativeEventsConstr
JS_NATIVE_ASYNC_HOOKS_CONSTRUCT.store(func as *mut (), Ordering::SeqCst);
}

/// Register the AsyncLocalStorage subclass-init dispatcher. See
/// `JS_NATIVE_ASYNC_LOCAL_STORAGE_SUBCLASS_INIT` for why perry-runtime needs
/// this indirection instead of calling perry-stdlib's
/// `js_async_local_storage_subclass_init` directly. (#10625)
#[no_mangle]
pub extern "C" fn js_set_native_async_local_storage_subclass_init(
func: JsNativeAsyncLocalStorageSubclassInitFn,
) {
JS_NATIVE_ASYNC_LOCAL_STORAGE_SUBCLASS_INIT.store(func as *mut (), Ordering::SeqCst);
}

/// Set the native module JS property loader (called by perry-jsruntime)
/// This callback loads a native module via V8 and gets a property from it.
#[no_mangle]
Expand Down
33 changes: 18 additions & 15 deletions crates/perry-runtime/src/value/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,22 +64,24 @@ pub(crate) use tags::{
};
pub use tags::{
JS_HANDLE_CALL_METHOD, JS_HANDLE_TYPEOF, JS_NATIVE_ASYNC_HOOKS_CONSTRUCT,
JS_NATIVE_BUN_TCP_DISPATCH, JS_NATIVE_CRYPTO_DISPATCH, JS_NATIVE_DOMAIN_DISPATCH,
JS_NATIVE_EVENTS_CONSTRUCT, JS_NATIVE_EVENTS_DISPATCH, JS_NATIVE_HTTP_DISPATCH,
JS_NATIVE_MODULE_JS_LOADER, JS_NATIVE_NET_DISPATCH, JS_NATIVE_QUERYSTRING_DISPATCH,
JS_NATIVE_SQLITE_DISPATCH, JS_NATIVE_TLS_DISPATCH, JS_NATIVE_WEBCRYPTO_DISPATCH,
JS_NATIVE_ZLIB_DISPATCH, JS_NEW_FROM_HANDLE_V8, SHORT_STRING_MAX_LEN,
JS_NATIVE_ASYNC_LOCAL_STORAGE_SUBCLASS_INIT, JS_NATIVE_BUN_TCP_DISPATCH,
JS_NATIVE_CRYPTO_DISPATCH, JS_NATIVE_DOMAIN_DISPATCH, JS_NATIVE_EVENTS_CONSTRUCT,
JS_NATIVE_EVENTS_DISPATCH, JS_NATIVE_HTTP_DISPATCH, JS_NATIVE_MODULE_JS_LOADER,
JS_NATIVE_NET_DISPATCH, JS_NATIVE_QUERYSTRING_DISPATCH, JS_NATIVE_SQLITE_DISPATCH,
JS_NATIVE_TLS_DISPATCH, JS_NATIVE_WEBCRYPTO_DISPATCH, JS_NATIVE_ZLIB_DISPATCH,
JS_NEW_FROM_HANDLE_V8, SHORT_STRING_MAX_LEN,
};

// Crate-internal handle dispatch atomics + callback type aliases (read by
// every dispatcher that needs to call back into perry-jsruntime).
pub(crate) use tags::{
JsHandleArrayGetFn, JsHandleArrayLengthFn, JsHandleCallMethodFn, JsHandleObjectGetPropertyFn,
JsHandleToStringFn, JsHandleTypeofFn, JsNativeBunTcpDispatchFn, JsNativeCryptoDispatchFn,
JsNativeDomainDispatchFn, JsNativeEventsConstructFn, JsNativeHttpDispatchFn,
JsNativeModuleJsLoaderFn, JsNativeNetDispatchFn, JsNativeQuerystringDispatchFn,
JsNativeSqliteDispatchFn, JsNativeTlsDispatchFn, JsNativeWebCryptoDispatchFn,
JsNativeZlibDispatchFn, JsNewFromHandleV8Fn, JS_HANDLE_ARRAY_GET, JS_HANDLE_ARRAY_LENGTH,
JsHandleToStringFn, JsHandleTypeofFn, JsNativeAsyncLocalStorageSubclassInitFn,
JsNativeBunTcpDispatchFn, JsNativeCryptoDispatchFn, JsNativeDomainDispatchFn,
JsNativeEventsConstructFn, JsNativeHttpDispatchFn, JsNativeModuleJsLoaderFn,
JsNativeNetDispatchFn, JsNativeQuerystringDispatchFn, JsNativeSqliteDispatchFn,
JsNativeTlsDispatchFn, JsNativeWebCryptoDispatchFn, JsNativeZlibDispatchFn,
JsNewFromHandleV8Fn, JS_HANDLE_ARRAY_GET, JS_HANDLE_ARRAY_LENGTH,
JS_HANDLE_OBJECT_GET_PROPERTY, JS_HANDLE_TO_STRING,
};

Expand All @@ -92,11 +94,12 @@ pub use handle::{
is_js_handle, js_handle_array_get, js_handle_array_length, js_set_handle_array_get,
js_set_handle_array_length, js_set_handle_call_method, js_set_handle_object_get_property,
js_set_handle_to_string, js_set_handle_typeof, js_set_native_async_hooks_construct,
js_set_native_bun_tcp_dispatch, js_set_native_crypto_dispatch, js_set_native_domain_dispatch,
js_set_native_events_construct, js_set_native_events_dispatch, js_set_native_http_dispatch,
js_set_native_module_js_loader, js_set_native_net_dispatch, js_set_native_querystring_dispatch,
js_set_native_sqlite_dispatch, js_set_native_tls_dispatch, js_set_native_webcrypto_dispatch,
js_set_native_zlib_dispatch, js_set_new_from_handle_v8, native_module_try_js_property,
js_set_native_async_local_storage_subclass_init, js_set_native_bun_tcp_dispatch,
js_set_native_crypto_dispatch, js_set_native_domain_dispatch, js_set_native_events_construct,
js_set_native_events_dispatch, js_set_native_http_dispatch, js_set_native_module_js_loader,
js_set_native_net_dispatch, js_set_native_querystring_dispatch, js_set_native_sqlite_dispatch,
js_set_native_tls_dispatch, js_set_native_webcrypto_dispatch, js_set_native_zlib_dispatch,
js_set_new_from_handle_v8, native_module_try_js_property,
};

// ----- Basic NaN-box pack / unpack FFI -----
Expand Down
16 changes: 16 additions & 0 deletions crates/perry-runtime/src/value/tags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,3 +218,19 @@ pub static JS_NATIVE_EVENTS_DISPATCH: AtomicPtr<()> = AtomicPtr::new(std::ptr::n
// (method_name_ptr, method_name_len, args_ptr, args_len), returns the NaN-boxed
// instance. Next.js standalone server startup blocker.
pub static JS_NATIVE_ASYNC_HOOKS_CONSTRUCT: AtomicPtr<()> = AtomicPtr::new(std::ptr::null_mut());
// Subclass-init hook for `class X extends <bound async_hooks.AsyncLocalStorage
// export>` reached through anything OTHER than the canonical bare
// `import { AsyncLocalStorage } from "node:async_hooks"` binding (a local
// alias, a namespace member, or a CJS destructured `require()`). Codegen
// already routes the canonical shape statically, straight to perry-stdlib's
// `js_async_local_storage_subclass_init` (declared as an extern symbol by
// codegen, which has no crate-dependency constraint); every other shape only
// resolves at runtime, inside `js_fetch_or_value_super` in THIS crate, which
// cannot depend on perry-stdlib (where the helper — and the `Handle` registry
// it needs — live). Registered by perry-stdlib at startup; stays null when
// stdlib isn't linked. Takes/returns the subclass instance (this_value) as a
// NaN-boxed f64, matching `js_async_local_storage_subclass_init`'s own
// signature. (#10625)
pub(crate) type JsNativeAsyncLocalStorageSubclassInitFn = unsafe extern "C" fn(f64) -> f64;
pub static JS_NATIVE_ASYNC_LOCAL_STORAGE_SUBCLASS_INIT: AtomicPtr<()> =
AtomicPtr::new(std::ptr::null_mut());
12 changes: 12 additions & 0 deletions crates/perry-stdlib/src/common/dispatch/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,18 @@ pub unsafe extern "C" fn js_stdlib_init_dispatch() {
}
}
perry_runtime::js_set_native_async_hooks_construct(async_hooks_native_construct);
// #10625: register the AsyncLocalStorage subclass-init dispatcher so
// `class X extends <bound async_hooks.AsyncLocalStorage export>` reached
// through a local alias, namespace member, or CJS destructured `require()`
// reaches the real handle at `super()` time — not just the canonical bare
// import shape codegen already routes statically. See
// `js_fetch_or_value_super` in perry-runtime for why this indirection
// exists (perry-runtime cannot depend on perry-stdlib, where
// `js_async_local_storage_subclass_init` and the `Handle` registry it uses
// live).
perry_runtime::js_set_native_async_local_storage_subclass_init(
crate::async_local_storage::js_async_local_storage_subclass_init,
);
super::super::net_socket_bridge::register_net_socket_handle_probe();
#[cfg(feature = "external-http-client-pump")]
{
Expand Down
22 changes: 22 additions & 0 deletions test-files/gap_10625_asynclocalstorage_heritage_helper.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use strict';
// CommonJS half of test_gap_10625_asynclocalstorage_heritage.ts: the exact
// shape #10621 fixed for AsyncResource (undici's own heritage pattern),
// mirrored here for AsyncLocalStorage per #10625.
const { AsyncLocalStorage } = require('node:async_hooks');
const asyncHooks = require('node:async_hooks');

class ViaRequire extends AsyncLocalStorage {
constructor() {
super();
}
}

// `require('node:async_hooks').AsyncLocalStorage` reached via a namespace
// member on a plain `require()` result (not destructured).
class ViaRequireNamespaceMember extends asyncHooks.AsyncLocalStorage {
constructor() {
super();
}
}

module.exports = { ViaRequire, ViaRequireNamespaceMember };
83 changes: 83 additions & 0 deletions test-files/test_gap_10625_asynclocalstorage_heritage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// #10625: `class X extends AsyncLocalStorage` has the same indirect-heritage
// defect #10621 fixed for AsyncResource (#10453) — every heritage shape
// EXCEPT a bare `import { AsyncLocalStorage } from "node:async_hooks"`
// binding fell through `js_fetch_or_value_super`
// (`crates/perry-runtime/src/object/global_this/fetch_globals.rs`) to a
// plain CALL of the bound native export, which throws "Class constructor
// AsyncLocalStorage cannot be invoked without 'new'" (or silently produces a
// class_id=0 instance whose inherited methods are missing, depending on the
// shape) instead of running the native-backing init the canonical import
// path already used.
//
// AsyncLocalStorage's subclass-init helper (`js_async_local_storage_subclass_init`)
// lives in perry-stdlib, not perry-runtime, so — unlike AsyncResource, whose
// implementation is entirely in perry-runtime — the fix routes through a
// registration hook perry-stdlib installs at startup
// (`JS_NATIVE_ASYNC_LOCAL_STORAGE_SUBCLASS_INIT`), since perry-runtime
// cannot depend on perry-stdlib.
import { AsyncLocalStorage } from "node:async_hooks";
import * as ah from "node:async_hooks";
import ahDefault from "node:async_hooks";
import {
ViaRequire,
ViaRequireNamespaceMember,
} from "./gap_10625_asynclocalstorage_heritage_helper.cjs";

const Alias = AsyncLocalStorage;

class ViaImport extends AsyncLocalStorage {
constructor() {
super();
}
}
class ViaAlias extends Alias {
constructor() {
super();
}
}
class ViaNamespace extends ah.AsyncLocalStorage {
constructor() {
super();
}
}
class ViaDefaultImport extends ahDefault.AsyncLocalStorage {
constructor() {
super();
}
}

function t(name: string, C: any) {
try {
const inst = new C();
// Round-trip through run()/getStore(), not just construction: a
// class_id=0 empty-object subclass instance would also survive `new`
// without throwing, so proving the fix needs the store to actually flow.
const outside = inst.getStore();
const inside = inst.run(42, () => inst.getStore());
const nested = inst.run("outer", () =>
inst.run("inner", () => inst.getStore()),
);
console.log(
name,
"ok",
"outside=" + String(outside),
"inside=" + inside,
"nested=" + nested,
inst instanceof AsyncLocalStorage,
typeof inst.run,
typeof inst.getStore,
typeof inst.enterWith,
typeof inst.exit,
typeof inst.disable,
);
} catch (e: any) {
console.log(name, "threw:", e.message);
}
}

t("TS extends AsyncLocalStorage (import) ", ViaImport);
t("TS extends Alias ", ViaAlias);
t("TS extends ah.AsyncLocalStorage ", ViaNamespace);
t("TS extends default.AsyncLocalStorage ", ViaDefaultImport);
t("CJS destructured require ", ViaRequire);
t("CJS namespace member export ", ViaRequireNamespaceMember);
Loading