diff --git a/changelog.d/9894-perf-hooks-validation.md b/changelog.d/9894-perf-hooks-validation.md new file mode 100644 index 0000000000..334a1b9231 --- /dev/null +++ b/changelog.d/9894-perf-hooks-validation.md @@ -0,0 +1,3 @@ +Make `Performance` methods reject invalid receivers and preserve +`ERR_ILLEGAL_CONSTRUCTOR` when histogram constructor values are invoked with +`new`. diff --git a/crates/perry-runtime/src/object/class_registry/construct.rs b/crates/perry-runtime/src/object/class_registry/construct.rs index 597cac4f79..15ec9f744a 100644 --- a/crates/perry-runtime/src/object/class_registry/construct.rs +++ b/crates/perry-runtime/src/object/class_registry/construct.rs @@ -331,6 +331,11 @@ pub unsafe extern "C-unwind" fn js_new_function_construct( return result; } } + if module == "perf_histogram" + && matches!(method.as_str(), "RecordableHistogram" | "ELDHistogram") + { + return crate::perf_hooks::js_perf_illegal_constructor(); + } if module == "sqlite" && matches!( method.as_str(), diff --git a/crates/perry-runtime/src/object/native_module.rs b/crates/perry-runtime/src/object/native_module.rs index c27e5d5ffe..4c2b58c61b 100644 --- a/crates/perry-runtime/src/object/native_module.rs +++ b/crates/perry-runtime/src/object/native_module.rs @@ -23,7 +23,7 @@ mod callable_export_check; mod callable_export_table; pub(crate) mod callable_exports; mod perf_instance_bind; -pub(crate) use perf_instance_bind::instance_bound_perf_method; +pub(crate) use perf_instance_bind::{instance_bound_perf_method, performance_namespace_method}; mod constants; mod constants_tables; mod constructor_exports; @@ -1194,6 +1194,12 @@ pub extern "C" fn js_native_module_bind_method( } } + if let Some(value) = + performance_namespace_method(&module_name, property_name, namespace.get_nanbox_f64()) + { + return value; + } + // Check for known constant properties first if let Some(val) = unsafe { get_native_module_constant(&module_name, property_name, namespace.get_nanbox_f64()) @@ -1833,6 +1839,9 @@ unsafe fn vt_get_own_field( if let Some(value) = super::field_get_set::native_module_own_field_by_key(obj, key) { return Some(value); } + if let Some(value) = performance_namespace_method(&module_name, property_name, nb_ptr) { + return Some(JSValue::from_bits(value.to_bits())); + } // #3687: node:cluster default-import EventEmitter methods on the // distinct `cluster.default` namespace (see original comment at the // pre-relocation site in field_get_set.rs history). diff --git a/crates/perry-runtime/src/object/native_module/constructor_exports.rs b/crates/perry-runtime/src/object/native_module/constructor_exports.rs index f0f670e57c..c61960817b 100644 --- a/crates/perry-runtime/src/object/native_module/constructor_exports.rs +++ b/crates/perry-runtime/src/object/native_module/constructor_exports.rs @@ -16,6 +16,13 @@ pub(crate) fn is_native_module_constructor_export(module: &str, property: &str) let module = normalize_native_module_alias(module); let property = canonical_native_callable_property(module, property); + // Histogram constructors are only reachable through an instance's + // `constructor` property. They are callable-shaped internal exports, and + // their construct path deliberately throws ERR_ILLEGAL_CONSTRUCTOR. + if module == "perf_histogram" && matches!(property, "RecordableHistogram" | "ELDHistogram") { + return true; + } + if !is_native_module_callable_export(module, property) { return false; } @@ -207,4 +214,16 @@ mod tests { "WriteStream" )); } + + #[test] + fn histogram_class_values_are_constructor_shaped() { + assert!(is_native_module_constructor_export( + "perf_histogram", + "RecordableHistogram" + )); + assert!(is_native_module_constructor_export( + "perf_histogram", + "ELDHistogram" + )); + } } diff --git a/crates/perry-runtime/src/object/native_module/perf_instance_bind.rs b/crates/perry-runtime/src/object/native_module/perf_instance_bind.rs index 55ecbdc210..eeff7cb3e3 100644 --- a/crates/perry-runtime/src/object/native_module/perf_instance_bind.rs +++ b/crates/perry-runtime/src/object/native_module/perf_instance_bind.rs @@ -54,3 +54,18 @@ pub(crate) fn instance_bound_perf_method( name.len(), )) } + +/// Return the receiver-aware method installed on `Performance.prototype` for +/// reads from the canonical `performance` singleton. The singleton shares the +/// `perf_hooks` dispatch tag with the module namespace, so identity distinguishes +/// these methods from ordinary native-module exports. +pub(crate) fn performance_namespace_method( + module_name: &str, + property_name: &str, + receiver: f64, +) -> Option { + if module_name != "perf_hooks" || !crate::perf_hooks::is_performance_namespace_value(receiver) { + return None; + } + crate::perf_hooks::performance_prototype_method_value(property_name) +} diff --git a/crates/perry-runtime/src/perf_hooks.rs b/crates/perry-runtime/src/perf_hooks.rs index c737dcadd5..68690d9d80 100644 --- a/crates/perry-runtime/src/perf_hooks.rs +++ b/crates/perry-runtime/src/perf_hooks.rs @@ -38,7 +38,10 @@ use std::time::{Instant, SystemTime, UNIX_EPOCH}; mod prototypes; -pub(crate) use prototypes::{attach_perf_hooks_constructor, perf_supported_entry_types_value}; +pub(crate) use prototypes::{ + attach_perf_hooks_constructor, perf_supported_entry_types_value, + performance_prototype_method_value, +}; use prototypes::{is_perf_constructor_name, link_perf_prototype}; const ENTRY_TYPE_MARK: u8 = 0; @@ -262,6 +265,17 @@ pub(crate) fn is_performance_object_value(value: f64) -> bool { false } +/// True only for the canonical `performance` singleton. The broader +/// `is_performance_object_value` predicate also accepts legacy perf-hooks +/// namespace objects for `instanceof` compatibility, but Performance +/// prototype methods require the object's actual internal brand. +pub(crate) fn is_performance_namespace_value(value: f64) -> bool { + PERFORMANCE_NS.with(|c| { + let cached = c.get(); + cached != 0 && cached == value.to_bits() + }) +} + pub(crate) fn is_perf_observer_list_value(value: f64) -> bool { unsafe { let Some(obj) = as_object_ptr(value) else { diff --git a/crates/perry-runtime/src/perf_hooks/prototypes.rs b/crates/perry-runtime/src/perf_hooks/prototypes.rs index 768eaaaec1..b10d16a729 100644 --- a/crates/perry-runtime/src/perf_hooks/prototypes.rs +++ b/crates/perry-runtime/src/perf_hooks/prototypes.rs @@ -1,5 +1,12 @@ use super::*; +mod performance_methods; +use performance_methods::{ + clear_marks, clear_measures, clear_resource_timings, event_loop_utilization, get_entries, + get_entries_by_name, get_entries_by_type, mark, mark_resource_timing, measure, now, + set_resource_timing_buffer_size, timerify, to_json, +}; + const PERF_CONSTRUCTOR_NAMES: &[&str] = &[ "Performance", "PerformanceEntry", @@ -216,6 +223,41 @@ fn perf_constructor_prototype(class_name: &str) -> f64 { crate::closure::closure_get_dynamic_prop(ptr, "prototype") } +/// Return the shared method installed on `Performance.prototype`. +/// +/// The `performance` object uses the same native-module tag as the top-level +/// `perf_hooks` namespace, whose generic bind path creates module-bound +/// closures. Route reads on the exact singleton back through its prototype so +/// extracted methods retain their receiver checks. +pub(crate) fn performance_prototype_method_value(name: &str) -> Option { + if !matches!( + name, + "clearMarks" + | "clearMeasures" + | "clearResourceTimings" + | "getEntries" + | "getEntriesByName" + | "getEntriesByType" + | "mark" + | "measure" + | "now" + | "setResourceTimingBufferSize" + | "toJSON" + | "eventLoopUtilization" + | "markResourceTiming" + | "timerify" + ) { + return None; + } + let scope = crate::gc::RuntimeHandleScope::new(); + let proto = scope.root_nanbox_f64(perf_constructor_prototype("Performance")); + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + let obj = + JSValue::from_bits(proto.get_nanbox_u64()).as_pointer::(); + let value = js_object_get_field_by_name(obj, key); + (value.bits() != crate::value::TAG_UNDEFINED).then(|| f64::from_bits(value.bits())) +} + /// Link runtime-created perf objects through their built-in class hierarchy. /// /// This is class-default wiring, not a user `Object.setPrototypeOf` override. @@ -261,25 +303,59 @@ pub(crate) unsafe fn attach_perf_hooks_constructor( match class_name { "Performance" => { - for method in [ - "clearMarks", - "clearMeasures", - "clearResourceTimings", - "getEntries", - "getEntriesByName", - "getEntriesByType", - "mark", - "measure", - "now", - "setResourceTimingBufferSize", - "toJSON", - ] { - let value = crate::object::bound_native_callable_export_value("perf_hooks", method); - install_perf_method(proto, method, value, true); - } - for method in ["eventLoopUtilization", "markResourceTiming", "timerify"] { - let value = crate::object::bound_native_callable_export_value("perf_hooks", method); - install_perf_method(proto, method, value, false); + let methods = [ + ("clearMarks", clear_marks as *const u8, 0, true), + ("clearMeasures", clear_measures as *const u8, 0, true), + ( + "clearResourceTimings", + clear_resource_timings as *const u8, + 0, + true, + ), + ("getEntries", get_entries as *const u8, 0, true), + ( + "getEntriesByName", + get_entries_by_name as *const u8, + 1, + true, + ), + ( + "getEntriesByType", + get_entries_by_type as *const u8, + 1, + true, + ), + ("mark", mark as *const u8, 1, true), + ("measure", measure as *const u8, 1, true), + ("now", now as *const u8, 0, true), + ( + "setResourceTimingBufferSize", + set_resource_timing_buffer_size as *const u8, + 1, + true, + ), + ("toJSON", to_json as *const u8, 0, true), + ( + "eventLoopUtilization", + event_loop_utilization as *const u8, + 2, + false, + ), + ( + "markResourceTiming", + mark_resource_timing as *const u8, + 7, + false, + ), + ("timerify", timerify as *const u8, 1, false), + ]; + for (method, thunk, arity, enumerable) in methods { + install_perf_method( + proto, + method, + perf_method_value(thunk, method, arity), + enumerable, + ); } let getter = perf_method_value( perf_time_origin_getter_thunk as *const u8, diff --git a/crates/perry-runtime/src/perf_hooks/prototypes/performance_methods.rs b/crates/perry-runtime/src/perf_hooks/prototypes/performance_methods.rs new file mode 100644 index 0000000000..a1d6ad18a9 --- /dev/null +++ b/crates/perry-runtime/src/perf_hooks/prototypes/performance_methods.rs @@ -0,0 +1,133 @@ +//! Receiver-aware `Performance.prototype` method thunks. + +use super::*; + +fn require_performance_receiver() { + if !is_performance_namespace_value(crate::object::js_implicit_this_get()) { + invalid_perf_receiver("Performance"); + } +} + +pub(super) extern "C" fn clear_marks( + _closure: *const crate::closure::ClosureHeader, + name: f64, +) -> f64 { + require_performance_receiver(); + js_perf_clear_marks(name) +} + +pub(super) extern "C" fn clear_measures( + _closure: *const crate::closure::ClosureHeader, + name: f64, +) -> f64 { + require_performance_receiver(); + js_perf_clear_measures(name) +} + +pub(super) extern "C" fn clear_resource_timings( + _closure: *const crate::closure::ClosureHeader, +) -> f64 { + require_performance_receiver(); + js_perf_clear_resource_timings() +} + +pub(super) extern "C" fn get_entries(_closure: *const crate::closure::ClosureHeader) -> f64 { + require_performance_receiver(); + js_perf_get_entries() +} + +pub(super) extern "C" fn get_entries_by_name( + _closure: *const crate::closure::ClosureHeader, + name: f64, + entry_type: f64, +) -> f64 { + require_performance_receiver(); + js_perf_get_entries_by_name(name, entry_type) +} + +pub(super) extern "C" fn get_entries_by_type( + _closure: *const crate::closure::ClosureHeader, + entry_type: f64, +) -> f64 { + require_performance_receiver(); + js_perf_get_entries_by_type(entry_type) +} + +pub(super) extern "C" fn mark( + _closure: *const crate::closure::ClosureHeader, + name: f64, + options: f64, +) -> f64 { + require_performance_receiver(); + js_perf_mark(name, options) +} + +pub(super) extern "C" fn measure( + _closure: *const crate::closure::ClosureHeader, + name: f64, + start_or_options: f64, + end: f64, +) -> f64 { + require_performance_receiver(); + js_perf_measure(name, start_or_options, end) +} + +pub(super) extern "C" fn now(_closure: *const crate::closure::ClosureHeader) -> f64 { + require_performance_receiver(); + crate::date::js_performance_now() +} + +pub(super) extern "C" fn set_resource_timing_buffer_size( + _closure: *const crate::closure::ClosureHeader, + size: f64, +) -> f64 { + require_performance_receiver(); + js_perf_set_resource_timing_buffer_size(size) +} + +pub(super) extern "C" fn to_json(_closure: *const crate::closure::ClosureHeader) -> f64 { + require_performance_receiver(); + js_perf_to_json() +} + +pub(super) extern "C" fn event_loop_utilization( + _closure: *const crate::closure::ClosureHeader, + utilization1: f64, + utilization2: f64, +) -> f64 { + require_performance_receiver(); + js_perf_event_loop_utilization(utilization1, utilization2) +} + +pub(super) extern "C" fn mark_resource_timing( + _closure: *const crate::closure::ClosureHeader, + timing_info: f64, + requested_url: f64, + initiator_type: f64, + global: f64, + cache_mode: f64, + body_info: f64, + response_status: f64, + delivery_type: f64, +) -> f64 { + require_performance_receiver(); + js_perf_mark_resource_timing( + timing_info, + requested_url, + initiator_type, + global, + cache_mode, + body_info, + response_status, + delivery_type, + ) +} + +pub(super) extern "C" fn timerify( + _closure: *const crate::closure::ClosureHeader, + function: f64, + options: f64, +) -> f64 { + require_performance_receiver(); + js_perf_timerify(function, options) +} diff --git a/test-parity/node-suite/perf_hooks/shapes/receiver-branding.ts b/test-parity/node-suite/perf_hooks/shapes/receiver-branding.ts index 49a9c984e9..e5986fdf30 100644 --- a/test-parity/node-suite/perf_hooks/shapes/receiver-branding.ts +++ b/test-parity/node-suite/perf_hooks/shapes/receiver-branding.ts @@ -19,6 +19,14 @@ outcome( "performance.mark", () => Reflect.apply(Object.getPrototypeOf(performance).mark, {}, ["x"]), ); +outcome( + "performance.now direct", + () => Reflect.apply(performance.now, {}, []), +); +outcome( + "performance.clearMarks direct", + () => Reflect.apply(performance.clearMarks, {}, []), +); outcome( "entry.toJSON", () => Reflect.apply(PerformanceEntry.prototype.toJSON, {}, []),