Skip to content

Commit a43a1d0

Browse files
author
Ralph Küpper
committed
fix(net): root TLS connect arguments across callbacks
1 parent d82fe3d commit a43a1d0

4 files changed

Lines changed: 138 additions & 58 deletions

File tree

changelog.d/8754-external-http-tls-preflight.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,6 @@ Fixed optimized `node:net`, `node:http`, `node:https`, and `node:http2` builds
22
after the TLS parity split. These external wrappers now retain Perry's shared
33
TLS server and SNI/ALPN preflight provider without also linking the bundled
44
network implementation, eliminating the `js_tls_client_preflight` undefined
5-
symbol that blocked HTTP gap and GC-stress fixtures at compile time.
5+
symbol that blocked HTTP gap and GC-stress fixtures at compile time. External
6+
`tls.connect` overload arguments also remain rooted when a user-replaced
7+
`createSecureContext` callback triggers a moving collection.

crates/perry-ext-net/src/tls.rs

Lines changed: 76 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -649,6 +649,17 @@ pub(crate) fn record_tls_handshake(
649649
/// ABI — see `NA_F64` lowering in perry-codegen.
650650
#[no_mangle]
651651
pub unsafe extern "C" fn js_tls_connect(arg1: f64, arg2: f64, arg3: f64, arg4: f64) -> i64 {
652+
// `js_tls_prepare_connect` may invoke a user-replaced createSecureContext
653+
// before overload resolution. Keep every incoming value in the runtime's
654+
// moving-GC root stack, and re-read the selected options/callback values
655+
// after every later callback-capable runtime call.
656+
let root_scope = perry_ffi::TransientRootScope::enter();
657+
let rooted_args = [
658+
root_scope.root_nanbox(arg1),
659+
root_scope.root_nanbox(arg2),
660+
root_scope.root_nanbox(arg3),
661+
root_scope.root_nanbox(arg4),
662+
];
652663
extern "C" {
653664
fn js_tls_prepare_connect();
654665
}
@@ -685,87 +696,89 @@ pub unsafe extern "C" fn js_tls_connect(arg1: f64, arg2: f64, arg3: f64, arg4: f
685696
(j.is_bool() && !j.to_bool()) || (j.is_number() && j.to_number() == 0.0)
686697
};
687698

688-
let (host, port, servername, verify, cb_f64, metadata_options);
689-
if let Some(h) = as_string(arg1) {
699+
let (host, port, servername, verify, callback_arg, metadata_options_arg);
700+
if let Some(h) = as_string(rooted_args[0].get()) {
690701
// Legacy Perry positional: (host, port, servername?, verify?).
691-
let p = JsValue::from_bits(arg2.to_bits());
702+
let p = JsValue::from_bits(rooted_args[1].get().to_bits());
692703
if !p.is_number() && !p.is_int32() {
693704
return 0;
694705
}
695706
port = p.to_number() as u16;
696-
servername = as_string(arg3).unwrap_or_else(|| h.clone());
707+
servername = as_string(rooted_args[2].get()).unwrap_or_else(|| h.clone());
697708
host = h;
698-
verify = !explicitly_off(arg4);
699-
cb_f64 = None;
700-
metadata_options = f64::from_bits(0x7FFC_0000_0000_0001);
701-
} else if JsValue::from_bits(arg1.to_bits()).is_number()
702-
|| JsValue::from_bits(arg1.to_bits()).is_int32()
709+
verify = !explicitly_off(rooted_args[3].get());
710+
callback_arg = None;
711+
metadata_options_arg = None;
712+
} else if JsValue::from_bits(rooted_args[0].get().to_bits()).is_number()
713+
|| JsValue::from_bits(rooted_args[0].get().to_bits()).is_int32()
703714
{
704715
// Node positional form: tls.connect(port[, host][, options][, cb]).
705-
js_net_validate_connect_port(arg1);
706-
port = JsValue::from_bits(arg1.to_bits()).to_number() as u16;
716+
js_net_validate_connect_port(rooted_args[0].get());
717+
port = JsValue::from_bits(rooted_args[0].get().to_bits()).to_number() as u16;
707718
let mut opt_host: Option<String> = None;
708-
let mut opts: Option<f64> = None;
709-
let mut cb: Option<f64> = None;
710-
for v in [arg2, arg3, arg4] {
719+
let mut opts_arg: Option<usize> = None;
720+
let mut cb_arg: Option<usize> = None;
721+
for index in [1, 2, 3] {
722+
let v = rooted_args[index].get();
711723
if opt_host.is_none() {
712724
if let Some(h) = as_string(v) {
713725
opt_host = Some(h);
714726
continue;
715727
}
716728
}
717729
if is_closure(v) {
718-
cb = cb.or(Some(v));
730+
cb_arg = cb_arg.or(Some(index));
719731
} else if is_nanboxed_pointer(v) {
720-
opts = opts.or(Some(v));
732+
opts_arg = opts_arg.or(Some(index));
721733
}
722734
}
723-
if let Some(options) = opts {
735+
if let Some(index) = opts_arg {
724736
extern "C" {
725737
fn js_tls_validate_positional_connect_options(options: f64);
726738
}
727-
js_tls_validate_positional_connect_options(options);
739+
js_tls_validate_positional_connect_options(rooted_args[index].get());
728740
}
729741
host = opt_host
730742
.or_else(|| {
731-
opts.and_then(|o| {
732-
get_object_string_field(o, "host")
733-
.or_else(|| get_object_string_field(o, "hostname"))
743+
opts_arg.and_then(|index| {
744+
let options = rooted_args[index].get();
745+
get_object_string_field(options, "host")
746+
.or_else(|| get_object_string_field(rooted_args[index].get(), "hostname"))
734747
})
735748
})
736749
.filter(|h| !h.is_empty())
737750
.unwrap_or_else(|| "localhost".to_string());
738-
servername = opts
739-
.and_then(|o| get_object_string_field(o, "servername"))
751+
servername = opts_arg
752+
.and_then(|index| get_object_string_field(rooted_args[index].get(), "servername"))
740753
.unwrap_or_else(|| host.clone());
741-
verify = opts
742-
.and_then(|o| get_object_bool_field(o, "rejectUnauthorized"))
754+
verify = opts_arg
755+
.and_then(|index| get_object_bool_field(rooted_args[index].get(), "rejectUnauthorized"))
743756
.unwrap_or(true);
744-
cb_f64 = cb;
745-
metadata_options = opts.unwrap_or_else(|| f64::from_bits(0x7FFC_0000_0000_0001));
746-
} else if is_nanboxed_pointer(arg1) && !is_closure(arg1) {
757+
callback_arg = cb_arg;
758+
metadata_options_arg = opts_arg;
759+
} else if is_nanboxed_pointer(rooted_args[0].get()) && !is_closure(rooted_args[0].get()) {
747760
// Node options form: tls.connect(options[, callback]).
748761
extern "C" {
749762
fn js_tls_validate_connect_options(options: f64);
750763
}
751-
js_tls_validate_connect_options(arg1);
752-
if let Some(socket_value) = crate::get_object_value_field(arg1, "socket") {
764+
js_tls_validate_connect_options(rooted_args[0].get());
765+
if let Some(socket_value) = crate::get_object_value_field(rooted_args[0].get(), "socket") {
753766
let socket_js = JsValue::from_bits(socket_value.to_bits());
754767
let handle = if socket_js.is_pointer() {
755768
crate::unbox_pointer(socket_value) as i64
756769
} else {
757770
0
758771
};
759772
if handle != 0 {
760-
host = get_object_string_field(arg1, "host")
761-
.or_else(|| get_object_string_field(arg1, "hostname"))
773+
host = get_object_string_field(rooted_args[0].get(), "host")
774+
.or_else(|| get_object_string_field(rooted_args[0].get(), "hostname"))
762775
.unwrap_or_else(|| "localhost".to_string());
763-
servername =
764-
get_object_string_field(arg1, "servername").unwrap_or_else(|| host.clone());
765-
verify = get_object_bool_field(arg1, "rejectUnauthorized").unwrap_or(true);
766-
cb_f64 = is_closure(arg2).then_some(arg2);
767-
metadata_options = arg1;
768-
let config = tls_client_config_data(metadata_options);
776+
servername = get_object_string_field(rooted_args[0].get(), "servername")
777+
.unwrap_or_else(|| host.clone());
778+
verify = get_object_bool_field(rooted_args[0].get(), "rejectUnauthorized")
779+
.unwrap_or(true);
780+
callback_arg = is_closure(rooted_args[1].get()).then_some(1);
781+
let config = tls_client_config_data(rooted_args[0].get());
769782
extern "C" {
770783
fn js_tls_client_record_start(
771784
handle: i64,
@@ -776,12 +789,12 @@ pub unsafe extern "C" fn js_tls_connect(arg1: f64, arg2: f64, arg3: f64, arg4: f
776789
}
777790
js_tls_client_record_start(
778791
handle,
779-
metadata_options,
792+
rooted_args[0].get(),
780793
servername.as_ptr(),
781794
servername.len(),
782795
);
783-
if let Some(cb) = cb_f64 {
784-
let cb_ptr = unbox_pointer(cb) as i64;
796+
if let Some(index) = callback_arg {
797+
let cb_ptr = unbox_pointer(rooted_args[index].get()) as i64;
785798
if cb_ptr != 0 {
786799
statics::listeners()
787800
.lock()
@@ -793,7 +806,7 @@ pub unsafe extern "C" fn js_tls_connect(arg1: f64, arg2: f64, arg3: f64, arg4: f
793806
.push(cb_ptr);
794807
}
795808
}
796-
let preflight = tls_preflight(0, &servername, metadata_options);
809+
let preflight = tls_preflight(0, &servername, rooted_args[0].get());
797810
if preflight != 0 {
798811
crate::push_event(crate::PendingNetEvent::Error(
799812
handle,
@@ -807,29 +820,35 @@ pub unsafe extern "C" fn js_tls_connect(arg1: f64, arg2: f64, arg3: f64, arg4: f
807820
return handle;
808821
}
809822
}
810-
port = match get_object_number_field(arg1, "port") {
823+
port = match get_object_number_field(rooted_args[0].get(), "port") {
811824
Some(p) => {
812825
js_net_validate_connect_port(p);
813826
p as u16
814827
}
815828
None => return 0,
816829
};
817-
host = match get_object_string_field(arg1, "host")
818-
.or_else(|| get_object_string_field(arg1, "hostname"))
830+
host = match get_object_string_field(rooted_args[0].get(), "host")
831+
.or_else(|| get_object_string_field(rooted_args[0].get(), "hostname"))
819832
{
820833
Some(h) if !h.is_empty() => h,
821834
_ => "localhost".to_string(),
822835
};
823-
servername = get_object_string_field(arg1, "servername").unwrap_or_else(|| host.clone());
824-
verify = get_object_bool_field(arg1, "rejectUnauthorized").unwrap_or(true);
825-
cb_f64 = is_closure(arg2).then_some(arg2);
826-
metadata_options = arg1;
836+
servername = get_object_string_field(rooted_args[0].get(), "servername")
837+
.unwrap_or_else(|| host.clone());
838+
verify = get_object_bool_field(rooted_args[0].get(), "rejectUnauthorized").unwrap_or(true);
839+
callback_arg = is_closure(rooted_args[1].get()).then_some(1);
840+
metadata_options_arg = Some(0);
827841
} else {
828842
return 0;
829843
}
830844

831-
let config = tls_client_config_data(metadata_options);
832-
if signal_is_pre_aborted(metadata_options) {
845+
let metadata_options = || {
846+
metadata_options_arg
847+
.map(|index| rooted_args[index].get())
848+
.unwrap_or_else(|| f64::from_bits(0x7FFC_0000_0000_0001))
849+
};
850+
let config = tls_client_config_data(metadata_options());
851+
if signal_is_pre_aborted(metadata_options()) {
833852
let handle = crate::js_net_socket_alloc();
834853
extern "C" {
835854
fn js_tls_client_record_start(
@@ -841,14 +860,14 @@ pub unsafe extern "C" fn js_tls_connect(arg1: f64, arg2: f64, arg3: f64, arg4: f
841860
}
842861
js_tls_client_record_start(
843862
handle,
844-
metadata_options,
863+
metadata_options(),
845864
servername.as_ptr(),
846865
servername.len(),
847866
);
848867
schedule_tls_abort(handle);
849868
return handle;
850869
}
851-
let preflight = tls_preflight(port, &servername, metadata_options);
870+
let preflight = tls_preflight(port, &servername, metadata_options());
852871
if preflight != 0 {
853872
let handle = crate::js_net_socket_alloc();
854873
extern "C" {
@@ -861,7 +880,7 @@ pub unsafe extern "C" fn js_tls_connect(arg1: f64, arg2: f64, arg3: f64, arg4: f
861880
}
862881
js_tls_client_record_start(
863882
handle,
864-
metadata_options,
883+
metadata_options(),
865884
servername.as_ptr(),
866885
servername.len(),
867886
);
@@ -885,14 +904,14 @@ pub unsafe extern "C" fn js_tls_connect(arg1: f64, arg2: f64, arg3: f64, arg4: f
885904
}
886905
js_tls_client_record_start(
887906
handle,
888-
metadata_options,
907+
metadata_options(),
889908
metadata_servername.as_ptr(),
890909
metadata_servername.len(),
891910
);
892911
});
893-
if let Some(cb) = cb_f64 {
912+
if let Some(index) = callback_arg {
894913
if handle != 0 {
895-
let cb_ptr = unbox_pointer(cb) as i64;
914+
let cb_ptr = unbox_pointer(rooted_args[index].get()) as i64;
896915
if cb_ptr != 0 {
897916
statics::listeners()
898917
.lock()
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// parity-env: PERRY_GC_FORCE_EVACUATE=1 PERRY_GC_VERIFY_EVACUATION=1 PERRY_GC_PROTECT_FROMSPACE=1
2+
// `tls.connect` consults a user-replaced createSecureContext before it resolves
3+
// either overload. The callback forces a moving collection while the original
4+
// options object and secureConnect callback exist only in the external-net FFI
5+
// arguments. Both must be re-read from transient roots after the callback.
6+
import tls from "node:tls";
7+
import { readFileSync } from "node:fs";
8+
import { isIP } from "node:net";
9+
10+
declare function gc(): void;
11+
12+
const fixture = new URL("../test-parity/node-suite/tls/fixtures/", import.meta.url);
13+
const key = readFileSync(new URL("localhost-key.pem", fixture));
14+
const cert = readFileSync(new URL("localhost-cert.pem", fixture));
15+
const originalCreateSecureContext = tls.createSecureContext;
16+
17+
if (isIP("127.0.0.1") !== 4) throw new Error("node:net route unavailable");
18+
19+
(tls as any).createSecureContext = (options: any) => {
20+
const churn: Array<{ value: string }> = [];
21+
for (let i = 0; i < 2000; i++) churn.push({ value: "tls-root-" + i });
22+
if (typeof gc === "function") gc();
23+
return originalCreateSecureContext(options);
24+
};
25+
26+
const server = tls.createServer({ key, cert }, (socket) => socket.end());
27+
server.listen(0, "127.0.0.1", () => {
28+
const port = (server.address() as any).port;
29+
const options = { port, host: "127.0.0.1", rejectUnauthorized: false };
30+
const optionsClient = tls.connect(options, function () {
31+
console.log(
32+
"options:",
33+
this === optionsClient,
34+
options.host,
35+
options.rejectUnauthorized,
36+
);
37+
});
38+
optionsClient.on("close", () => {
39+
const positionalOptions = { rejectUnauthorized: false };
40+
const positionalClient = tls.connect(
41+
port,
42+
"127.0.0.1",
43+
positionalOptions,
44+
function () {
45+
console.log(
46+
"positional:",
47+
this === positionalClient,
48+
positionalOptions.rejectUnauthorized,
49+
);
50+
},
51+
);
52+
positionalClient.on("close", () => {
53+
(tls as any).createSecureContext = originalCreateSecureContext;
54+
server.close();
55+
});
56+
});
57+
});
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
options: true 127.0.0.1 false
2+
positional: true false

0 commit comments

Comments
 (0)