Skip to content
Closed
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
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions changelog.d/8663-node-tls-parity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Completed `node:tls` compatibility across the full node-suite inventory. TLS
servers and sockets now support real loopback handshakes, ALPN and SNI
selection, certificate and secure-context rotation, custom trust stores,
client certificates, identity callbacks, negotiated state, orderly shutdown,
and Node-compatible validation and error shapes in both bundled and optimized
external-net builds. The current TLS inventory passes 100/100 fixtures.
8 changes: 8 additions & 0 deletions crates/perry-api-manifest/src/entries/part_1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -778,6 +778,13 @@ pub(crate) const API_MANIFEST_PART_1: &[ApiEntry] = &[
method("net", "isSessionReused", true, Some("Socket")),
method("net", "exportKeyingMaterial", true, Some("Socket")),
method("net", "setMaxSendFragment", true, Some("Socket")),
method("net", "getEphemeralKeyInfo", true, Some("Socket")),
method("net", "getFinished", true, Some("Socket")),
method("net", "getPeerFinished", true, Some("Socket")),
method("net", "getSharedSigalgs", true, Some("Socket")),
method("net", "getX509Certificate", true, Some("Socket")),
method("net", "getPeerX509Certificate", true, Some("Socket")),
method("net", "setKeyCert", true, Some("Socket")),
// Issue #1123 followup — `net.Server` instance methods backing
// `createServer(...).listen/.close/.address/.on`. Mirrors the
// shape of the http-server rows at entries.rs:2298. The
Expand Down Expand Up @@ -861,6 +868,7 @@ pub(crate) const API_MANIFEST_PART_1: &[ApiEntry] = &[
TypeSpec::Any,
),
method("tls", "getCiphers", false, None),
method("tls", "getCertificateCompressionAlgorithms", false, None),
method_sig(
"tls",
"setDefaultCACertificates",
Expand Down
8 changes: 8 additions & 0 deletions crates/perry-codegen/src/expr/property_get/globalget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,14 @@ pub(crate) fn lower_globalget_property(ctx: &mut FnCtx<'_>, property: &str) -> R
&[(I64, &ctor_handle), (I64, &key_raw)],
));
}
// `Buffer.isBuffer` used as a callback (for example
// `values.every(Buffer.isBuffer)`) needs the callable value, not only the
// direct-call intrinsic. Bare builtin receivers are represented by the
// shared `GlobalGet(0)` sentinel, and `isBuffer` is distinctive among the
// builtin statics, so recover it from the populated Buffer constructor.
if property == "isBuffer" {
return Ok(lower_global_builtin_static_value(ctx, "Buffer", property));
}
// #6674: `Uint8Array.fromBase64` / `fromHex` read as a VALUE (not a direct
// call) — jose/Auth.js feature-detect with `Uint8Array.fromBase64 ? native
// : fallback`. The bare `Uint8Array` receiver collapses to `GlobalGet(0)`
Expand Down
54 changes: 54 additions & 0 deletions crates/perry-codegen/src/lower_call/native/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,60 @@ pub(crate) fn lower_native_method_call(
}
}

// `X509Certificate` instances are compact native handles. Their method
// calls normally miss the static native table and used to fall through to
// `js_native_call_method_nullsafe`. That entry point also serves native
// *property reads*, so its zero-argument path asks the handle-property
// dispatcher first. For `cert.toLegacyObject()` this returned the bound
// method closure instead of invoking it; valid-host identity checks then
// appeared to pass only because a closure is not a certificate object.
//
// Bare method-value reads (`const f = cert.toLegacyObject`) remain ordinary
// `PropertyGet`s in HIR (`is_native_dispatch_member` deliberately excludes
// crypto), so an actual `NativeMethodCall` for this exact class is
// unambiguously a call. Route it through the non-property dispatcher just
// like the Console instance arm above.
if module == "crypto" && class_name == Some("X509Certificate") {
if let Some(recv) = object {
let recv_box = lower_expr(ctx, recv)?;
let mut lowered_args: Vec<String> = Vec::with_capacity(args.len());
for arg in args {
lowered_args.push(lower_expr(ctx, arg)?);
}

let (args_ptr, args_len) = if lowered_args.is_empty() {
("null".to_string(), "0".to_string())
} else {
let n = lowered_args.len();
let buf = ctx.func.alloca_entry_array(DOUBLE, n);
{
let blk = ctx.block();
for (i, value) in lowered_args.iter().enumerate() {
let slot = blk.gep(DOUBLE, &buf, &[(I64, &i.to_string())]);
blk.store(DOUBLE, value, &slot);
}
}
(buf, n.to_string())
};

let method_idx = ctx.strings.intern(method);
let entry = ctx.strings.entry(method_idx);
let bytes_global = format!("@{}", entry.bytes_global);
let name_len = entry.byte_len.to_string();
return Ok(ctx.block().call(
DOUBLE,
"js_native_call_method",
&[
(DOUBLE, &recv_box),
(PTR, &bytes_global),
(I64, &name_len),
(PTR, &args_ptr),
(I64, &args_len),
],
));
}
}

// Receiver-less native method calls (e.g. plugin::setConfig(...)
// as a static module function): lower args for side effects and
// return TAG_UNDEFINED. Using TAG_UNDEFINED (not 0.0) so that
Expand Down
39 changes: 35 additions & 4 deletions crates/perry-codegen/src/lower_call/native_module_rooting_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ use perry_hir::types::Type;
use perry_hir::{Expr, Function, Module, Stmt};

fn compile_native_call(args: Vec<Expr>) -> String {
compile_native_instance_call("https", None, None, "createServer", args)
}

fn compile_native_instance_call(
native_module: &str,
class_name: Option<&str>,
object: Option<Expr>,
method: &str,
args: Vec<Expr>,
) -> String {
let mut module = Module::new("native_module_rooting_test.ts");
module.functions.push(Function {
id: 0,
Expand All @@ -19,10 +29,10 @@ fn compile_native_call(args: Vec<Expr>) -> String {
params: Vec::new(),
return_type: Type::Any,
body: vec![Stmt::Expr(Expr::NativeMethodCall {
module: "https".to_string(),
class_name: None,
object: None,
method: "createServer".to_string(),
module: native_module.to_string(),
class_name: class_name.map(str::to_string),
object: object.map(Box::new),
method: method.to_string(),
args,
})],
is_async: false,
Expand Down Expand Up @@ -84,3 +94,24 @@ fn native_module_first_argument_is_rooted_across_allocating_second_argument() {
"native-module options argument",
);
}

#[test]
fn x509_zero_argument_method_call_uses_invoking_dispatch() {
let module_ir = compile_native_instance_call(
"crypto",
Some("X509Certificate"),
Some(Expr::Number(1.0)),
"toLegacyObject",
Vec::new(),
);
let ir = build_function_ir(&module_ir);

assert!(
ir.contains("call double @js_native_call_method("),
"X509Certificate method calls must use the invoking dispatcher:\n{ir}"
);
assert!(
!ir.contains("@js_native_call_method_nullsafe("),
"the zero-argument property-read fallback returns a bound method closure:\n{ir}"
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,15 @@ pub(crate) const NODE_CORE_MODULE_SEA_TLS_TEST_ROWS: &[NativeModSig] = &[
args: &[],
ret: NR_F64,
},
NativeModSig {
module: "tls",
has_receiver: false,
method: "getCertificateCompressionAlgorithms",
class_filter: None,
runtime: "js_tls_get_certificate_compression_algorithms",
args: &[],
ret: NR_F64,
},
NativeModSig {
module: "tls",
has_receiver: false,
Expand Down
69 changes: 66 additions & 3 deletions crates/perry-codegen/src/lower_call/native_table/tls_events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ pub(super) const TLS_EVENTS_ROWS: &[NativeModSig] = &[
class_filter: Some("Server"),
runtime: "js_tls_server_set_secure_context",
args: &[NA_JSV],
ret: NR_PTR,
ret: NR_VOID,
},
NativeModSig {
module: "tls",
Expand All @@ -159,7 +159,7 @@ pub(super) const TLS_EVENTS_ROWS: &[NativeModSig] = &[
class_filter: Some("Server"),
runtime: "js_tls_server_set_ticket_keys",
args: &[NA_JSV],
ret: NR_PTR,
ret: NR_VOID,
},
NativeModSig {
module: "net",
Expand Down Expand Up @@ -221,7 +221,7 @@ pub(super) const TLS_EVENTS_ROWS: &[NativeModSig] = &[
method: "exportKeyingMaterial",
class_filter: Some("Socket"),
runtime: "js_tls_socket_export_keying_material",
args: &[NA_F64, NA_STR],
args: &[NA_F64, NA_JSV, NA_JSV],
ret: NR_F64,
},
NativeModSig {
Expand All @@ -233,4 +233,67 @@ pub(super) const TLS_EVENTS_ROWS: &[NativeModSig] = &[
args: &[NA_F64],
ret: NR_F64,
},
NativeModSig {
module: "net",
has_receiver: true,
method: "getEphemeralKeyInfo",
class_filter: Some("Socket"),
runtime: "js_tls_socket_get_ephemeral_key_info",
args: &[],
ret: NR_F64,
},
NativeModSig {
module: "net",
has_receiver: true,
method: "getFinished",
class_filter: Some("Socket"),
runtime: "js_tls_socket_get_finished",
args: &[],
ret: NR_F64,
},
NativeModSig {
module: "net",
has_receiver: true,
method: "getPeerFinished",
class_filter: Some("Socket"),
runtime: "js_tls_socket_get_peer_finished",
args: &[],
ret: NR_F64,
},
NativeModSig {
module: "net",
has_receiver: true,
method: "getSharedSigalgs",
class_filter: Some("Socket"),
runtime: "js_tls_socket_get_shared_sigalgs",
args: &[],
ret: NR_F64,
},
NativeModSig {
module: "net",
has_receiver: true,
method: "getX509Certificate",
class_filter: Some("Socket"),
runtime: "js_tls_socket_get_x509_certificate",
args: &[],
ret: NR_F64,
},
NativeModSig {
module: "net",
has_receiver: true,
method: "getPeerX509Certificate",
class_filter: Some("Socket"),
runtime: "js_tls_socket_get_peer_x509_certificate",
args: &[],
ret: NR_F64,
},
NativeModSig {
module: "net",
has_receiver: true,
method: "setKeyCert",
class_filter: Some("Socket"),
runtime: "js_tls_socket_set_key_cert",
args: &[NA_JSV],
ret: NR_F64,
},
];
1 change: 1 addition & 0 deletions crates/perry-ext-net/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ tokio-rustls.workspace = true
rustls.workspace = true
rustls-native-certs = "0.8"
serde_json.workspace = true
rustls-pemfile.workspace = true

[dev-dependencies]
perry-ffi = { workspace = true, features = ["runtime-link"] }
Expand Down
34 changes: 32 additions & 2 deletions crates/perry-ext-net/src/jsvalue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,18 @@ pub(crate) unsafe fn get_object_string_field(obj_f64: f64, field_name: &str) ->
None
}

pub(crate) unsafe fn get_object_value_field(obj_f64: f64, field_name: &str) -> Option<f64> {
if !is_nanboxed_pointer(obj_f64) {
return None;
}
let obj_ptr = unbox_pointer(obj_f64) as *const ObjectHeader;
if (obj_ptr as usize) < 0x100000 {
return None;
}
let key = js_string_from_bytes(field_name.as_ptr(), field_name.len() as u32);
Some(js_object_get_field_by_name_f64(obj_ptr, key))
}

pub(crate) unsafe fn get_object_number_field(obj_f64: f64, field_name: &str) -> Option<f64> {
if !is_nanboxed_pointer(obj_f64) {
return None;
Expand Down Expand Up @@ -253,10 +265,10 @@ pub(crate) unsafe fn get_object_bool_field(obj_f64: f64, field_name: &str) -> Op
/// instances, not raw strings. Returns a NaN-boxed `f64` pointing at
/// the object. Issue #770.
pub(crate) unsafe fn build_error_object(msg: &str) -> f64 {
let keys: [&str; 1] = ["message"];
let keys: [&str; 3] = ["message", "code", "name"];
let (packed, shape_id) = build_object_shape(&keys);
let obj: *mut ObjectHeader =
js_object_alloc_with_shape(shape_id, 1, packed.as_ptr(), packed.len() as u32);
js_object_alloc_with_shape(shape_id, 3, packed.as_ptr(), packed.len() as u32);
if obj.is_null() {
// Fall back to the bare string so the listener still receives
// *something* if the object alloc failed.
Expand All @@ -266,6 +278,24 @@ pub(crate) unsafe fn build_error_object(msg: &str) -> f64 {
let s = alloc_string(msg);
let v = JsValue::from_string_ptr(s.as_raw());
js_object_set_field(obj, 0, v);
let code = if msg.starts_with("ERR_") {
Some(msg)
} else if msg.contains("UnknownIssuer")
|| msg.contains("unknown issuer")
|| msg.contains("invalid peer certificate")
{
Some("DEPTH_ZERO_SELF_SIGNED_CERT")
} else if msg.to_ascii_lowercase().contains("connection refused") {
Some("ECONNREFUSED")
} else {
None
};
if let Some(code) = code {
let code = alloc_string(code);
js_object_set_field(obj, 1, JsValue::from_string_ptr(code.as_raw()));
}
let name = alloc_string("Error");
js_object_set_field(obj, 2, JsValue::from_string_ptr(name.as_raw()));
let obj_v = JsValue::from_object_ptr(obj as *mut u8);
f64::from_bits(obj_v.bits())
}
Expand Down
Loading
Loading