fix: shift 返回钉结果寄存器并收 length 写序偏离

This commit is contained in:
2026-09-19 10:33:59 +00:00
parent 0697cde219
commit a953652da8
6 changed files with 113 additions and 23 deletions
+9 -3
View File
@@ -480,6 +480,10 @@ pub fn array_shift<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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());
+8 -2
View File
@@ -1267,7 +1267,10 @@ pub fn object_from_entries<H: VmHost>(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<H: VmHost>(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 同款)。
+8
View File
@@ -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(())
}
+27 -18
View File
@@ -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");
@@ -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 的元素搬移与返回值。
+21
View File
@@ -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 窗口内执行期收集