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
5 changes: 5 additions & 0 deletions changelog.d/8659-follow-up-review-fixes.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion crates/perry-runtime/src/bun_ffi/read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const READERS: &[(&str, u8)] = &[
("f64", super::types::T_F64),
];

thread_local! {
crate::perry_thread_local! {
static READ_OBJECT_CACHE: Cell<u64> = const { Cell::new(0) };
}

Expand Down
18 changes: 15 additions & 3 deletions crates/perry-runtime/src/intl/list_relative_plural.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<f64> {
(number.is_finite() && number >= min && number <= max).then(|| number.floor())
}

fn plural_digit_option(options: f64, key: &str, min: f64, max: f64) -> Option<f64> {
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)
}

Expand Down Expand Up @@ -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 = [
Expand Down
80 changes: 63 additions & 17 deletions crates/perry-runtime/src/object/class_registry/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,17 @@ fn install_class_decl_prototype_method_fields(proto: *mut ObjectHeader, class_id
}
}

fn class_parent_prototype_bits(value: f64) -> Option<u64> {
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);
Expand Down Expand Up @@ -710,42 +721,51 @@ 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
// explicitly so "no custom link" is not mistaken for the ordinary
// 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::<u8>() 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);
Expand Down Expand Up @@ -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);
}
}
5 changes: 3 additions & 2 deletions crates/perry-stdlib/src/net/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
}
Expand Down
Loading