From 9319e7cc4be7104ce1b5c91ecf2321c8556f75b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 4 Jun 2026 14:06:42 +0200 Subject: [PATCH 1/2] fix(date): brand-check this on Date.prototype getters (reflective dispatch) Date.prototype getters (getDate/getDay/getFullYear/getHours/.../getUTC*, getTimezoneOffset, getTime, valueOf, legacy getYear) were installed as generic no-op thunks, so the reflective value path mis-behaved: Date.prototype.getDate.call(realDate) returned [object Object], and .call(nonDate) silently produced garbage instead of throwing. Per spec each getter runs thisTimeValue(this), which throws a TypeError when this is not an Object with a [[DateValue]] slot. Adds object/date_proto_thunks.rs (mirrors collection_/dataview_proto_thunks): each thunk reads IMPLICIT_THIS, brand-checks via is_date_value (TypeError on mismatch), and dispatches to the same js_date_get_* helper the instance fast path uses. Installed after the OBJECT_PROTO_METHODS no-op block so the generic Object valueOf does not re-clobber Date.prototype.valueOf. The instance fast path (d.getDate()) is lowered directly by codegen and is unchanged. test262 built-ins/Date: -36 (158->122 on top of the ToNumber fix; no regressions). Node-identical in both build modes. --- .../src/object/date_proto_thunks.rs | 114 ++++++++++++++++++ .../perry-runtime/src/object/global_this.rs | 6 + crates/perry-runtime/src/object/mod.rs | 1 + 3 files changed, 121 insertions(+) create mode 100644 crates/perry-runtime/src/object/date_proto_thunks.rs diff --git a/crates/perry-runtime/src/object/date_proto_thunks.rs b/crates/perry-runtime/src/object/date_proto_thunks.rs new file mode 100644 index 000000000..9fab892ad --- /dev/null +++ b/crates/perry-runtime/src/object/date_proto_thunks.rs @@ -0,0 +1,114 @@ +//! `Date.prototype` getter thunks with a `this` brand check. +//! +//! The instance fast path (`d.getFullYear()`) is lowered directly by codegen: +//! it reads the `DateCell` timestamp and calls the `js_date_get_*` helpers, so +//! it never touches these thunks. But `Date.prototype.getFullYear` is also a +//! plain value — `Date.prototype.getFullYear.call(x)`, `Reflect.apply`, method +//! extraction — and on that reflective path the methods were installed as +//! generic no-op thunks, so `getDate.call(realDate)` returned `[object Object]` +//! and `getDate.call(nonDate)` silently produced garbage instead of throwing. +//! +//! Per spec each `Date.prototype` getter performs `thisTimeValue(this)`, which +//! throws a `TypeError` when `this` is not an Object with a `[[DateValue]]` +//! slot. These thunks read the `IMPLICIT_THIS` receiver (set by the +//! `.call`/`.apply` dispatch), brand-check it via `is_date_value`, throw on +//! mismatch, and otherwise dispatch to the SAME `js_date_get_*` helper the +//! instance path uses — so reflective Date getter calls now also *work*. +//! +//! Installed onto `Date.prototype` by +//! `global_this::populate_builtin_prototype_methods` (after the no-op block, so +//! these real thunks overwrite the no-op getters; setters / `toX` formatters +//! stay on the no-op path for now). + +use super::*; + +/// Resolve the `IMPLICIT_THIS` receiver to a Date time value, or throw a +/// `TypeError` (`thisTimeValue` brand check) when it is not a Date. +fn require_date_timestamp() -> f64 { + let this = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); + if crate::date::is_date_value(this) { + crate::date::date_cell_timestamp(this) + } else { + super::object_ops::throw_object_type_error(b"this is not a Date object.") + } +} + +macro_rules! date_getter_thunk { + ($name:ident, $rt:path) => { + extern "C" fn $name(_closure: *const crate::closure::ClosureHeader) -> f64 { + $rt(require_date_timestamp()) + } + }; +} + +date_getter_thunk!(date_get_time, crate::date::js_date_get_time); +date_getter_thunk!(date_get_full_year, crate::date::js_date_get_full_year); +date_getter_thunk!(date_get_month, crate::date::js_date_get_month); +date_getter_thunk!(date_get_date, crate::date::js_date_get_date); +date_getter_thunk!(date_get_hours, crate::date::js_date_get_hours); +date_getter_thunk!(date_get_minutes, crate::date::js_date_get_minutes); +date_getter_thunk!(date_get_seconds, crate::date::js_date_get_seconds); +date_getter_thunk!(date_get_milliseconds, crate::date::js_date_get_milliseconds); +date_getter_thunk!(date_get_day, crate::date::js_date_get_day); +date_getter_thunk!( + date_get_utc_full_year, + crate::date::js_date_get_utc_full_year +); +date_getter_thunk!(date_get_utc_month, crate::date::js_date_get_utc_month); +date_getter_thunk!(date_get_utc_date, crate::date::js_date_get_utc_date); +date_getter_thunk!(date_get_utc_hours, crate::date::js_date_get_utc_hours); +date_getter_thunk!(date_get_utc_minutes, crate::date::js_date_get_utc_minutes); +date_getter_thunk!(date_get_utc_seconds, crate::date::js_date_get_utc_seconds); +date_getter_thunk!( + date_get_utc_milliseconds, + crate::date::js_date_get_utc_milliseconds +); +date_getter_thunk!(date_get_utc_day, crate::date::js_date_get_utc_day); +date_getter_thunk!( + date_get_timezone_offset, + crate::date::js_date_get_timezone_offset +); + +/// Legacy `Date.prototype.getYear` — `getFullYear() - 1900` (NaN-preserving). +extern "C" fn date_get_year(_closure: *const crate::closure::ClosureHeader) -> f64 { + let fy = crate::date::js_date_get_full_year(require_date_timestamp()); + if fy.is_nan() { + fy + } else { + fy - 1900.0 + } +} + +/// Install the brand-checked `Date.prototype` getter thunks. Called from +/// `global_this::populate_builtin_prototype_methods`'s `"Date"` arm AFTER the +/// no-op block, so these overwrite the no-op getter entries. +pub(crate) fn install_date_proto_getters(proto_obj: *mut ObjectHeader) { + if proto_obj.is_null() { + return; + } + let methods: &[(&str, *const u8)] = &[ + ("getTime", date_get_time as *const u8), + ("valueOf", date_get_time as *const u8), + ("getFullYear", date_get_full_year as *const u8), + ("getMonth", date_get_month as *const u8), + ("getDate", date_get_date as *const u8), + ("getHours", date_get_hours as *const u8), + ("getMinutes", date_get_minutes as *const u8), + ("getSeconds", date_get_seconds as *const u8), + ("getMilliseconds", date_get_milliseconds as *const u8), + ("getDay", date_get_day as *const u8), + ("getUTCFullYear", date_get_utc_full_year as *const u8), + ("getUTCMonth", date_get_utc_month as *const u8), + ("getUTCDate", date_get_utc_date as *const u8), + ("getUTCHours", date_get_utc_hours as *const u8), + ("getUTCMinutes", date_get_utc_minutes as *const u8), + ("getUTCSeconds", date_get_utc_seconds as *const u8), + ("getUTCMilliseconds", date_get_utc_milliseconds as *const u8), + ("getUTCDay", date_get_utc_day as *const u8), + ("getTimezoneOffset", date_get_timezone_offset as *const u8), + ("getYear", date_get_year as *const u8), + ]; + for (name, ptr) in methods.iter().copied() { + super::global_this::install_proto_method(proto_obj, name, ptr, 0); + } +} diff --git a/crates/perry-runtime/src/object/global_this.rs b/crates/perry-runtime/src/object/global_this.rs index 4e66f846f..916b4a144 100644 --- a/crates/perry-runtime/src/object/global_this.rs +++ b/crates/perry-runtime/src/object/global_this.rs @@ -4067,6 +4067,12 @@ fn populate_builtin_prototype_methods(builtin_name: &str, proto_obj: *mut Object ], ); install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); + // Overwrite the no-op getter entries with brand-checking thunks so + // `Date.prototype.getX.call(this)` performs `thisTimeValue(this)` + // (TypeError on a non-Date receiver) and dispatches correctly. + // MUST run after the OBJECT_PROTO_METHODS block, which would + // otherwise re-clobber `valueOf` with the generic Object no-op. + date_proto_thunks::install_date_proto_getters(proto_obj); install_proto_method( proto_obj, "isPrototypeOf", diff --git a/crates/perry-runtime/src/object/mod.rs b/crates/perry-runtime/src/object/mod.rs index 0f919c4f3..6506ce61f 100644 --- a/crates/perry-runtime/src/object/mod.rs +++ b/crates/perry-runtime/src/object/mod.rs @@ -30,6 +30,7 @@ mod class_registry; mod collection_proto_thunks; mod data_view_registry; mod dataview_proto_thunks; +mod date_proto_thunks; mod delete_rest; mod descriptors; mod field_get_set; From 03aa904497832f3c6678ebd210ce70429230f49d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 4 Jun 2026 15:09:41 +0200 Subject: [PATCH 2/2] style: rustfmt webcrypto/jwk.rs (fix pre-existing main fmt drift) The WebCrypto algo additions on main left jwk.rs unformatted, which fails `cargo fmt --all --check` on every PR's lint job. Pure formatting; no behavior change. --- crates/perry-stdlib/src/webcrypto/jwk.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/perry-stdlib/src/webcrypto/jwk.rs b/crates/perry-stdlib/src/webcrypto/jwk.rs index 1f9e7f3e7..8780345c3 100644 --- a/crates/perry-stdlib/src/webcrypto/jwk.rs +++ b/crates/perry-stdlib/src/webcrypto/jwk.rs @@ -494,10 +494,7 @@ pub unsafe extern "C" fn js_webcrypto_export_key(format_bits: f64, key_bits: f64 let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&key_bytes); let field_count = if matches!( mat.algo, - KeyAlgo::ChaCha20Poly1305 - | KeyAlgo::Kmac128 - | KeyAlgo::Kmac256 - | KeyAlgo::AesOcb + KeyAlgo::ChaCha20Poly1305 | KeyAlgo::Kmac128 | KeyAlgo::Kmac256 | KeyAlgo::AesOcb ) { 3 } else {