From f92d14fbe8aa761bf4bfaaf7c744efbda1780fb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 30 Jul 2026 22:44:18 +0200 Subject: [PATCH 1/5] fix(tls): expose ALPN and socket prototype surface (#6765) --- .../perry-api-manifest/src/entries/part_1.rs | 8 + .../node_core/module_sea_tls_test.rs | 9 + crates/perry-runtime/src/object/instanceof.rs | 16 +- .../perry-runtime/src/object/native_module.rs | 8 +- .../native_module/callable_export_check.rs | 2 + .../object/native_module/callable_exports.rs | 179 ++++++++++++++++++ .../src/object/native_module/module_keys.rs | 1 + crates/perry-stdlib/src/tls.rs | 73 +++++++ crates/perry/tests/issue_6765_tls_surface.rs | 82 ++++++++ 9 files changed, 371 insertions(+), 7 deletions(-) create mode 100644 crates/perry/tests/issue_6765_tls_surface.rs diff --git a/crates/perry-api-manifest/src/entries/part_1.rs b/crates/perry-api-manifest/src/entries/part_1.rs index 9248b07039..aa4b99fe13 100644 --- a/crates/perry-api-manifest/src/entries/part_1.rs +++ b/crates/perry-api-manifest/src/entries/part_1.rs @@ -797,6 +797,14 @@ pub(crate) const API_MANIFEST_PART_1: &[ApiEntry] = &[ &[p_any("hostname"), p_any("cert")], TypeSpec::Any, ), + method_sig( + "tls", + "convertALPNProtocols", + false, + None, + &[p_any("protocols"), p_any("out")], + TypeSpec::Any, + ), method_sig( "tls", "createSecureContext", diff --git a/crates/perry-codegen/src/lower_call/native_table/node_core/module_sea_tls_test.rs b/crates/perry-codegen/src/lower_call/native_table/node_core/module_sea_tls_test.rs index 85579fc74d..2075a05182 100644 --- a/crates/perry-codegen/src/lower_call/native_table/node_core/module_sea_tls_test.rs +++ b/crates/perry-codegen/src/lower_call/native_table/node_core/module_sea_tls_test.rs @@ -274,6 +274,15 @@ pub(crate) const NODE_CORE_MODULE_SEA_TLS_TEST_ROWS: &[NativeModSig] = &[ args: &[NA_F64, NA_F64], ret: NR_F64, }, + NativeModSig { + module: "tls", + has_receiver: false, + method: "convertALPNProtocols", + class_filter: None, + runtime: "js_tls_convert_alpn_protocols", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, NativeModSig { module: "tls", has_receiver: false, diff --git a/crates/perry-runtime/src/object/instanceof.rs b/crates/perry-runtime/src/object/instanceof.rs index 19ae394d82..f2917c9287 100644 --- a/crates/perry-runtime/src/object/instanceof.rs +++ b/crates/perry-runtime/src/object/instanceof.rs @@ -296,11 +296,15 @@ pub extern "C" fn js_instanceof_dynamic(value: f64, type_ref: f64) -> f64 { method.as_str(), "Readable" | "Writable" | "Duplex" | "Transform" | "PassThrough" | "Stream" ) - && crate::node_stream::is_classic_stream_instance_of(value, method.as_str()) + && (crate::node_stream::is_classic_stream_instance_of(value, method.as_str()) + || super::tls_constructor_prototype_is_instance_of(value, method.as_str())) { return f64::from_bits(crate::value::TAG_TRUE); } - if module == "events" && method == "EventEmitter" && is_event_emitter_instance_value(value) + if module == "events" + && method == "EventEmitter" + && (is_event_emitter_instance_value(value) + || super::tls_constructor_prototype_is_instance_of(value, method.as_str())) { return f64::from_bits(crate::value::TAG_TRUE); } @@ -1007,14 +1011,18 @@ pub extern "C" fn js_instanceof(value: f64, class_id: u32) -> f64 { _ => None, }; if let Some(name) = classic_stream_name { - return if crate::node_stream::is_classic_stream_instance_of(value, name) { + return if crate::node_stream::is_classic_stream_instance_of(value, name) + || super::tls_constructor_prototype_is_instance_of(value, name) + { true_val } else { false_val }; } if class_id == CLASS_ID_EVENT_EMITTER { - return if is_event_emitter_instance_value(value) { + return if is_event_emitter_instance_value(value) + || super::tls_constructor_prototype_is_instance_of(value, "EventEmitter") + { true_val } else { false_val diff --git a/crates/perry-runtime/src/object/native_module.rs b/crates/perry-runtime/src/object/native_module.rs index 71a60986c1..8176f0949e 100644 --- a/crates/perry-runtime/src/object/native_module.rs +++ b/crates/perry-runtime/src/object/native_module.rs @@ -27,9 +27,10 @@ pub(crate) use callable_exports::{ fs_namespace_descriptor_getter_value, fs_namespace_descriptor_setter_value, is_buffer_constructor_value, is_cluster_emitter_method, module_cjs_cache_value, module_cjs_extensions_value, module_cjs_global_paths_value, module_cjs_path_cache_value, - native_string_value, set_bound_native_closure_name, set_builtin_closure_length, - set_builtin_closure_non_constructable, sqlite_session_constructor_value, - sqlite_statement_sync_constructor_value, timers_promises_parent_namespace, + native_string_value, scan_tls_derived_prototype_roots_mut, set_bound_native_closure_name, + set_builtin_closure_length, set_builtin_closure_non_constructable, + sqlite_session_constructor_value, sqlite_statement_sync_constructor_value, + timers_promises_parent_namespace, tls_constructor_prototype_is_instance_of, util_inspect_default_options_value, zlib_codes_object, }; pub(crate) use constants::get_native_module_constant; @@ -239,6 +240,7 @@ pub fn scan_native_callable_export_roots_mut(visitor: &mut crate::gc::RuntimeRoo #[cfg(feature = "mod-http2-constants")] crate::node_http2_constants::scan_roots_mut(visitor); scan_stream_event_emitter_prototype_roots_mut(visitor); + scan_tls_derived_prototype_roots_mut(visitor); } /// Special class ID for native module namespace objects diff --git a/crates/perry-runtime/src/object/native_module/callable_export_check.rs b/crates/perry-runtime/src/object/native_module/callable_export_check.rs index e5e86c235a..09c6574e85 100644 --- a/crates/perry-runtime/src/object/native_module/callable_export_check.rs +++ b/crates/perry-runtime/src/object/native_module/callable_export_check.rs @@ -283,6 +283,7 @@ pub(crate) fn is_native_module_callable_export_reference(module: &str, prop: &st | ("net", "_normalizeArgs") | ("net", "_createServerHandle") | ("tls", "connect") + | ("tls", "convertALPNProtocols") | ("tls", "createServer") | ("tls", "Server") | ("tls", "TLSSocket") @@ -1834,6 +1835,7 @@ static CALLABLE_EXPORT_TABLE: &[(&str, &[&str])] = &[ "TLSSocket", "checkServerIdentity", "connect", + "convertALPNProtocols", "createSecureContext", "createServer", "getCACertificates", diff --git a/crates/perry-runtime/src/object/native_module/callable_exports.rs b/crates/perry-runtime/src/object/native_module/callable_exports.rs index 3607051746..0e98ddf8b4 100644 --- a/crates/perry-runtime/src/object/native_module/callable_exports.rs +++ b/crates/perry-runtime/src/object/native_module/callable_exports.rs @@ -258,6 +258,7 @@ fn native_callable_export_arity_reference(module: &str, prop: &str) -> Option Some(2), + ("tls", "convertALPNProtocols") => Some(2), ("tls", "SecureContext") => Some(1), // #3726: `crypto.Cipheriv` / `crypto.Decipheriv` constructor exports — // `(cipher, key, iv, options)` arity matches Node's length 4. @@ -1396,6 +1397,135 @@ fn attach_tls_secure_context_prototype(constructor_value: f64) { crate::tls::attach_secure_context_constructor_prototype(constructor_value); } +const TLS_SOCKET_PROTOTYPE_METHODS: &[(&str, u32)] = &[ + ("setKeyCert", 1), + ("getSharedSigalgs", 0), + ("getX509Certificate", 0), + ("getPeerX509Certificate", 1), +]; + +thread_local! { + static TLS_DERIVED_PROTOTYPES: RefCell> = const { RefCell::new(Vec::new()) }; +} + +const TLS_PARENT_EVENT_EMITTER: u8 = 1; +const TLS_PARENT_DUPLEX: u8 = 2; + +pub(crate) fn scan_tls_derived_prototype_roots_mut( + visitor: &mut crate::gc::RuntimeRootVisitor<'_>, +) { + TLS_DERIVED_PROTOTYPES.with(|prototypes| { + for (bits, _) in prototypes.borrow_mut().iter_mut() { + visitor.visit_nanbox_u64_slot(bits); + } + }); +} + +extern "C" fn tls_prototype_method_thunk( + closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + unsafe { + let name_ptr = crate::closure::js_closure_get_capture_ptr(closure, 0) as *const i8; + let name_len = crate::closure::js_closure_get_capture_ptr(closure, 1) as usize; + let receiver = crate::object::js_implicit_this_get(); + let args_array = crate::value::js_nanbox_get_pointer(rest); + crate::object::js_native_call_method_apply(receiver, name_ptr, name_len, args_array) + } +} + +fn attach_tls_constructor_prototype(constructor_value: f64, constructor_name: &str) { + let methods = if constructor_name == "TLSSocket" { + TLS_SOCKET_PROTOTYPE_METHODS + } else { + &[] + }; + let constructor_js = JSValue::from_bits(constructor_value.to_bits()); + if !constructor_js.is_pointer() { + return; + } + let constructor = constructor_js.as_pointer::() as usize; + if constructor == 0 { + return; + } + + let prototype = js_object_alloc(0, 0); + if prototype.is_null() { + return; + } + let constructor_key = + crate::string::js_string_from_bytes(b"constructor".as_ptr(), "constructor".len() as u32); + js_object_set_field_by_name(prototype, constructor_key, constructor_value); + super::super::set_builtin_property_attrs( + prototype as usize, + "constructor".to_string(), + super::super::PropertyAttrs::new(true, false, true), + ); + + let thunk = tls_prototype_method_thunk as *const u8; + crate::closure::js_register_closure_rest(thunk, 0); + for &(name, length) in methods { + let method = crate::closure::js_closure_alloc(thunk, 2); + if method.is_null() { + continue; + } + crate::closure::js_closure_set_capture_ptr(method, 0, name.as_ptr() as i64); + crate::closure::js_closure_set_capture_ptr(method, 1, name.len() as i64); + set_bound_native_closure_name(method, name); + set_builtin_closure_length(method as usize, length); + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + js_object_set_field_by_name( + prototype, + key, + crate::value::js_nanbox_pointer(method as i64), + ); + super::super::set_builtin_property_attrs( + prototype as usize, + name.to_string(), + super::super::PropertyAttrs::new(true, false, true), + ); + } + + crate::closure::closure_set_dynamic_prop( + constructor, + "prototype", + crate::value::js_nanbox_pointer(prototype as i64), + ); + let parent_kind = match constructor_name { + "Server" => TLS_PARENT_EVENT_EMITTER, + "TLSSocket" => TLS_PARENT_DUPLEX, + _ => 0, + }; + if parent_kind != 0 { + let bits = crate::value::js_nanbox_pointer(prototype as i64).to_bits(); + TLS_DERIVED_PROTOTYPES.with(|prototypes| { + let mut prototypes = prototypes.borrow_mut(); + if !prototypes.iter().any(|(existing, _)| *existing == bits) { + prototypes.push((bits, parent_kind)); + } + }); + } + super::super::set_builtin_property_attrs( + constructor, + "prototype".to_string(), + super::super::PropertyAttrs::new(false, false, false), + ); +} + +pub(crate) fn tls_constructor_prototype_is_instance_of(value: f64, parent_name: &str) -> bool { + let parent_kind = match parent_name { + "EventEmitter" => TLS_PARENT_EVENT_EMITTER, + "Duplex" => TLS_PARENT_DUPLEX, + _ => return false, + }; + TLS_DERIVED_PROTOTYPES.with(|prototypes| { + prototypes + .borrow() + .iter() + .any(|(bits, kind)| *bits == value.to_bits() && *kind == parent_kind) + }) +} + pub(crate) unsafe fn bound_native_callable_module_and_method( value: f64, ) -> Option<(String, String)> { @@ -1574,6 +1704,8 @@ pub(crate) unsafe fn nm_attach_tls( ) -> f64 { if property_name == "SecureContext" { attach_tls_secure_context_prototype(value); + } else if matches!(property_name, "Server" | "TLSSocket") { + attach_tls_constructor_prototype(value, property_name); } value } @@ -2103,6 +2235,7 @@ static CALLABLE_EXPORT_ARITY_TABLE: &[(&str, &[(&str, u32)])] = &[ ("TLSSocket", 2), ("checkServerIdentity", 2), ("connect", 4), + ("convertALPNProtocols", 2), ("createSecureContext", 1), ("createServer", 2), ("getCACertificates", 1), @@ -2220,4 +2353,50 @@ mod callable_export_arity_table_tests { } } } + + #[test] + fn tls_constructor_prototypes_match_node_parent_classes() { + let server = bound_native_callable_export_value("tls", "Server"); + let server_addr = (server.to_bits() & crate::value::POINTER_MASK) as usize; + let server_proto = crate::closure::closure_get_dynamic_prop(server_addr, "prototype"); + assert!(tls_constructor_prototype_is_instance_of( + server_proto, + "EventEmitter" + )); + assert_eq!( + crate::object::js_instanceof(server_proto, 0xFFFF_0076).to_bits(), + crate::value::TAG_TRUE + ); + let event_emitter = bound_native_callable_export_value("events", "EventEmitter"); + assert_eq!( + crate::object::js_instanceof_dynamic(server_proto, event_emitter).to_bits(), + crate::value::TAG_TRUE + ); + + let socket = bound_native_callable_export_value("tls", "TLSSocket"); + let socket_addr = (socket.to_bits() & crate::value::POINTER_MASK) as usize; + let socket_proto = crate::closure::closure_get_dynamic_prop(socket_addr, "prototype"); + assert!(tls_constructor_prototype_is_instance_of( + socket_proto, + "Duplex" + )); + let socket_proto_obj = + JSValue::from_bits(socket_proto.to_bits()).as_pointer::(); + for &(name, length) in TLS_SOCKET_PROTOTYPE_METHODS { + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + let method = crate::object::js_object_get_field_by_name(socket_proto_obj, key); + let method_addr = method.as_pointer::() as usize; + assert!(crate::closure::is_closure_ptr(method_addr), "{name}"); + assert_eq!(builtin_closure_length(method_addr), Some(length), "{name}"); + } + assert_eq!( + crate::object::js_instanceof(socket_proto, 0xFFFF_0073).to_bits(), + crate::value::TAG_TRUE + ); + let duplex = bound_native_callable_export_value("stream", "Duplex"); + assert_eq!( + crate::object::js_instanceof_dynamic(socket_proto, duplex).to_bits(), + crate::value::TAG_TRUE + ); + } } diff --git a/crates/perry-runtime/src/object/native_module/module_keys.rs b/crates/perry-runtime/src/object/native_module/module_keys.rs index 864a507a60..d59ab1bf58 100644 --- a/crates/perry-runtime/src/object/native_module/module_keys.rs +++ b/crates/perry-runtime/src/object/native_module/module_keys.rs @@ -1827,6 +1827,7 @@ pub(crate) fn native_module_enumerable_keys(module_name: &str) -> Option<&'stati "tls" => Some(&[ b"checkServerIdentity", b"connect", + b"convertALPNProtocols", b"createServer", b"createSecureContext", b"getCACertificates", diff --git a/crates/perry-stdlib/src/tls.rs b/crates/perry-stdlib/src/tls.rs index 7ee9681f0a..895083301a 100644 --- a/crates/perry-stdlib/src/tls.rs +++ b/crates/perry-stdlib/src/tls.rs @@ -1885,6 +1885,78 @@ pub unsafe extern "C" fn js_tls_secure_context_constructor(options_bits: i64) -> make_secure_context(f64_from_raw_bits(options_bits)) } +#[no_mangle] +pub unsafe extern "C" fn js_tls_convert_alpn_protocols(protocols: f64, out: f64) -> f64 { + let mut encoded = Vec::new(); + if is_array_value(protocols) { + let array = + JSValue::from_bits(protocols.to_bits()).as_pointer::(); + let length = js_array_length(array); + for index in 0..length { + let protocol = js_array_get_f64(array, index); + if !JSValue::from_bits(protocol.to_bits()).is_any_string() { + throw_type_error( + "The \"protocols\" argument must contain only strings", + "ERR_INVALID_ARG_TYPE", + ); + } + let protocol = value_to_string(protocol).unwrap_or_default(); + if protocol.len() > u8::MAX as usize { + perry_runtime::fs::validate::throw_range_error_named( + "ALPN protocol names must not exceed 255 bytes", + "ERR_OUT_OF_RANGE", + ); + } + encoded.push(protocol.len() as u8); + encoded.extend_from_slice(protocol.as_bytes()); + } + } else if let Some(addr) = pointer_addr(protocols) { + if perry_runtime::buffer::is_registered_buffer(addr) + && !perry_runtime::buffer::is_any_array_buffer(addr) + { + let data = perry_runtime::buffer::js_native_buffer_data_ptr(protocols); + let length = perry_runtime::buffer::js_native_buffer_byte_len(protocols); + if !data.is_null() && length != 0 { + encoded.extend_from_slice(std::slice::from_raw_parts(data, length)); + } + } else if perry_runtime::typedarray::lookup_typed_array_kind(addr).is_some() { + let mut length = 0u32; + let data = + perry_runtime::buffer::js_value_buffer_or_typedarray_data(protocols, &mut length); + if !data.is_null() && length != 0 { + encoded.extend_from_slice(std::slice::from_raw_parts(data, length as usize)); + } + } else { + return undefined(); + } + } else { + // Node's internal helper ignores non-array/non-view values and leaves + // the target object untouched. + return undefined(); + } + + let Some(out_addr) = pointer_addr(out) else { + throw_type_error( + "The \"out\" argument must be of type object", + "ERR_INVALID_ARG_TYPE", + ); + }; + let buffer = perry_runtime::buffer::js_buffer_alloc(encoded.len() as i32, 0); + if !encoded.is_empty() { + std::ptr::copy_nonoverlapping( + encoded.as_ptr(), + perry_runtime::buffer::buffer_data_mut(buffer), + encoded.len(), + ); + } + set_field( + out_addr as *mut ObjectHeader, + "ALPNProtocols", + js_nanbox_pointer(buffer as i64), + ); + undefined() +} + pub unsafe extern "C" fn js_tls_native_dispatch( method_ptr: *const u8, method_len: usize, @@ -1910,6 +1982,7 @@ pub unsafe extern "C" fn js_tls_native_dispatch( "checkServerIdentity" => { js_tls_check_server_identity(arg(0).to_bits() as i64, arg(1).to_bits() as i64) } + "convertALPNProtocols" => js_tls_convert_alpn_protocols(arg(0), arg(1)), "connect" => { // Pass the args through raw — js_tls_connect resolves Node's // `connect(options[, cb])` / `connect(port[, host][, options][, diff --git a/crates/perry/tests/issue_6765_tls_surface.rs b/crates/perry/tests/issue_6765_tls_surface.rs new file mode 100644 index 0000000000..2b64bd560e --- /dev/null +++ b/crates/perry/tests/issue_6765_tls_surface.rs @@ -0,0 +1,82 @@ +//! Regression coverage for the first #6765 TLS surface increment. + +use std::path::PathBuf; +use std::process::Command; + +fn perry_bin() -> PathBuf { + PathBuf::from(env!("CARGO_BIN_EXE_perry")) +} + +#[test] +fn tls_exposes_extended_socket_and_alpn_helpers() { + let dir = tempfile::tempdir().expect("tempdir"); + let entry = dir.path().join("main.ts"); + let output = dir.path().join("main_bin"); + std::fs::write( + &entry, + r#" +import tls from "node:tls"; + +console.log( + "surface:", + typeof tls.convertALPNProtocols, + typeof tls.TLSSocket.prototype.setKeyCert, + typeof tls.TLSSocket.prototype.getSharedSigalgs, + typeof tls.TLSSocket.prototype.getX509Certificate, + typeof tls.TLSSocket.prototype.getPeerX509Certificate, +); + +const out: any = {}; +tls.convertALPNProtocols(["h2", "http/1.1"], out); +console.log("array:", Buffer.isBuffer(out.ALPNProtocols), out.ALPNProtocols.toString("hex")); + +const source = Buffer.from([9, 2, 104, 50, 9]); +const copied: any = {}; +tls.convertALPNProtocols(source.subarray(1, 4), copied); +source[2] = 120; +console.log("copy:", copied.ALPNProtocols.toString("hex")); + +try { + tls.convertALPNProtocols(["a".repeat(256)], {}); +} catch (error: any) { + console.log("range:", error instanceof RangeError, error.code); +} +"#, + ) + .expect("write fixture"); + + let compile = Command::new(perry_bin()) + .current_dir(dir.path()) + .arg("compile") + .arg(&entry) + .arg("-o") + .arg(&output) + .output() + .expect("run perry compile"); + assert!( + compile.status.success(), + "perry compile failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&compile.stdout), + String::from_utf8_lossy(&compile.stderr) + ); + + let run = Command::new(&output) + .output() + .expect("run compiled fixture"); + assert!( + run.status.success(), + "compiled fixture failed\nstatus: {:?}\nstdout:\n{}\nstderr:\n{}", + run.status, + String::from_utf8_lossy(&run.stdout), + String::from_utf8_lossy(&run.stderr) + ); + assert_eq!( + String::from_utf8_lossy(&run.stdout), + concat!( + "surface: function function function function function\n", + "array: true 02683208687474702f312e31\n", + "copy: 026832\n", + "range: true ERR_OUT_OF_RANGE\n", + ) + ); +} From 4a07e6eae79abc80fe3981ae152fd2c43e4a1501 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 30 Jul 2026 22:44:50 +0200 Subject: [PATCH 2/5] docs: add changelog for #7094 --- changelog.d/7094-tls-alpn-surface.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7094-tls-alpn-surface.md diff --git a/changelog.d/7094-tls-alpn-surface.md b/changelog.d/7094-tls-alpn-surface.md new file mode 100644 index 0000000000..9fdf77ff3f --- /dev/null +++ b/changelog.d/7094-tls-alpn-surface.md @@ -0,0 +1 @@ +Added Node-compatible TLS ALPN conversion and modern `TLSSocket` prototype metadata. From efeeecc86f74a522ef7f2d178d1259d04281fca6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 30 Jul 2026 23:16:27 +0200 Subject: [PATCH 3/5] fix(tls): root constructor prototype metadata (#6765) --- .../object/native_module/callable_exports.rs | 88 ++++++++++++++----- crates/perry/tests/issue_6765_tls_surface.rs | 8 ++ 2 files changed, 75 insertions(+), 21 deletions(-) diff --git a/crates/perry-runtime/src/object/native_module/callable_exports.rs b/crates/perry-runtime/src/object/native_module/callable_exports.rs index 0e98ddf8b4..df48ce7f4c 100644 --- a/crates/perry-runtime/src/object/native_module/callable_exports.rs +++ b/crates/perry-runtime/src/object/native_module/callable_exports.rs @@ -1401,7 +1401,7 @@ const TLS_SOCKET_PROTOTYPE_METHODS: &[(&str, u32)] = &[ ("setKeyCert", 1), ("getSharedSigalgs", 0), ("getX509Certificate", 0), - ("getPeerX509Certificate", 1), + ("getPeerX509Certificate", 0), ]; thread_local! { @@ -1434,7 +1434,7 @@ extern "C" fn tls_prototype_method_thunk( } } -fn attach_tls_constructor_prototype(constructor_value: f64, constructor_name: &str) { +fn attach_tls_constructor_prototype(constructor_value: f64, constructor_name: &str) -> f64 { let methods = if constructor_name == "TLSSocket" { TLS_SOCKET_PROTOTYPE_METHODS } else { @@ -1442,22 +1442,37 @@ fn attach_tls_constructor_prototype(constructor_value: f64, constructor_name: &s }; let constructor_js = JSValue::from_bits(constructor_value.to_bits()); if !constructor_js.is_pointer() { - return; + return constructor_value; } let constructor = constructor_js.as_pointer::() as usize; if constructor == 0 { - return; + return constructor_value; } + // Every allocator below can move objects. Hold only updateable handles + // across allocations and reload the current address at each use. + let scope = crate::gc::RuntimeHandleScope::new(); + let constructor_handle = + scope.root_raw_mut_ptr(constructor as *mut crate::closure::ClosureHeader); let prototype = js_object_alloc(0, 0); if prototype.is_null() { - return; + return crate::value::js_nanbox_pointer( + constructor_handle.get_raw_mut_ptr::() as i64, + ); } + let prototype_handle = scope.root_raw_mut_ptr(prototype); let constructor_key = crate::string::js_string_from_bytes(b"constructor".as_ptr(), "constructor".len() as u32); - js_object_set_field_by_name(prototype, constructor_key, constructor_value); + let constructor_key_handle = scope.root_string_ptr(constructor_key); + js_object_set_field_by_name( + prototype_handle.get_raw_mut_ptr(), + constructor_key_handle.get_raw_mut_ptr(), + crate::value::js_nanbox_pointer( + constructor_handle.get_raw_mut_ptr::() as i64, + ), + ); super::super::set_builtin_property_attrs( - prototype as usize, + prototype_handle.get_raw_mut_ptr::() as usize, "constructor".to_string(), super::super::PropertyAttrs::new(true, false, true), ); @@ -1469,27 +1484,51 @@ fn attach_tls_constructor_prototype(constructor_value: f64, constructor_name: &s if method.is_null() { continue; } - crate::closure::js_closure_set_capture_ptr(method, 0, name.as_ptr() as i64); - crate::closure::js_closure_set_capture_ptr(method, 1, name.len() as i64); - set_bound_native_closure_name(method, name); - set_builtin_closure_length(method as usize, length); - let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + let method_handle = scope.root_raw_mut_ptr(method); + crate::closure::js_closure_set_capture_ptr( + method_handle.get_raw_mut_ptr(), + 0, + name.as_ptr() as i64, + ); + crate::closure::js_closure_set_capture_ptr( + method_handle.get_raw_mut_ptr(), + 1, + name.len() as i64, + ); + let name_string = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + let name_handle = scope.root_string_ptr(name_string); + crate::closure::closure_set_dynamic_prop( + method_handle.get_raw_mut_ptr::() as usize, + "name", + f64::from_bits(JSValue::string_ptr(name_handle.get_raw_mut_ptr()).bits()), + ); + super::super::set_builtin_property_attrs( + method_handle.get_raw_mut_ptr::() as usize, + "name".to_string(), + super::super::PropertyAttrs::new(false, false, true), + ); + set_builtin_closure_length( + method_handle.get_raw_mut_ptr::() as usize, + length, + ); js_object_set_field_by_name( - prototype, - key, - crate::value::js_nanbox_pointer(method as i64), + prototype_handle.get_raw_mut_ptr(), + name_handle.get_raw_mut_ptr(), + crate::value::js_nanbox_pointer( + method_handle.get_raw_mut_ptr::() as i64, + ), ); super::super::set_builtin_property_attrs( - prototype as usize, + prototype_handle.get_raw_mut_ptr::() as usize, name.to_string(), super::super::PropertyAttrs::new(true, false, true), ); } crate::closure::closure_set_dynamic_prop( - constructor, + constructor_handle.get_raw_mut_ptr::() as usize, "prototype", - crate::value::js_nanbox_pointer(prototype as i64), + crate::value::js_nanbox_pointer(prototype_handle.get_raw_mut_ptr::() as i64), ); let parent_kind = match constructor_name { "Server" => TLS_PARENT_EVENT_EMITTER, @@ -1497,7 +1536,11 @@ fn attach_tls_constructor_prototype(constructor_value: f64, constructor_name: &s _ => 0, }; if parent_kind != 0 { - let bits = crate::value::js_nanbox_pointer(prototype as i64).to_bits(); + let bits = crate::value::js_nanbox_pointer( + prototype_handle.get_raw_mut_ptr::() as i64, + ) + .to_bits(); + crate::gc::runtime_write_barrier_root_nanbox(bits); TLS_DERIVED_PROTOTYPES.with(|prototypes| { let mut prototypes = prototypes.borrow_mut(); if !prototypes.iter().any(|(existing, _)| *existing == bits) { @@ -1506,10 +1549,13 @@ fn attach_tls_constructor_prototype(constructor_value: f64, constructor_name: &s }); } super::super::set_builtin_property_attrs( - constructor, + constructor_handle.get_raw_mut_ptr::() as usize, "prototype".to_string(), super::super::PropertyAttrs::new(false, false, false), ); + crate::value::js_nanbox_pointer( + constructor_handle.get_raw_mut_ptr::() as i64, + ) } pub(crate) fn tls_constructor_prototype_is_instance_of(value: f64, parent_name: &str) -> bool { @@ -1705,7 +1751,7 @@ pub(crate) unsafe fn nm_attach_tls( if property_name == "SecureContext" { attach_tls_secure_context_prototype(value); } else if matches!(property_name, "Server" | "TLSSocket") { - attach_tls_constructor_prototype(value, property_name); + value = attach_tls_constructor_prototype(value, property_name); } value } diff --git a/crates/perry/tests/issue_6765_tls_surface.rs b/crates/perry/tests/issue_6765_tls_surface.rs index 2b64bd560e..6ec592237f 100644 --- a/crates/perry/tests/issue_6765_tls_surface.rs +++ b/crates/perry/tests/issue_6765_tls_surface.rs @@ -25,6 +25,13 @@ console.log( typeof tls.TLSSocket.prototype.getX509Certificate, typeof tls.TLSSocket.prototype.getPeerX509Certificate, ); +console.log( + "lengths:", + tls.TLSSocket.prototype.setKeyCert.length, + tls.TLSSocket.prototype.getSharedSigalgs.length, + tls.TLSSocket.prototype.getX509Certificate.length, + tls.TLSSocket.prototype.getPeerX509Certificate.length, +); const out: any = {}; tls.convertALPNProtocols(["h2", "http/1.1"], out); @@ -74,6 +81,7 @@ try { String::from_utf8_lossy(&run.stdout), concat!( "surface: function function function function function\n", + "lengths: 1 0 0 0\n", "array: true 02683208687474702f312e31\n", "copy: 026832\n", "range: true ERR_OUT_OF_RANGE\n", From 88f1d15995c27630a808cd7f891350ecf70d11f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 30 Jul 2026 23:19:07 +0200 Subject: [PATCH 4/5] fix(tls): use configured rustls provider (#6765) --- crates/perry-stdlib/src/tls.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/perry-stdlib/src/tls.rs b/crates/perry-stdlib/src/tls.rs index 895083301a..abc20e5161 100644 --- a/crates/perry-stdlib/src/tls.rs +++ b/crates/perry-stdlib/src/tls.rs @@ -759,7 +759,12 @@ unsafe fn build_server_config_from_options( // Node serves whatever cert/key the user supplies; load the signing // key directly and install a fixed-cert resolver. (Mirrors // `perry-ext-http::tls::build_server_config`.) - let signing_key = rustls::crypto::ring::default_provider() + // The minimal auto-optimized `tls` graph uses rustls's default AWS-LC + // provider (installed by `ensure_crypto_provider_installed`) and does not + // enable the optional `ring` module. Load the key through that same + // provider so a TLS-only program can build without unrelated features + // pulling `ring` in by feature unification. + let signing_key = rustls::crypto::aws_lc_rs::default_provider() .key_provider .load_private_key(key) .map_err(|e| format!("rustls: build server config: {e}"))?; From 297cdc595298176f3a55d287ad1c05b97ea61929 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 30 Jul 2026 23:35:14 +0200 Subject: [PATCH 5/5] fix(tls): match constructor prototype descriptors --- .../src/object/native_module/callable_exports.rs | 2 +- crates/perry/tests/issue_6765_tls_surface.rs | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/crates/perry-runtime/src/object/native_module/callable_exports.rs b/crates/perry-runtime/src/object/native_module/callable_exports.rs index df48ce7f4c..2dacf2ee0b 100644 --- a/crates/perry-runtime/src/object/native_module/callable_exports.rs +++ b/crates/perry-runtime/src/object/native_module/callable_exports.rs @@ -1551,7 +1551,7 @@ fn attach_tls_constructor_prototype(constructor_value: f64, constructor_name: &s super::super::set_builtin_property_attrs( constructor_handle.get_raw_mut_ptr::() as usize, "prototype".to_string(), - super::super::PropertyAttrs::new(false, false, false), + super::super::PropertyAttrs::new(true, false, false), ); crate::value::js_nanbox_pointer( constructor_handle.get_raw_mut_ptr::() as i64, diff --git a/crates/perry/tests/issue_6765_tls_surface.rs b/crates/perry/tests/issue_6765_tls_surface.rs index 6ec592237f..fca6017fb8 100644 --- a/crates/perry/tests/issue_6765_tls_surface.rs +++ b/crates/perry/tests/issue_6765_tls_surface.rs @@ -32,6 +32,15 @@ console.log( tls.TLSSocket.prototype.getX509Certificate.length, tls.TLSSocket.prototype.getPeerX509Certificate.length, ); +for (const constructor of [tls.Server, tls.TLSSocket]) { + const descriptor = Object.getOwnPropertyDescriptor(constructor, "prototype")!; + console.log( + "prototype descriptor:", + descriptor.writable, + descriptor.enumerable, + descriptor.configurable, + ); +} const out: any = {}; tls.convertALPNProtocols(["h2", "http/1.1"], out); @@ -82,6 +91,8 @@ try { concat!( "surface: function function function function function\n", "lengths: 1 0 0 0\n", + "prototype descriptor: true false false\n", + "prototype descriptor: true false false\n", "array: true 02683208687474702f312e31\n", "copy: 026832\n", "range: true ERR_OUT_OF_RANGE\n",