From 4b176c12f7159a92bc922a56f1db85f67e30cf91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 24 Aug 2026 12:11:48 +0200 Subject: [PATCH 1/2] fix(intl): close follow-up review gaps --- changelog.d/8659-follow-up-review-fixes.md | 5 ++ .../src/intl/list_relative_plural.rs | 18 ++++- .../src/object/class_registry/state.rs | 80 +++++++++++++++---- crates/perry-stdlib/src/net/mod.rs | 5 +- 4 files changed, 86 insertions(+), 22 deletions(-) create mode 100644 changelog.d/8659-follow-up-review-fixes.md diff --git a/changelog.d/8659-follow-up-review-fixes.md b/changelog.d/8659-follow-up-review-fixes.md new file mode 100644 index 0000000000..9c1d5fda95 --- /dev/null +++ b/changelog.d/8659-follow-up-review-fixes.md @@ -0,0 +1,5 @@ +### Fixed + +Validate fractional `Intl.PluralRules` digit options before flooring, reject +invalid superclass prototypes, and keep emitted network errors alive across +successive listeners. diff --git a/crates/perry-runtime/src/intl/list_relative_plural.rs b/crates/perry-runtime/src/intl/list_relative_plural.rs index 152a0557cb..935f12ed50 100644 --- a/crates/perry-runtime/src/intl/list_relative_plural.rs +++ b/crates/perry-runtime/src/intl/list_relative_plural.rs @@ -670,18 +670,21 @@ pub(crate) extern "C" fn rtf_bound_resolved_options_thunk(closure: *const Closur // ---- Intl.PluralRules ------------------------------------------------------ +fn plural_digit_integer(number: f64, min: f64, max: f64) -> Option { + (number.is_finite() && number >= min && number <= max).then(|| number.floor()) +} + fn plural_digit_option(options: f64, key: &str, min: f64, max: f64) -> Option { let value = get_option_value(options, key); if JSValue::from_bits(value.to_bits()).is_undefined() { return None; } let number = to_number_reject_bigint(value); - let integer = number.trunc(); - if integer.is_nan() || integer < min || integer > max { + let Some(integer) = plural_digit_integer(number, min, max) else { throw_range_error(&format!( "Value {number} out of range for Intl.PluralRules options property {key}" )); - } + }; Some(integer) } @@ -929,6 +932,15 @@ pub(crate) fn plural_rules_select(obj: *const ObjectHeader, value: f64) -> f64 { mod plural_category_tests { use super::*; + #[test] + fn digit_options_validate_before_flooring() { + assert_eq!(plural_digit_integer(20.9, 1.0, 21.0), Some(20.0)); + assert_eq!(plural_digit_integer(21.9, 1.0, 21.0), None); + assert_eq!(plural_digit_integer(100.5, 0.0, 100.0), None); + assert_eq!(plural_digit_integer(-0.5, 0.0, 100.0), None); + assert_eq!(plural_digit_integer(f64::INFINITY, 0.0, 100.0), None); + } + #[test] fn locale_selectors_only_return_advertised_categories() { let samples = [ diff --git a/crates/perry-runtime/src/object/class_registry/state.rs b/crates/perry-runtime/src/object/class_registry/state.rs index 6ace1cbc6d..cc78325683 100644 --- a/crates/perry-runtime/src/object/class_registry/state.rs +++ b/crates/perry-runtime/src/object/class_registry/state.rs @@ -632,6 +632,17 @@ fn install_class_decl_prototype_method_fields(proto: *mut ObjectHeader, class_id } } +fn class_parent_prototype_bits(value: f64) -> Option { + let bits = value.to_bits(); + if bits == crate::value::TAG_NULL { + return Some(bits); + } + if !unsafe { super::super::object_ops::value_is_object_like(value) } { + return None; + } + (unsafe { crate::symbol::js_is_symbol(value) } == 0).then_some(bits) +} + pub(crate) fn class_decl_prototype_value(class_id: u32) -> f64 { // #7757: a specialization answers with its generic's prototype. let class_id = decl_prototype_identity_id(class_id); @@ -710,8 +721,9 @@ pub(crate) fn class_decl_prototype_value(class_id: u32) -> f64 { unsafe { mirror_prototype_method_on_object(proto, &name, value_bits, enumerable) }; } - let dynamic_parent = js_get_dynamic_parent_value(class_id); - let null_heritage = dynamic_parent.to_bits() == crate::value::TAG_NULL; + let scope = crate::gc::RuntimeHandleScope::new(); + let dynamic_parent = scope.root_nanbox_f64(js_get_dynamic_parent_value(class_id)); + let null_heritage = dynamic_parent.get_nanbox_f64().to_bits() == crate::value::TAG_NULL; let parent_proto_bits = if null_heritage { // A class extending null creates a prototype object whose // [[Prototype]] is null, not Object.prototype. Record TAG_NULL @@ -719,33 +731,41 @@ pub(crate) fn class_decl_prototype_value(class_id: u32) -> f64 { // Object.prototype default. Some(crate::value::TAG_NULL) } else { - get_parent_class_id(class_id) + let registered_parent_proto = get_parent_class_id(class_id) .filter(|parent_id| *parent_id != 0 && *parent_id != class_id) .and_then(|parent_id| { let parent_proto = class_decl_prototype_value(parent_id); let parent_bits = parent_proto.to_bits(); ((parent_bits >> 48) == 0x7FFD).then_some(parent_bits) - }) + }); + if registered_parent_proto.is_some() { + registered_parent_proto + } else { // A runtime function-valued superclass (including Intl service // constructors) has no class-id edge. Link the declared prototype // to the parent's own `.prototype` exactly once, while this fresh // class prototype is initialized. Construction must never rewrite // this edge after user code mutates it. - .or_else(|| { - let parent = JSValue::from_bits(dynamic_parent.to_bits()); - if !parent.is_pointer() { - return None; - } + let parent = JSValue::from_bits(dynamic_parent.get_nanbox_f64().to_bits()); + if parent.is_pointer() { let parent_addr = parent.as_pointer::() as usize; - if !crate::closure::is_closure_ptr(parent_addr) { - return None; + if crate::closure::is_closure_ptr(parent_addr) { + let parent_proto = + crate::closure::closure_get_dynamic_prop(parent_addr, "prototype"); + if let Some(bits) = class_parent_prototype_bits(parent_proto) { + Some(bits) + } else { + super::super::object_ops::throw_object_type_error( + b"Class extends value does not have valid prototype property", + ); + } + } else { + global_object_prototype_bits() } - let parent_proto = - crate::closure::closure_get_dynamic_prop(parent_addr, "prototype"); - let bits = parent_proto.to_bits(); - ((bits >> 48) == 0x7FFD).then_some(bits) - }) - .or_else(global_object_prototype_bits) + } else { + global_object_prototype_bits() + } + } }; if let Some(bits) = parent_proto_bits { let proto = class_decl_prototype_object(class_id); @@ -905,3 +925,29 @@ mod class_dynamic_prop_store_tests { assert_eq!(stored(cid, "k"), Some(3.0)); } } + +#[cfg(test)] +mod class_parent_prototype_tests { + use super::*; + + #[test] + fn only_object_and_null_parent_prototypes_are_valid() { + let object_ptr = crate::object::js_object_alloc(0, 0); + assert!(!object_ptr.is_null()); + let object = crate::value::js_nanbox_pointer(object_ptr as i64); + assert_eq!(class_parent_prototype_bits(object), Some(object.to_bits())); + assert_eq!( + class_parent_prototype_bits(f64::from_bits(crate::value::POINTER_TAG | 0x1234)), + None + ); + assert_eq!( + class_parent_prototype_bits(f64::from_bits(crate::value::TAG_NULL)), + Some(crate::value::TAG_NULL) + ); + assert_eq!( + class_parent_prototype_bits(f64::from_bits(crate::value::TAG_UNDEFINED)), + None + ); + assert_eq!(class_parent_prototype_bits(1.0), None); + } +} diff --git a/crates/perry-stdlib/src/net/mod.rs b/crates/perry-stdlib/src/net/mod.rs index 66de8a8254..b2d832e096 100644 --- a/crates/perry-stdlib/src/net/mod.rs +++ b/crates/perry-stdlib/src/net/mod.rs @@ -1856,10 +1856,11 @@ pub unsafe extern "C" fn js_net_process_pending() -> i32 { // so user code can read `err.message`. Pre-fix the listener // received a raw NaN-boxed string and `err.message` came // back as `undefined`. - let err_f64 = build_error_object(&msg); + let scope = perry_runtime::gc::RuntimeHandleScope::new(); + let error = scope.root_nanbox_f64(build_error_object(&msg)); for cb in cbs { if cb != 0 { - js_closure_call1(cb as *const ClosureHeader, err_f64); + js_closure_call1(cb as *const ClosureHeader, error.get_nanbox_f64()); } } } From 6855f83cfbac812ffe5d83929b4adf97d5113772 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 24 Aug 2026 12:33:22 +0200 Subject: [PATCH 2/2] fix(ci): restore thread-local policy gate --- crates/perry-runtime/src/bun_ffi/read.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/perry-runtime/src/bun_ffi/read.rs b/crates/perry-runtime/src/bun_ffi/read.rs index 1a5e34cc94..27602374da 100644 --- a/crates/perry-runtime/src/bun_ffi/read.rs +++ b/crates/perry-runtime/src/bun_ffi/read.rs @@ -22,7 +22,7 @@ const READERS: &[(&str, u8)] = &[ ("f64", super::types::T_F64), ]; -thread_local! { +crate::perry_thread_local! { static READ_OBJECT_CACHE: Cell = const { Cell::new(0) }; }