diff --git a/crates/oxide_builtins/src/array/element.rs b/crates/oxide_builtins/src/array/element.rs index 7fb95def..6434c3d5 100644 --- a/crates/oxide_builtins/src/array/element.rs +++ b/crates/oxide_builtins/src/array/element.rs @@ -480,6 +480,10 @@ pub fn array_shift(vm: &mut H, args: &[u8]) -> NativeResult { Ok(v) => v, Err(msg) => return NativeResult::Err(from_engine_error(vm, &msg)), }; + // 首位为 hole 时 Get 落原型链 getter,返回值可能是全新临时对象。钉入结果寄存器 + // (GC 根)再跨循环:循环内用户 setter 可触发回收,Rust 局部不属根集,回收后 + // 返回即悬垂;重入调用保存/恢复调用方窗口,槽值至返回前有效。 + vm.set_reg(0, first); for k in 1..len { let from = vm.string_key_si(&k.to_string()); @@ -507,13 +511,14 @@ pub fn array_shift(vm: &mut H, args: &[u8]) -> NativeResult { 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)); } - NativeResult::Ok(first) + // 从钉位读回(回收搬移后局部副本已失效)。 + NativeResult::Ok(vm.reg(0)) } /// `Array.prototype.unshift(...items)`:插入元素到头部,返回新长度。 /// /// # 步骤 -/// 1. 读 `LengthOfArrayLike(this)`;`len + 参数数` 超出 2^53-1 时抛 TypeError。 +/// 1. 读 `LengthOfArrayLike(this)`;`len + 参数数` 超出 2^53-1 时抛 RangeError。 /// 2. 参数数大于 0 时自高到低搬移已有元素(源存在则 Get + 严格 Set,缺失则 /// DeletePropertyOrThrow),再把实参逐个严格 Set 到头部。 /// 3. 以 `Set(this, "length", len+参数数, true)` 收尾。 @@ -529,7 +534,8 @@ pub fn array_unshift(vm: &mut H, args: &[u8]) -> NativeResult { 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")); + // 长度上限越界按规范为 RangeError。 + return NativeResult::Err(crate::error::create_range_error(vm, "Invalid array length")); } for k in (1..=len).rev() { let from = vm.string_key_si(&(k - 1).to_string()); diff --git a/crates/oxide_builtins/src/object.rs b/crates/oxide_builtins/src/object.rs index 6bd3e7e6..7aaa1a8b 100644 --- a/crates/oxide_builtins/src/object.rs +++ b/crates/oxide_builtins/src/object.rs @@ -1267,7 +1267,10 @@ pub fn object_from_entries(vm: &mut H, args: &[u8]) -> NativeResult { // ToPropertyKey 语义建键:int/规范数字串/symbol 统一映射,避免数字键分裂。 let si = vm.property_key_si(key_val); let promoted = vm.promote_if_needed_for_write_ptr(obj, value_val); - let _ = vm.ordinary_set(unsafe { &mut *obj }, si, promoted, target_val, true); + // 写入失败按 builtin 边界传播(目标为 fresh 对象实际不可达,不得静默吞)。 + if let Err(err) = vm.ordinary_set(unsafe { &mut *obj }, si, promoted, target_val, true) { + return NativeResult::Err(crate::array::from_engine_error(vm, &err)); + } } NativeResult::Ok(target_val) } @@ -1722,7 +1725,10 @@ pub fn object_group_by(vm: &mut H, args: &[u8]) -> NativeResult { let new_arr_val = JsValue::from_js_object(arr); let result_ref_mut = unsafe { &mut *result }; let promoted = vm.promote_if_needed_for_write_ptr(result, new_arr_val); - let _ = vm.ordinary_set(result_ref_mut, key_si, promoted, result_val, true); + // 写入失败按 builtin 边界传播(result 为 fresh 对象实际不可达,不得静默吞)。 + if let Err(err) = vm.ordinary_set(result_ref_mut, key_si, promoted, result_val, true) { + return NativeResult::Err(crate::array::from_engine_error(vm, &err)); + } new_arr_val }; // push element 到分组数组(写入前 promote,与 Array.prototype.push 同款)。 diff --git a/crates/oxide_vm/src/dispatch/property.rs b/crates/oxide_vm/src/dispatch/property.rs index d050cd70..6dc838a2 100644 --- a/crates/oxide_vm/src/dispatch/property.rs +++ b/crates/oxide_vm/src/dispatch/property.rs @@ -628,6 +628,14 @@ impl Vm { self.raise_type_error("object is not extensible")?; return Ok(()); } + // 越界索引写 length 不可写时失败(与 ordinary_set_inner 同序):SET_ELEM 当前 + // 仅对 fresh 数组发射不可达,防未来 codegen 对非 fresh 数组发射时绕过守卫。 + if idx >= obj.logical_len() && !obj.is_length_writable() { + if self.current_strict() { + self.raise_type_error("Cannot add property, array length is not writable")?; + } + return Ok(()); + } obj.set_prop_at(idx, value); Ok(()) } diff --git a/crates/oxide_vm/src/vm_props.rs b/crates/oxide_vm/src/vm_props.rs index 0c8e2410..27412706 100644 --- a/crates/oxide_vm/src/vm_props.rs +++ b/crates/oxide_vm/src/vm_props.rs @@ -422,9 +422,19 @@ impl Vm { } } - // 数组索引增长(index >= 当前 length)先过 length 可写性检查:与 define 侧 - // `define_array_index_element` 同序,早于对象可扩展性检查;length 不可写时 - // 严格抛 TypeError、sloppy 静默。索引小于 length 的已有元素写入不受此限。 + // 新属性(自身与原型链均无同名):须对象可扩展(OrdinarySet 的 extensible + // 检查先于 length 增长判定),不可扩展时赋值失败(严格抛错,sloppy 静默 no-op)。 + if !obj.is_extensible() { + if strict { + return self.write_protection_failure(builtin, "object is not extensible"); + } + 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( @@ -434,15 +444,6 @@ impl Vm { ); } } - - // 新属性(自身与原型链均无同名):须对象可扩展(OrdinarySet 的 extensible - // 检查),不可扩展时赋值失败(严格抛错,sloppy 静默 no-op)。 - if !obj.is_extensible() { - if strict { - return self.write_protection_failure(builtin, "object is not extensible"); - } - return Ok(()); - } self.set_or_create_prop_value(obj, prop_name_si, val); Ok(()) } @@ -451,22 +452,30 @@ impl Vm { /// 调整元素区(与 define 侧 [`Self::define_array_length`] 共用截断/扩洞逻辑)。 /// /// # 步骤 - /// 1. `ToUint32` 与 `ToNumber` 两次强转各自执行一次(均可触发用户代码)。 - /// 2. 两次结果不等(非整数、负数、NaN/Infinity、2^32 等)抛 RangeError。 - /// 3. 强转完成后按当前可写位判定:不可写时严格抛 TypeError、sloppy 静默,且 - /// 不做任何截断(写不可写数据描述符先于 ArraySetLength 失败)。 - /// 4. 收缩求最高不可配置阻挡索引:有则以「阻挡索引 + 1」部分截断后按模式返回 + /// 1. 入口可写位检查:已不可写时按模式直接返回(严格抛 TypeError、sloppy 静默), + /// 不执行强转——可写位判定先于 ArrayLength,用户 valueOf/toPrimitive 不触发。 + /// 2. `ToUint32` 与 `ToNumber` 两次强转各自执行一次(均可触发用户代码)。 + /// 3. 两次结果不等(非整数、负数、NaN/Infinity、2^32 等)抛 RangeError。 + /// 4. 强转完成后复查可写位:强转期用户代码可能已收窄 length;失败按模式返回 + /// 且不做任何截断(写不可写数据描述符先于 ArraySetLength 失败)。 + /// 5. 收缩求最高不可配置阻挡索引:有则以「阻挡索引 + 1」部分截断后按模式返回 /// 失败;无则完整截断/扩 hole 到目标长度。 /// /// # 边界与前提 /// - 仅由 `ordinary_set_inner` 对 `is_array()` 对象且键为 length 时调用。 - /// - 可写位判定必须放在两次强转之后:用户代码可能在强转期收窄 length 可写位。 + /// - 两次强转之后须复查可写位:用户代码可能在强转期收窄 length 可写位。 /// /// # 副作用 /// - 修改元素区与元素元数据、可能置/清 `array_len_override`、bump 世代。 fn set_array_length_value( &mut self, obj: &mut JsObject, val: JsValue, strict: bool, builtin: bool, ) -> Result<(), String> { + // 可写位判定先于 ArrayLength:入口即不可写时不执行强转——用户强转副作用 + // 不触发,强转期抛出的自定义错误也不得穿透 kind。 + if !obj.is_length_writable() { + return self.array_length_write_failure(strict, builtin, "Cannot assign to read only property 'length'"); + } + // ToNumber(BigInt) 抛 TypeError:在通用强转近似接受 BigInt 之前拦截。 if val.is_bigint() { return self.write_protection_failure(builtin, "Cannot convert a BigInt value to a number"); diff --git a/crates/oxide_vm/tests/array_length_set_tests.rs b/crates/oxide_vm/tests/array_length_set_tests.rs index 35406825..aaadac7d 100644 --- a/crates/oxide_vm/tests/array_length_set_tests.rs +++ b/crates/oxide_vm/tests/array_length_set_tests.rs @@ -88,6 +88,20 @@ fn set_length_invalid_values_throw_range_error() { ); } +#[test] +fn set_length_entry_non_writable_skips_coercion() { + // 入口即不可写:可写位判定先于 ArrayLength,两次强转不执行(valueOf 零调用), + // sloppy 静默、strict 抛 TypeError 而非强转期抛出的自定义 kind。 + assert_eq!( + eval_str("var a=[1,2,3]; Object.defineProperty(a,'length',{writable:false}); var n=0; var v={valueOf:function(){n++;throw new RangeError('x');}}; a.length=v; n+':'+a.length"), + "0:3" + ); + assert_eq!( + eval_str("var a=[1,2,3]; Object.defineProperty(a,'length',{writable:false}); var n=0; var v={valueOf:function(){n++;throw new RangeError('x');}}; var r; (function(){'use strict';try{a.length=v;r='no-throw';}catch(e){r=e.name;}})(); r+':'+n+':'+a.length"), + "TypeError:0:3" + ); +} + #[test] fn set_length_coerces_twice_before_writable_check() { // ToUint32 + ToNumber 各触发一次 ToPrimitive(number);第二次强转收窄可写位后 @@ -178,6 +192,32 @@ fn push_element_write_uses_prototype_setter() { ); } +#[test] +fn set_elem_non_extensible_precedes_length_guard() { + // 不可扩展 + length 不可写:extensible 检查先于 length 可写性判定, + // 错误消息取 not extensible 形态(node 同序)。 + assert_eq!( + eval_str("var a=[1]; Object.preventExtensions(a); Object.defineProperty(a,'length',{writable:false}); var m='none'; (function(){'use strict';try{a[5]=9;m='no-throw';}catch(e){m=String(e.message);}})(); /extensible/.test(m)+':'+a.length+':'+(5 in a)"), + "true:1:false" + ); + // 单条件各自独立生效:仅 length 不可写(可扩展)报 length 形态。 + assert_eq!( + eval_str("var a=[1]; Object.defineProperty(a,'length',{writable:false}); var m='none'; (function(){'use strict';try{a[5]=9;m='no-throw';}catch(e){m=String(e.message);}})(); /writable/.test(m)+':'+a.length"), + "true:1" + ); +} + +#[test] +fn shift_getter_temporary_survives_setter_window() { + // 首位为 hole:Get 落原型链 getter(返回新对象),循环内用户 setter 大量 + // 分配——getter 临时对象经结果寄存器钉为 GC 根,跨重入窗口存活,返回值 + // 身份保持。 + assert_eq!( + eval_str("var box=null; var a=[,2,3]; Object.defineProperty(Array.prototype,'0',{get:function(){box={m:1};return box;},set:function(v){Object.defineProperty(this,0,{value:v,writable:true,enumerable:true,configurable:true});var t='z';for(var i=0;i<16;i++){t=t+t;}}}); var r=a.shift(); (r===box)+':'+r.m+':'+a.length+':'+a[0]+':'+a[1]"), + "true:1:2:2:3" + ); +} + #[test] fn mutators_still_work_with_writable_length() { // 重写后正常路径回归:push/pop/shift/unshift 的元素搬移与返回值。 diff --git a/crates/oxide_vm/tests/gc_runtime_tests.rs b/crates/oxide_vm/tests/gc_runtime_tests.rs index 9698ba3c..8edf4f41 100644 --- a/crates/oxide_vm/tests/gc_runtime_tests.rs +++ b/crates/oxide_vm/tests/gc_runtime_tests.rs @@ -354,6 +354,27 @@ fn unreferenced_table_gens_do_not_grow_with_runs() { assert!(pinned > baseline, "每 run 函数对象钉住创建期代际,条目数应增长,实际 {pinned}"); } +/// shift 首位 hole 落原型链 getter:getter 临时对象经结果寄存器钉为 GC 根, +/// 跨循环内 setter 分配窗口与低阈值执行期收集存活,返回身份保持。 +#[test] +fn shift_getter_temporary_object_survives_runtime_gc() { + let mut vm = vm_with_threshold(512); + let module = compile( + "var g = {}; for (var i = 0; i < 20; i++) { g['k' + i] = 'v'.repeat(64); } \ + var box = null; var a = [, 2, 3]; \ + Object.defineProperty(Array.prototype, '0', { \ + get: function() { box = { m: 1 }; return box; }, \ + set: function(v) { \ + Object.defineProperty(this, 0, { value: v, writable: true, enumerable: true, configurable: true }); \ + var t = 'z'; for (var j = 0; j < 20; j++) { t = t + t; } } }); \ + var r = a.shift(); (r === box) + ':' + r.m + ':' + a.length + ':' + a[0] + ':' + a[1]", + ); + let result = vm.run(&Arc::new(module)).expect("run"); + let text = vm.lookup_str(result).expect("结果应为字符串"); + assert_eq!(text, "true:1:2:2:3"); + assert!(vm.session_gc_stats().total_collections > 0, "执行期应触发收集"); +} + // ── 原生盒内字符串/BigInt 边存活(mark 边收集去对象预过滤) ───────────────── /// 盒持唯一引用 session 串(Promise 结算值):churn 窗口内执行期收集