Skip to content

Commit 8922c42

Browse files
proggeramlugRalph Küpper
andauthored
fix: clear the TLS and GC-spill release blockers; resolve DateTimeFormat locale options (#8759)
Lands #8754 and #8757. #8754 clears the `_js_tls_client_preflight` undefined-symbol compile failures in the gap and GC-stress suites, including `test_gap_gc_http2_pending_event_callback_rooting`. The stdlib TLS server/preflight provider is retained when `net`, `http`, `https` or `http2` routes to the optimized external wrappers, and the bundled and external `js_tls_connect` adapters are split so the external path no longer reintroduces duplicate net symbols. It also roots all four external TLS connect inputs across user-replaced `createSecureContext` and preflight callbacks -- both of which can run arbitrary user JS -- via `root_scope.root_nanbox(..)`, with a dedicated gap fixture and expected output (`test_issue_8754_tls_connect_args_gc_rooting`). The mixed-frame cargo integration test now matches the enriched root-spill diagnostic. That is a tightening rather than a relaxation: the asserted substring moves from "GC roots in a shadow frame" to the more specific "in a shadow frame instead of statepoints", and it still requires the spill to have happened. #8757 fixes `Intl.DateTimeFormat` locale option resolution, covered by `test_gap_intl_datetimeformat_locale_resolution_5899`. A changelog fragment was added for #8757; it had neither one nor a skip-changelog label. No version bump. Co-authored-by: Ralph Küpper <ralph@skelpo.com>
1 parent ed14ce5 commit 8922c42

19 files changed

Lines changed: 524 additions & 106 deletions
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Fixed optimized `node:net`, `node:http`, `node:https`, and `node:http2` builds
2+
after the TLS parity split. These external wrappers now retain Perry's shared
3+
TLS server and SNI/ALPN preflight provider without also linking the bundled
4+
network implementation, eliminating the `js_tls_client_preflight` undefined
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.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed `Intl.DateTimeFormat` locale option resolution so requested options are reflected in the resolved locale rather than dropped. Covered by `test_gap_intl_datetimeformat_locale_resolution_5899`.

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()

crates/perry-runtime/src/intl.rs

Lines changed: 42 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ mod locale;
3333
mod locales;
3434
use locales::{get_canonical_locales_thunk, supported_values_of_thunk};
3535
mod date_collator;
36+
mod date_time_locale;
37+
use date_time_locale::resolve_date_time_locale;
3638
mod date_names;
3739
#[cfg(feature = "intl-datetime")]
3840
pub(crate) mod icu_dtf;
@@ -1089,20 +1091,16 @@ fn make_instance(closure: *const ClosureHeader, kind: &str, locales: f64, option
10891091
&["lookup", "best fit"],
10901092
"best fit",
10911093
);
1092-
// `calendar` must match the Unicode locale `type` nonterminal; store
1093-
// the canonicalized ID so `resolvedOptions().calendar` reflects it.
1094-
if let Some(calendar) = get_locale_extension_option(current_options(), "calendar") {
1095-
match canonicalize_calendar_id(&calendar) {
1096-
Some(canonical) => set_internal_field_from_raw_handle(
1097-
&obj_handle,
1098-
KEY_CALENDAR,
1099-
string_value(&canonical),
1100-
),
1101-
None => throw_range_error(&format!(
1102-
"Value {calendar} out of range for Intl options property calendar"
1103-
)),
1104-
}
1105-
}
1094+
// `calendar` must match the Unicode locale `type` nonterminal.
1095+
// Unsupported well-formed values fall through ResolveLocale.
1096+
let calendar_option =
1097+
get_locale_extension_option(current_options(), "calendar").map(|calendar| {
1098+
canonicalize_calendar_id(&calendar).unwrap_or_else(|| {
1099+
throw_range_error(&format!(
1100+
"Value {calendar} out of range for Intl options property calendar"
1101+
))
1102+
})
1103+
});
11061104
// `numberingSystem` must be a well-formed `type` nonterminal. Read
11071105
// it here (preserving the GetOption order options-order.js asserts),
11081106
// then run ResolveLocale for `nu` — reconciling the option with the
@@ -1117,25 +1115,43 @@ fn make_instance(closure: *const ClosureHeader, kind: &str, locales: f64, option
11171115
}
11181116
ns.to_ascii_lowercase()
11191117
});
1120-
let (dtf_locale, dtf_numbering) =
1121-
resolve_numbering_system(&locale, dtf_opt_ns.as_deref());
1122-
set_internal_field_from_raw_handle(&obj_handle, KEY_LOCALE, string_value(&dtf_locale));
1123-
set_internal_field_from_raw_handle(
1124-
&obj_handle,
1125-
KEY_NUMBERING_SYSTEM,
1126-
string_value(&dtf_numbering),
1127-
);
11281118
// hour12 (boolean) then hourCycle (enum) — both only surface in
11291119
// `resolvedOptions` when the resolved pattern has an hour field.
1130-
if let Some(h12) = get_bool_option(current_options(), "hour12") {
1131-
set_internal_field_from_raw_handle(&obj_handle, KEY_HOUR12, bool_value(h12));
1132-
}
1133-
if let Some(hc) = get_option_string(current_options(), "hourCycle") {
1120+
let hour12 = get_bool_option(current_options(), "hour12");
1121+
let hour_cycle_option = get_option_string(current_options(), "hourCycle");
1122+
if let Some(ref hc) = hour_cycle_option {
11341123
if !["h11", "h12", "h23", "h24"].contains(&hc.as_str()) {
11351124
throw_range_error(&format!(
11361125
"Value {hc} out of range for Intl options property hourCycle"
11371126
));
11381127
}
1128+
}
1129+
let resolved = resolve_date_time_locale(
1130+
&locale,
1131+
calendar_option.as_deref(),
1132+
dtf_opt_ns.as_deref(),
1133+
hour12,
1134+
hour_cycle_option.as_deref(),
1135+
);
1136+
set_internal_field_from_raw_handle(
1137+
&obj_handle,
1138+
KEY_LOCALE,
1139+
string_value(&resolved.locale),
1140+
);
1141+
set_internal_field_from_raw_handle(
1142+
&obj_handle,
1143+
KEY_CALENDAR,
1144+
string_value(&resolved.calendar),
1145+
);
1146+
set_internal_field_from_raw_handle(
1147+
&obj_handle,
1148+
KEY_NUMBERING_SYSTEM,
1149+
string_value(&resolved.numbering_system),
1150+
);
1151+
if let Some(h12) = hour12 {
1152+
set_internal_field_from_raw_handle(&obj_handle, KEY_HOUR12, bool_value(h12));
1153+
}
1154+
if let Some(hc) = resolved.hour_cycle {
11391155
set_internal_field_from_raw_handle(&obj_handle, KEY_HOUR_CYCLE, string_value(&hc));
11401156
}
11411157
// ECMA-402 DefaultTimeZone(): when no `timeZone` option is given, use

0 commit comments

Comments
 (0)