From 41bebaa05a2061dfa1d7f0b205dcb4e05a1ad92e Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Fri, 18 Sep 2026 03:01:53 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=95=B0=E7=BB=84=E9=95=BF=E5=BA=A6?= =?UTF-8?q?=E8=B5=8B=E5=80=BC=E8=B5=B0=E7=BB=9F=E4=B8=80=E6=A3=80=E6=9F=A5?= =?UTF-8?q?=E5=B9=B6=E6=94=AF=E6=8C=81=E9=83=A8=E5=88=86=E6=88=AA=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_builtins/src/array/element.rs | 182 +++++++++++--- crates/oxide_vm/src/vm/vmhost.rs | 3 +- crates/oxide_vm/src/vm_props.rs | 235 +++++++++++------- .../oxide_vm/tests/array_length_set_tests.rs | 189 ++++++++++++++ .../tests/property_write_protection_tests.rs | 2 +- 5 files changed, 484 insertions(+), 127 deletions(-) create mode 100644 crates/oxide_vm/tests/array_length_set_tests.rs diff --git a/crates/oxide_builtins/src/array/element.rs b/crates/oxide_builtins/src/array/element.rs index c97432a2..7fb95def 100644 --- a/crates/oxide_builtins/src/array/element.rs +++ b/crates/oxide_builtins/src/array/element.rs @@ -9,37 +9,81 @@ use crate::builtins_debug; use crate::builtins_error; use super::common::{ - array_ptr, arraylike_get, arraylike_get_or_err, check_array_create_len, clamp_relative, create_new_array, - get_this_array_ref, get_this_arraylike, js_array_index, to_integer_or_infinity_bounded, + array_ptr, array_type_error, arraylike_get, arraylike_get_or_err, check_array_create_len, clamp_relative, + create_new_array, get_this_array_ref, get_this_arraylike, js_array_index, to_integer_or_infinity_bounded, }; use super::from::{array_species_create, create_data_property_or_throw, from_engine_error}; /// `Array.prototype.push(...items)`:追加元素到尾部,返回新长度。 +/// +/// # 步骤 +/// 1. 读 `LengthOfArrayLike(this)` 作起始索引。 +/// 2. 每个实参按当前索引生成属性键,以严格模式 `[[Set]]` 写入(继承 setter / +/// 不可写元素按规范抛 TypeError),写入成功后才递增索引。 +/// 3. 以 `Set(this, "length", len, true)` 收尾,length 不可写时抛 TypeError。 +/// +/// # 副作用 +/// - 修改元素区与 length;元素写入可经原型链触发用户 setter。 pub fn array_push(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("Array.prototype.push called with {} args", args.len()); let arr_ptr = array_ptr!(vm, args); + let recv = JsValue::from_js_object(arr_ptr); + let mut len = unsafe { &*arr_ptr }.logical_len() as usize; + for &arg_reg in args.iter().skip(1) { - let val = vm.promote_if_needed_for_write_ptr(arr_ptr, vm.reg(arg_reg)); - // 元素写入须维护 array_prop_count(set_prop_at 对数组自动更新)。 - let arr = unsafe { &mut *arr_ptr }; - let idx = arr.prop_count(); - arr.set_prop_at(idx, val); + let key = vm.string_key_si(&len.to_string()); + if let Err(err) = vm.ordinary_set(unsafe { &mut *arr_ptr }, key, vm.reg(arg_reg), recv, true) { + return NativeResult::Err(from_engine_error(vm, &err)); + } + len += 1; } - let len = unsafe { &*arr_ptr }.prop_count(); - NativeResult::Ok(JsValue::int(len as i32)) + + // length 收尾同样走 Set:不可写 length 在此抛 TypeError。 + let length_si = vm.string_key_si("length"); + if let Err(err) = vm.ordinary_set(unsafe { &mut *arr_ptr }, length_si, js_array_index(len), recv, true) { + return NativeResult::Err(from_engine_error(vm, &err)); + } + NativeResult::Ok(js_array_index(len)) } /// `Array.prototype.pop()`:移除并返回末位元素;空数组返回 undefined。 +/// +/// # 步骤 +/// 1. 读 `LengthOfArrayLike(this)`;为 0 时以 `Set(this, "length", +0, true)` 收尾。 +/// 2. 否则按 Get / DeletePropertyOrThrow / Set length 的顺序处理末位元素。 +/// +/// # 副作用 +/// - 删除末位元素并收缩 length;Get 可触发原型链 getter,length 不可写时抛 TypeError。 pub fn array_pop(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("Array.prototype.pop called with {} args", args.len()); let arr_ptr = array_ptr!(vm, args); - let arr = unsafe { &mut *arr_ptr }; - let len = arr.prop_count(); + let recv = JsValue::from_js_object(arr_ptr); + let len = unsafe { &*arr_ptr }.logical_len() as usize; + let length_si = vm.string_key_si("length"); if len == 0 { + if let Err(err) = vm.ordinary_set(unsafe { &mut *arr_ptr }, length_si, JsValue::int(0), recv, true) { + return NativeResult::Err(from_engine_error(vm, &err)); + } return NativeResult::Ok(JsValue::undefined()); } - let last = arr.get_prop_at(len - 1); - arr.set_prop_count_fast(len - 1); + let index = len - 1; + let key = vm.string_key_si(&index.to_string()); + let last = match vm.ordinary_get(unsafe { &*arr_ptr }, key, recv) { + Ok(v) => v, + Err(msg) => return NativeResult::Err(from_engine_error(vm, &msg)), + }; + + // DeletePropertyOrThrow:不可配置元素删除失败抛 TypeError,否则标记为 hole。 + match unsafe { &*arr_ptr }.prop_meta_at(index) { + Some(meta) if !meta.attributes.configurable() => { + return NativeResult::Err(array_type_error(vm, "Cannot delete property")); + } + _ => unsafe { (*arr_ptr).mark_hole_at(index) }, + } + + if let Err(err) = vm.ordinary_set(unsafe { &mut *arr_ptr }, length_si, js_array_index(index), recv, true) { + return NativeResult::Err(from_engine_error(vm, &err)); + } NativeResult::Ok(last) } @@ -410,40 +454,118 @@ pub fn array_flat(vm: &mut H, args: &[u8]) -> NativeResult { } /// `Array.prototype.shift()`:移除并返回首元素,其余元素前移;空数组返回 undefined。 +/// +/// # 步骤 +/// 1. 读 `LengthOfArrayLike(this)`;为 0 时以 `Set(this, "length", +0, true)` 收尾。 +/// 2. Get 首元素后,把 `1..len` 逐位前移:源存在则 Get + 严格 Set 到前一格, +/// 源缺失(hole)则 DeletePropertyOrThrow 目标格。 +/// 3. 以 `Set(this, "length", len-1, true)` 收尾。 +/// +/// # 副作用 +/// - 元素整体前移并收缩 length;Get/Set 可经原型链触发用户代码。 pub fn array_shift(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("Array.prototype.shift called with {} args", args.len()); let arr_ptr = array_ptr!(vm, args); - let arr = unsafe { &mut *arr_ptr }; - let len = arr.prop_count(); + let recv = JsValue::from_js_object(arr_ptr); + let len = unsafe { &*arr_ptr }.logical_len() as usize; + let length_si = vm.string_key_si("length"); if len == 0 { + if let Err(err) = vm.ordinary_set(unsafe { &mut *arr_ptr }, length_si, JsValue::int(0), recv, true) { + return NativeResult::Err(from_engine_error(vm, &err)); + } return NativeResult::Ok(JsValue::undefined()); } - let first = arr.get_prop_at(0); - for i in 1..len { - let v = arr.get_prop_at(i); - arr.set_prop_at(i - 1, v); + let first_key = vm.string_key_si("0"); + let first = match vm.ordinary_get(unsafe { &*arr_ptr }, first_key, recv) { + Ok(v) => v, + Err(msg) => return NativeResult::Err(from_engine_error(vm, &msg)), + }; + + for k in 1..len { + let from = vm.string_key_si(&k.to_string()); + let to = vm.string_key_si(&(k - 1).to_string()); + // HasProperty 走原型链存在性判定(hole 视同缺失),命中才 Get + Set。 + if vm.resolve_property(unsafe { &*arr_ptr }, from).is_some() { + let val = match vm.ordinary_get(unsafe { &*arr_ptr }, from, recv) { + Ok(v) => v, + Err(msg) => return NativeResult::Err(from_engine_error(vm, &msg)), + }; + if let Err(err) = vm.ordinary_set(unsafe { &mut *arr_ptr }, to, val, recv, true) { + return NativeResult::Err(from_engine_error(vm, &err)); + } + } else { + // DeletePropertyOrThrow(O, to):不可配置元素删除失败抛 TypeError。 + match unsafe { &*arr_ptr }.prop_meta_at(k - 1) { + Some(meta) if !meta.attributes.configurable() => { + return NativeResult::Err(array_type_error(vm, "Cannot delete property")); + } + _ => unsafe { (*arr_ptr).mark_hole_at(k - 1) }, + } + } + } + + if let Err(err) = vm.ordinary_set(unsafe { &mut *arr_ptr }, length_si, js_array_index(len - 1), recv, true) { + return NativeResult::Err(from_engine_error(vm, &err)); } - arr.set_prop_count_fast(len - 1); NativeResult::Ok(first) } /// `Array.prototype.unshift(...items)`:插入元素到头部,返回新长度。 +/// +/// # 步骤 +/// 1. 读 `LengthOfArrayLike(this)`;`len + 参数数` 超出 2^53-1 时抛 TypeError。 +/// 2. 参数数大于 0 时自高到低搬移已有元素(源存在则 Get + 严格 Set,缺失则 +/// DeletePropertyOrThrow),再把实参逐个严格 Set 到头部。 +/// 3. 以 `Set(this, "length", len+参数数, true)` 收尾。 +/// +/// # 副作用 +/// - 元素整体后移并扩容 length;Get/Set 可经原型链触发用户代码。 pub fn array_unshift(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("Array.prototype.unshift called with {} args", args.len()); let arr_ptr = array_ptr!(vm, args); - let arr = unsafe { &mut *arr_ptr }; - let len = arr.prop_count(); + let recv = JsValue::from_js_object(arr_ptr); + let len = unsafe { &*arr_ptr }.logical_len() as usize; let n_items = args.len().saturating_sub(1); - for i in (0..len as usize).rev() { - let v = arr.get_prop_at(i); - arr.set_prop_at(i + n_items, v); + let length_si = vm.string_key_si("length"); + if n_items > 0 { + if len as f64 + n_items as f64 > 9_007_199_254_740_991.0 { + return NativeResult::Err(array_type_error(vm, "Invalid array length")); + } + for k in (1..=len).rev() { + let from = vm.string_key_si(&(k - 1).to_string()); + let to_idx = k - 1 + n_items; + let to = vm.string_key_si(&to_idx.to_string()); + if vm.resolve_property(unsafe { &*arr_ptr }, from).is_some() { + let val = match vm.ordinary_get(unsafe { &*arr_ptr }, from, recv) { + Ok(v) => v, + Err(msg) => return NativeResult::Err(from_engine_error(vm, &msg)), + }; + if let Err(err) = vm.ordinary_set(unsafe { &mut *arr_ptr }, to, val, recv, true) { + return NativeResult::Err(from_engine_error(vm, &err)); + } + } else { + // DeletePropertyOrThrow(O, to):不可配置元素删除失败抛 TypeError。 + match unsafe { &*arr_ptr }.prop_meta_at(to_idx) { + Some(meta) if !meta.attributes.configurable() => { + return NativeResult::Err(array_type_error(vm, "Cannot delete property")); + } + _ => unsafe { (*arr_ptr).mark_hole_at(to_idx) }, + } + } + } + for (j, &arg_reg) in args.iter().skip(1).enumerate() { + let key = vm.string_key_si(&j.to_string()); + if let Err(err) = vm.ordinary_set(unsafe { &mut *arr_ptr }, key, vm.reg(arg_reg), recv, true) { + return NativeResult::Err(from_engine_error(vm, &err)); + } + } } - for (j, &arg_reg) in args.iter().skip(1).enumerate() { - arr.set_prop_at(j, vm.reg(arg_reg)); + + let new_len = len + n_items; + if let Err(err) = vm.ordinary_set(unsafe { &mut *arr_ptr }, length_si, js_array_index(new_len), recv, true) { + return NativeResult::Err(from_engine_error(vm, &err)); } - let new_len = len as usize + n_items; - arr.set_prop_count_fast(new_len); - NativeResult::Ok(JsValue::int(new_len as i32)) + NativeResult::Ok(js_array_index(new_len)) } /// `Array.prototype.fill(value, start, end)`:用给定值填充区间,返回 this。 diff --git a/crates/oxide_vm/src/vm/vmhost.rs b/crates/oxide_vm/src/vm/vmhost.rs index 7d89ded4..419c590b 100644 --- a/crates/oxide_vm/src/vm/vmhost.rs +++ b/crates/oxide_vm/src/vm/vmhost.rs @@ -96,7 +96,8 @@ impl oxide_runtime_api::VmHost for Vm { fn ordinary_set( &mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue, strict: bool, ) -> Result<(), String> { - self.ordinary_set(obj, prop_name_si, val, receiver, strict) + // builtin 调用边界:写失败返回格式化 Err,不就地展开(见 ordinary_set_builtin)。 + self.ordinary_set_builtin(obj, prop_name_si, val, receiver, strict) } fn define_data_property( &mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, attributes: PropAttributes, diff --git a/crates/oxide_vm/src/vm_props.rs b/crates/oxide_vm/src/vm_props.rs index 883cec49..0c8e2410 100644 --- a/crates/oxide_vm/src/vm_props.rs +++ b/crates/oxide_vm/src/vm_props.rs @@ -308,19 +308,33 @@ impl Vm { &mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue, strict: bool, ) -> Result<(), String> { let val = self.promote_if_needed_for_write_ptr(obj as *mut JsObject, val); - self.ordinary_set_inner(obj, prop_name_si, val, receiver, false, strict) + self.ordinary_set_inner(obj, prop_name_si, val, receiver, false, strict, false) + } + + /// builtin 内部写入口:失败一律返回格式化 `Err` 由调用边界恢复为异常对象, + /// 不就地 `unwind`——builtin 执行在重入的字节码上下文中,就地展开会跳入 + /// 调用方 catch 并让 builtin 继续,随后覆盖异常寄存器。 + pub(crate) fn ordinary_set_builtin( + &mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue, strict: bool, + ) -> Result<(), String> { + let val = self.promote_if_needed_for_write_ptr(obj as *mut JsObject, val); + self.ordinary_set_inner(obj, prop_name_si, val, receiver, false, strict, true) } /// 分发期入口:调用方(dispatch_set_prop 等)已对值做过 promote。 pub(crate) fn ordinary_set_dispatch( &mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue, strict: bool, ) -> Result<(), String> { - self.ordinary_set_inner(obj, prop_name_si, val, receiver, true, strict) + self.ordinary_set_inner(obj, prop_name_si, val, receiver, true, strict, false) } + #[expect( + clippy::too_many_arguments, + reason = "写路径须透传 receiver、帧化与 builtin 三组调用契约标志" + )] fn ordinary_set_inner( &mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue, use_frame_push: bool, - strict: bool, + strict: bool, builtin: bool, ) -> Result<(), String> { vm_trace!( "ordinary_set_inner: shape={} prop_si={} frame_push={}", @@ -332,7 +346,7 @@ impl Vm { // sloppy 静默 no-op;Reflect.set 以 strict=true 调用并投影为 false。 if obj.is_module_namespace() { if strict { - return self.raise_type_error("Cannot assign to a module namespace export"); + return self.write_protection_failure(builtin, "Cannot assign to a module namespace export"); } return Ok(()); } @@ -349,86 +363,18 @@ impl Vm { index as usize, use_frame_push, strict, + builtin, ); } return oxide_builtins::typed_array::typed_array_element_set(self, obj, index, val); } } - // 数组 length 赋值:ArraySetLength 语义(ToUint32 + 调整元素区)。 - // length 不得落影子命名属性:否则 `arr.length = N` 后 prop_count / - // 迭代 / 内置方法看到的长度与元素区不一致。 + // 数组 length 赋值:走 ArraySetLength 语义(两次数值强转、可写性判定与 + // 元素区调整),不得落影子命名属性——否则 prop_count / 迭代 / 内置方法 + // 看到的长度与元素区不一致。 let length_si = self.length_si; if obj.is_array() && prop_name_si == length_si { - // 冻结数组的 length 属性不可写(writable=false),赋值直接失败; - // 两模式统一抛 TypeError(length 冻结检查的 strict/sloppy 差异不在本路径范围)。 - if obj.is_frozen() { - return self.raise_type_error("Cannot assign to read only property 'length'"); - } - // defineProperty 显式收窄的不可写 length:严格模式抛 TypeError,sloppy 静默。 - if !obj.is_length_writable() { - if strict { - return self.raise_type_error("Cannot assign to read only property 'length'"); - } - return Ok(()); - } - let pc_before = self.pc; - let number_len = self.coerce_number_bounded(val)?; - // ToPrimitive 抛错(valueOf/toString throw)已被 unwind 定向到外围 catch 时 - // pc 指向 catch 入口:主 dispatch 约定异常后 opcode 不得继续 raise,直接 - // 返回由 dispatch 继续执行 catch,避免二次抛错覆盖原异常。 - if self.pc != pc_before { - return Ok(()); - } - let raw_new_len = if number_len == 0.0 || !number_len.is_finite() { - 0 - } else { - number_len.trunc().rem_euclid(4_294_967_296.0) as u32 as usize - }; - // ToUint32 截断结果与 ToNumber 不等价(1.5 / NaN / Infinity / 负数 / - // 2**32 等输入)时抛 RangeError:赋值后 length 读不回原值。 - if raw_new_len as f64 != number_len { - return self.raise_error_kind("RangeError", "Invalid array length"); - } - let old_logical = obj.logical_len() as usize; - let old_count = obj.array_prop_count as usize; - // ArraySetLength:增长(newLen > oldLen)要求对象可扩展,不可扩展时 - // 整个赋值失败且不修改(length 失败两模式均静默 no-op,不抛)。 - if raw_new_len > old_logical && !obj.is_extensible() { - return Ok(()); - } - // ArraySetLength:收缩时若 [newLen, oldLen) 内存在不可配置元素,整个收缩 - // 失败且不做任何修改(length 失败两模式均静默 no-op,不抛)。 - if raw_new_len < old_logical { - for idx in raw_new_len..old_count { - if let Some(meta) = obj.prop_meta_at(idx) { - if !meta.attributes.configurable() { - return Ok(()); - } - } - } - } - // 元素区物理槽数以 `MAX_DENSE_PROPS` 为上限,超限长度不扩槽。 - let new_len_u = raw_new_len.min(oxide_types::object::MAX_DENSE_PROPS); - obj.set_prop_count(new_len_u); - // 扩出的槽是稀疏 hole:存在性检查与原型链读取须视同不存在, - // 故逐个打 hole 标记。 - for idx in old_count..new_len_u { - obj.mark_hole_at(idx); - } - // 超过稠密上限的长度另记逻辑长度槽:`a.length` 读回真实赋值 - // 而非封顶后的物理槽数。 - if raw_new_len > oxide_types::object::MAX_DENSE_PROPS { - obj.set_array_len_override(raw_new_len as u32); - } else { - obj.clear_array_len_override(); - } - // 对象自身属性区可能存在影子 length 命名属性(shape 槽),同步写入 - // 新逻辑长度,保证 IC 快路径读到新值。 - if let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), length_si) { - let store_idx = obj.array_prop_count as usize + pos as usize; - obj.set_prop_storage(store_idx, obj.logical_len_value()); - } - return Ok(()); + return self.set_array_length_value(obj, val, strict, builtin); } if let Some(pos) = self.get_own_property_slot(obj, prop_name_si) { if let Some(meta) = obj.prop_meta_at(pos) { @@ -436,7 +382,7 @@ impl Vm { if meta.set.is_undefined() { // 无 setter:严格抛错,sloppy 静默 no-op。 if strict { - return self.raise_type_error("property has no setter"); + return self.write_protection_failure(builtin, "property has no setter"); } return Ok(()); } @@ -445,7 +391,7 @@ impl Vm { if !meta.attributes.writable() { // 只读数据属性:严格抛错,sloppy 静默 no-op。 if strict { - return self.raise_type_error("cannot assign to read-only property"); + return self.write_protection_failure(builtin, "cannot assign to read-only property"); } return Ok(()); } @@ -461,7 +407,7 @@ impl Vm { if meta.set.is_undefined() { // 继承无 setter:严格抛错,sloppy 静默 no-op。 if strict { - return self.raise_type_error("property has no setter"); + return self.write_protection_failure(builtin, "property has no setter"); } return Ok(()); } @@ -470,17 +416,30 @@ impl Vm { if !meta.attributes.writable() { // 继承只读数据属性:严格抛错,sloppy 静默 no-op(不遮蔽)。 if strict { - return self.raise_type_error("cannot assign to read-only property"); + return self.write_protection_failure(builtin, "cannot assign to read-only property"); } return Ok(()); } } + // 数组索引增长(index >= 当前 length)先过 length 可写性检查:与 define 侧 + // `define_array_index_element` 同序,早于对象可扩展性检查;length 不可写时 + // 严格抛 TypeError、sloppy 静默。索引小于 length 的已有元素写入不受此限。 + if let Some(index) = self.array_index_from_property_key(prop_name_si) { + if obj.is_array() && index >= obj.logical_len() && !obj.is_length_writable() { + return self.array_length_write_failure( + strict, + builtin, + "Cannot add property, array length is not writable", + ); + } + } + // 新属性(自身与原型链均无同名):须对象可扩展(OrdinarySet 的 extensible // 检查),不可扩展时赋值失败(严格抛错,sloppy 静默 no-op)。 if !obj.is_extensible() { if strict { - return self.raise_type_error("object is not extensible"); + return self.write_protection_failure(builtin, "object is not extensible"); } return Ok(()); } @@ -488,13 +447,92 @@ impl Vm { Ok(()) } + /// 数组 length 赋值路径:`a.length = v` 的 `[[Set]]`,按 ArraySetLength 语义 + /// 调整元素区(与 define 侧 [`Self::define_array_length`] 共用截断/扩洞逻辑)。 + /// + /// # 步骤 + /// 1. `ToUint32` 与 `ToNumber` 两次强转各自执行一次(均可触发用户代码)。 + /// 2. 两次结果不等(非整数、负数、NaN/Infinity、2^32 等)抛 RangeError。 + /// 3. 强转完成后按当前可写位判定:不可写时严格抛 TypeError、sloppy 静默,且 + /// 不做任何截断(写不可写数据描述符先于 ArraySetLength 失败)。 + /// 4. 收缩求最高不可配置阻挡索引:有则以「阻挡索引 + 1」部分截断后按模式返回 + /// 失败;无则完整截断/扩 hole 到目标长度。 + /// + /// # 边界与前提 + /// - 仅由 `ordinary_set_inner` 对 `is_array()` 对象且键为 length 时调用。 + /// - 可写位判定必须放在两次强转之后:用户代码可能在强转期收窄 length 可写位。 + /// + /// # 副作用 + /// - 修改元素区与元素元数据、可能置/清 `array_len_override`、bump 世代。 + fn set_array_length_value( + &mut self, obj: &mut JsObject, val: JsValue, strict: bool, builtin: bool, + ) -> Result<(), String> { + // ToNumber(BigInt) 抛 TypeError:在通用强转近似接受 BigInt 之前拦截。 + if val.is_bigint() { + return self.write_protection_failure(builtin, "Cannot convert a BigInt value to a number"); + } + + let pc_before = self.pc; + let new_len = self.coerce_uint32_bounded(val)?; + // 强转抛错在主 dispatch 下已 unwind 到外围 catch,此时 opcode 不得继续, + // 直接返回由 dispatch 执行 catch,避免二次抛错覆盖原异常。 + if self.pc != pc_before { + return Ok(()); + } + let number_len = self.coerce_number_bounded(val)?; + if self.pc != pc_before { + return Ok(()); + } + if new_len as f64 != number_len { + if builtin { + return Err(self.error_message_text("RangeError", "Invalid array length")); + } + return self.raise_error_kind("RangeError", "Invalid array length"); + } + + // 两次强转后重新判定:强转期间用户代码可能已把 length 收窄为不可写。 + if !obj.is_length_writable() { + return self.array_length_write_failure(strict, builtin, "Cannot assign to read only property 'length'"); + } + + let old_logical = obj.logical_len(); + let target_len = Self::array_length_shrink_target(obj, new_len, old_logical); + Self::apply_array_length(obj, target_len); + obj.bump_generation(); + + if target_len != new_len { + return self.array_length_write_failure(strict, builtin, "Cannot assign to read only property 'length'"); + } + Ok(()) + } + + /// 属性写保护失败的 strict/sloppy 分派:严格模式抛 TypeError(非 builtin 就地 + /// 展开到最近 catch),sloppy 静默 no-op。 + fn array_length_write_failure(&mut self, strict: bool, builtin: bool, msg: &str) -> Result<(), String> { + if !strict { + return Ok(()); + } + self.write_protection_failure(builtin, msg) + } + + /// TypeError 的两态出口:非 builtin 就地抛可捕获异常并展开,builtin 内部返回 + /// 格式化 `Err` 由调用边界恢复为异常对象(builtin 在重入字节码上下文中,就地 + /// 展开会跳入调用方 catch 并让 builtin 继续,随后覆盖异常寄存器)。 + fn write_protection_failure(&mut self, builtin: bool, msg: &str) -> Result<(), String> { + if builtin { + Err(self.error_message_text("TypeError", msg)) + } else { + self.raise_type_error(msg) + } + } + /// TypedArray 整数索引在 `receiver` ≠ TA 时的 [[Set]] 语义:越界或非对象 /// receiver 直接返回 true(不写、不 ToNumber);界内对象 receiver 走普通 set /// 把属性落到 receiver 自身。 #[allow(clippy::too_many_arguments)] fn set_to_receiver( &mut self, ta_obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue, index: usize, - use_frame_push: bool, strict: bool, + use_frame_push: bool, strict: bool, builtin: bool, ) -> Result<(), String> { let Some((_, length)) = oxide_builtins::typed_array::typed_array_integer_index(self, ta_obj, prop_name_si) else { @@ -509,7 +547,7 @@ impl Vm { } let promoted = self.promote_if_needed_for_write_ptr(receiver_ptr, val); let receiver_obj = unsafe { &mut *receiver_ptr }; - self.ordinary_set_inner(receiver_obj, prop_name_si, promoted, receiver, use_frame_push, strict) + self.ordinary_set_inner(receiver_obj, prop_name_si, promoted, receiver, use_frame_push, strict, builtin) } /// 调用或帧化访问器 setter:native(或禁止帧化的调用方)同步执行, @@ -1104,17 +1142,8 @@ impl Vm { return Err("cannot redefine non-configurable property".to_string()); } - // 收缩按 ArraySetLength 删除循环语义:自最高索引向下删除可配置元素,遇最高 - // 不可配置索引 P 时停止并把 length 收敛到 P+1(其上元素已在循环中删除)。 - // 仅当 [newLen, oldLen) 全可配置时才完整截断到 newLen。 - let target_len = if new_len < old_logical { - match Self::highest_non_configurable_index(obj, new_len, old_logical) { - Some(blocker) => blocker + 1, - None => new_len, - } - } else { - new_len - }; + // 收缩按 ArraySetLength 删除循环语义求目标长度(部分截断 / 完整截断)。 + let target_len = Self::array_length_shrink_target(obj, new_len, old_logical); Self::apply_array_length(obj, target_len); obj.set_length_non_writable(!attributes.writable()); @@ -1126,6 +1155,22 @@ impl Vm { Ok(()) } + /// 数组 length 收缩的目标长度:`[new_len, old_len)` 内存在不可配置元素时取 + /// 最高阻挡索引 + 1(部分截断,其上可配置元素已删除),否则完整截断到 `new_len`。 + /// + /// # 边界与前提 + /// - `new_len >= old_logical` 时直接返回 `new_len`(非收缩路径)。 + fn array_length_shrink_target(obj: &JsObject, new_len: u32, old_logical: u32) -> u32 { + if new_len < old_logical { + match Self::highest_non_configurable_index(obj, new_len, old_logical) { + Some(blocker) => blocker + 1, + None => new_len, + } + } else { + new_len + } + } + /// 返回 `[from, old_len)` 内最高的不可配置自身元素索引;无则 `None`。 /// /// # 边界与前提 diff --git a/crates/oxide_vm/tests/array_length_set_tests.rs b/crates/oxide_vm/tests/array_length_set_tests.rs new file mode 100644 index 00000000..35406825 --- /dev/null +++ b/crates/oxide_vm/tests/array_length_set_tests.rs @@ -0,0 +1,189 @@ +//! 数组 length 虚拟属性的赋值侧回归:`a.length = v` 的两次数值强转、strict/sloppy +//! 可写位语义、部分截断、增长、异常原值传播,以及 push/pop/shift/unshift 与 +//! `a[i] = v` 字节码路径对不可写 length 的行为。 + +use std::sync::Arc; + +use oxide_compiler::compiler::Compiler; +use oxide_parser::Allocator; +use oxide_vm::vm::Vm; + +/// 求值并取字符串结果内容(字符串值走 lookup_str,非字符串走 Display)。 +fn eval_str(source: &str) -> String { + let allocator = Allocator::default(); + let program = oxide_parser::parse(&allocator, source).expect("parse ok"); + let module = Compiler::new().compile(&program).expect("compile ok"); + let mut vm = Vm::new(); + let val = vm.run(&Arc::new(module)).expect("run ok"); + if val.is_string() { + vm.lookup_str(val).unwrap_or_default() + } else { + format!("{val}") + } +} + +#[test] +fn set_length_truncates_and_grows() { + assert_eq!(eval_str("var a=[1,2,3]; a.length=1; ''+a.length+':'+(2 in a)"), "1:false"); + assert_eq!(eval_str("var a=[1]; a.length=3; ''+a.length+':'+(1 in a)"), "3:false"); +} + +#[test] +fn set_length_non_writable_strict_throws_sloppy_silent() { + // sloppy:静默 no-op,length 不变。 + assert_eq!( + eval_str("var a=[1,2,3]; Object.defineProperty(a,'length',{writable:false}); a.length=1; ''+a.length"), + "3" + ); + // strict:TypeError,length 不变。 + assert_eq!( + eval_str("var a=[1,2,3]; Object.defineProperty(a,'length',{writable:false}); (function(){'use strict';try{a.length=1;return 'no-throw';}catch(e){return e.name;}})()+':'+a.length"), + "TypeError:3" + ); + // 同值赋值同样失败:不可写数据描述符先于 ArraySetLength 返回 false。 + assert_eq!( + eval_str("var a=[1,2,3]; Object.defineProperty(a,'length',{writable:false}); (function(){'use strict';try{a.length=3;return 'no-throw';}catch(e){return e.name;}})()+':'+a.length"), + "TypeError:3" + ); +} + +#[test] +fn set_length_frozen_strict_throws_sloppy_silent() { + // 冻结数组的 length 同样不可写:sloppy 静默而非恒抛。 + assert_eq!(eval_str("var a=Object.freeze([1,2,3]); a.length=1; ''+a.length"), "3"); + assert_eq!( + eval_str("var a=Object.freeze([1,2,3]); (function(){'use strict';try{a.length=1;return 'no-throw';}catch(e){return e.name;}})()+':'+a.length"), + "TypeError:3" + ); +} + +#[test] +fn set_length_shrink_partial_truncation_at_blocker() { + // strict:阻挡索引 1,length 收敛到 2,index 2 起删除,并抛 TypeError。 + assert_eq!( + eval_str("var a=[0,1,2,3,4]; Object.defineProperty(a,'1',{configurable:false}); var r; (function(){'use strict';try{a.length=1;r='no-throw';}catch(e){r=e.name;}})(); r+':'+a.length+':'+a.hasOwnProperty('1')+':'+a.hasOwnProperty('2')"), + "TypeError:2:true:false" + ); + // sloppy:同样部分截断,静默失败。 + assert_eq!( + eval_str("var a=[0,1,2,3,4]; Object.defineProperty(a,'1',{configurable:false}); a.length=0; ''+a.length+':'+a.hasOwnProperty('1')+':'+a.hasOwnProperty('2')"), + "2:true:false" + ); +} + +#[test] +fn set_length_growth_ignores_extensibility() { + // ArraySetLength 的增长不要求对象可扩展:preventExtensions 后仍成功。 + assert_eq!( + eval_str("var a=[1]; Object.preventExtensions(a); a.length=3; ''+a.length+':'+(1 in a)+':'+(2 in a)"), + "3:false:false" + ); +} + +#[test] +fn set_length_invalid_values_throw_range_error() { + assert_eq!( + eval_str("var a=[1,2,3]; var r=[]; [1.5,-1,4294967296,NaN].forEach(function(v){try{a.length=v;r.push('no-throw');}catch(e){r.push(e.name);}}); r.join(',')"), + "RangeError,RangeError,RangeError,RangeError" + ); +} + +#[test] +fn set_length_coerces_twice_before_writable_check() { + // ToUint32 + ToNumber 各触发一次 ToPrimitive(number);第二次强转收窄可写位后 + // 由可写位判定拒绝,且不做任何截断。 + assert_eq!( + eval_str("var a=[1,2,3], n=0; var v={}; v[Symbol.toPrimitive]=function(h){n++;Object.defineProperty(a,'length',{writable:false});return 0;}; var r; (function(){'use strict';try{a.length=v;r='no-throw';}catch(e){r=e.name;}})(); r+':'+n+':'+a.length"), + "TypeError:2:3" + ); + // Reflect.set 以严格模式调用并投影为 false,两次强转同样执行。 + assert_eq!( + eval_str("var a=[1,2,3], n=0; var v={}; v[Symbol.toPrimitive]=function(h){n++;Object.defineProperty(a,'length',{writable:false});return 0;}; Reflect.set(a,'length',v)+':'+n+':'+a.length"), + "false:2:3" + ); +} + +#[test] +fn set_length_coercion_exception_propagates_original_value() { + // 强转期 valueOf / Symbol.toPrimitive 抛出的自定义错误须原值重抛(identity 保留)。 + assert_eq!( + eval_str("var err=new Error('boom'); var v={valueOf:function(){throw err;}}; var a=[]; var c=null; try{a.length=v;}catch(e){c=e;} (c===err)+':'+c.name"), + "true:Error" + ); + assert_eq!( + eval_str("var err=new RangeError('sym'); var v={}; v[Symbol.toPrimitive]=function(h){throw err;}; var a=[]; var c=null; try{a.length=v;}catch(e){c=e;} (c===err)+':'+c.name"), + "true:RangeError" + ); +} + +#[test] +fn set_length_bigint_and_symbol_throw_type_error() { + assert_eq!( + eval_str("var a=[]; var r=[]; try{a.length=1n;r.push('no-throw');}catch(e){r.push(e.name);} try{a.length=Symbol();r.push('no-throw');}catch(e){r.push(e.name);} r.join(',')"), + "TypeError,TypeError" + ); +} + +#[test] +fn set_elem_beyond_non_writable_length_respects_strict() { + // `a[i] = v` 字节码路径:越界写须过 length 可写性检查,sloppy 静默、strict 抛。 + assert_eq!( + eval_str( + "var a=[]; Object.defineProperty(a,'length',{writable:false}); var i=3; a[i]=9; ''+a.length+':'+(3 in a)" + ), + "0:false" + ); + assert_eq!( + eval_str("var a=[]; Object.defineProperty(a,'length',{writable:false}); var i=3; var r; (function(){'use strict';try{a[i]=9;r='no-throw';}catch(e){r=e.name;}})(); r+':'+a.length+':'+(3 in a)"), + "TypeError:0:false" + ); +} + +#[test] +fn mutators_on_non_writable_length_throw() { + // 空数组 + 不可写 length:四个变更方法收尾的 Set length 均抛 TypeError。 + assert_eq!( + eval_str("var a=[]; Object.defineProperty(a,'length',{writable:false}); var r=[]; try{a.push();r.push('no-throw');}catch(e){r.push(e.name);} r.push(a.length); r.join(',')"), + "TypeError,0" + ); + assert_eq!( + eval_str( + "var a=[]; Object.defineProperty(a,'length',{writable:false}); try{a.pop();'no-throw';}catch(e){e.name}" + ), + "TypeError" + ); + assert_eq!( + eval_str( + "var a=[]; Object.defineProperty(a,'length',{writable:false}); try{a.shift();'no-throw';}catch(e){e.name}" + ), + "TypeError" + ); + assert_eq!( + eval_str("var a=[]; Object.defineProperty(a,'length',{writable:false}); try{a.unshift();'no-throw';}catch(e){e.name}"), + "TypeError" + ); + // frozen 空数组同样四方法皆抛。 + assert_eq!( + eval_str("var a=Object.freeze([]); var r=[]; try{a.push();r.push('no-throw');}catch(e){r.push(e.name);} try{a.pop();r.push('no-throw');}catch(e){r.push(e.name);} try{a.shift();r.push('no-throw');}catch(e){r.push(e.name);} try{a.unshift();r.push('no-throw');}catch(e){r.push(e.name);} r.join(',')"), + "TypeError,TypeError,TypeError,TypeError" + ); +} + +#[test] +fn push_element_write_uses_prototype_setter() { + // 继承的索引 setter 在元素写入时执行并收窄 length,收尾 Set length 失败。 + assert_eq!( + eval_str("var a=[]; var calls=0; Object.defineProperty(Array.prototype,'0',{set:function(v){Object.defineProperty(a,'length',{writable:false});calls++;}}); var r; try{a.push(1);r='no-throw';}catch(e){r=e.name;} r+':'+a.length+':'+a.hasOwnProperty(0)+':'+calls"), + "TypeError:0:false:1" + ); +} + +#[test] +fn mutators_still_work_with_writable_length() { + // 重写后正常路径回归:push/pop/shift/unshift 的元素搬移与返回值。 + assert_eq!(eval_str("var a=[]; a.push(1,2); ''+a.length+':'+a[1]"), "2:2"); + assert_eq!( + eval_str("var a=[1,2,3]; var p=a.pop(); var s=a.shift(); var u=a.unshift(9,8); p+':'+s+':'+u+':'+a.join(',')"), + "3:1:3:9,8,2" + ); +} diff --git a/crates/oxide_vm/tests/property_write_protection_tests.rs b/crates/oxide_vm/tests/property_write_protection_tests.rs index b6aba72d..1b6e9170 100644 --- a/crates/oxide_vm/tests/property_write_protection_tests.rs +++ b/crates/oxide_vm/tests/property_write_protection_tests.rs @@ -235,7 +235,7 @@ fn frozen_array_write_and_length_throws_in_strict() { ); } -// ── sloppy Object.freeze 数组:元素写静默失败(length 冻结检查仍抛错) ── +// ── sloppy Object.freeze 数组:元素写与 length 写均静默失败(严格模式才抛) ── #[test] fn frozen_array_element_write_silently_fails_in_sloppy() { let (_vm, r) = eval_keep_vm(