From 528f893e638de7af200205112c162c7a40ecf145 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Sat, 15 Aug 2026 21:30:28 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E9=94=99=E8=AF=AF=E5=AF=B9=E8=B1=A1?= =?UTF-8?q?=E5=B1=9E=E6=80=A7=E6=9E=9A=E4=B8=BE=E6=80=A7=E3=80=81toString?= =?UTF-8?q?=20=E6=A0=87=E7=AD=BE=E4=B8=8E=E6=9E=84=E9=80=A0=E5=99=A8?= =?UTF-8?q?=E8=B0=83=E7=94=A8=E8=AF=AD=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - message/name 属性改非枚举(CreateNonEnumerableDataPropertyOrThrow), Object.keys/for-in/JSON.stringify 不再泄漏错误信息 - 新增 Error 家族对象标签,Object.prototype.toString 输出 [object Error], 含子类型与用户子类(沿原型链判定) - Error 家族构造器补 length=1,函数调用忽略 this 一律新建对象 - JSON.stringify 序列化仅含可枚举自身属性(规范 EnumerableOwnPropertyNames) --- crates/oxide_builtins/src/error.rs | 30 +++++--- crates/oxide_builtins/src/json.rs | 9 ++- crates/oxide_builtins/src/object.rs | 21 ++++++ crates/oxide_kernel/src/builtin.rs | 11 ++- crates/oxide_types/src/object.rs | 9 +++ crates/oxide_vm/src/bindings/bind_error.rs | 21 +++++- crates/oxide_vm/tests/builtins_error_tests.rs | 73 +++++++++++++++++++ 7 files changed, 156 insertions(+), 18 deletions(-) diff --git a/crates/oxide_builtins/src/error.rs b/crates/oxide_builtins/src/error.rs index 2c3fbe17..2de7b2f8 100644 --- a/crates/oxide_builtins/src/error.rs +++ b/crates/oxide_builtins/src/error.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; use oxide_runtime_api::{to_string, NativeResult, VmHost}; use oxide_types::mem::P; -use oxide_types::object::JsObject; +use oxide_types::object::{JsObject, PropAttributes}; use oxide_types::value::JsValue; fn set_own_message(host: &mut H, this: *mut JsObject, args: &[u8]) { @@ -22,7 +22,9 @@ fn set_own_message(host: &mut H, this: *mut JsObject, args: &[u8]) { let perm_val = host.new_string(&msg_str); unsafe { (*this).set_shape_id(new_shape); - (*this).push_prop(perm_val); + let pos = (*this).push_prop(perm_val); + // message 按规范为非枚举数据属性(CreateNonEnumerableDataPropertyOrThrow)。 + (*this).set_data_meta(pos, PropAttributes::new(true, false, true)); } } @@ -49,9 +51,15 @@ pub fn create_kind_error(host: &mut H, kind: &str, msg: &str) -> JsVa let msg_val = host.new_string(msg); unsafe { (*obj).set_shape_id(shape); - (*obj).push_prop(msg_val); + let pos = (*obj).push_prop(msg_val); + // message 按规范为非枚举数据属性,避免泄漏进 Object.keys/for-in/JSON。 + (*obj).set_data_meta(pos, PropAttributes::new(true, false, true)); } } + // 标记 Error 家族标签:Object.prototype.toString 据此输出 `[object Error]`。 + unsafe { + (*obj).type_tag = JsObject::OBJ_TYPE_ERROR; + } JsValue::from_js_object(obj) } @@ -115,15 +123,15 @@ macro_rules! error_ctor { ($name:ident, $proto_field:ident) => { /// 对应 Error 子类(如 `TypeError`)的构造函数:接收第一个实参作为 message, /// 返回原型链指向对应 prototype 的 Error 对象。 + /// + /// # 注意事项 + /// 无论以 `new` 还是普通函数调用(如 `TypeError.call(obj)`)都新建对象, + /// 忽略调用方传入的 this——与规范构造器语义一致。 pub fn $name(host: &mut H, args: &[u8]) -> NativeResult { - let this_val = host.reg(255); - let this = if this_val.is_object() { - this_val.as_js_object_ptr() - } else { - let proto_ptr = P::as_ptr(&host.session().builtin_world().$proto_field) as *mut JsObject; - host.epoch() - .alloc(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(proto_ptr))) - }; + let proto_ptr = P::as_ptr(&host.session().builtin_world().$proto_field) as *mut JsObject; + let this = host + .epoch() + .alloc(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(proto_ptr))); set_own_message(host, this, args); NativeResult::Ok(JsValue::from_js_object(this)) } diff --git a/crates/oxide_builtins/src/json.rs b/crates/oxide_builtins/src/json.rs index 7f147be2..8b731f28 100644 --- a/crates/oxide_builtins/src/json.rs +++ b/crates/oxide_builtins/src/json.rs @@ -2,7 +2,7 @@ use std::collections::HashSet; use std::fmt::Write; use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; -use oxide_types::object::JsObject; +use oxide_types::object::{JsObject, PropAttributes}; use oxide_types::private_key::make_int_key; use oxide_types::value::JsValue; @@ -361,9 +361,14 @@ fn stringify_object( out.push('{'); let keys = walk_own_keys(vm, obj); - // 预计算 (名称, 槽位) 对,并应用白名单过滤。 + // 仅序列化可枚举自身属性(规范 EnumerableOwnPropertyNames)。 let entries: Vec<(String, u32)> = keys .into_iter() + .filter(|(_si, pos)| { + obj.prop_meta_at(*pos) + .map(|m| m.attributes.enumerable()) + .unwrap_or(PropAttributes::DEFAULT_DATA.enumerable()) + }) .filter_map(|(si, pos)| { let name = key_si_to_string(vm, si); if let Some(whitelist) = replacer_whitelist { diff --git a/crates/oxide_builtins/src/object.rs b/crates/oxide_builtins/src/object.rs index eead06e1..544d9b3a 100644 --- a/crates/oxide_builtins/src/object.rs +++ b/crates/oxide_builtins/src/object.rs @@ -1096,6 +1096,25 @@ pub fn object_proto_value_of(vm: &mut H, args: &[u8]) -> NativeResult NativeResult::Ok(vm.reg(args[0])) } +/// 沿原型链判断是否为 Error 家族对象:对象自身带 `OBJ_TYPE_ERROR` 标签,或原型链上 +/// 含 Error.prototype(等价 [[ErrorData]] 内部槽语义,覆盖 Error 用户子类)。 +fn is_error_family(vm: &H, ptr: *mut JsObject) -> bool { + let error_proto_ptr = vm.session().builtin_world().error_proto.as_ptr() as *mut JsObject; + let mut cur = ptr; + while !cur.is_null() { + if std::ptr::eq(cur, error_proto_ptr) { + return true; + } + // SAFETY: cur 沿原型链遍历,链上每个节点都是合法 JsObject(proto 非对象时为空指针终止)。 + let o = unsafe { &*cur }; + if o.is_error_obj() { + return true; + } + cur = o.proto().as_js_object_ptr(); + } + false +} + /// `Object.prototype.toString`:返回 `[object Tag]`。对象路径先按内置类型判定标签, /// 再读 `@@toStringTag`——为字符串时覆盖内置标签(TypedArray 依赖它区分具体类型)。 pub fn object_proto_to_string(vm: &mut H, args: &[u8]) -> NativeResult { @@ -1138,6 +1157,8 @@ pub fn object_proto_to_string(vm: &mut H, args: &[u8]) -> NativeResul "ArrayBuffer" } else if obj.is_arguments_obj() { "Arguments" + } else if is_error_family(vm, ptr) { + "Error" } else if std::ptr::eq(ptr, vm.session().builtin_world().math_object.as_ptr()) { "Math" } else if std::ptr::eq(ptr, vm.session().builtin_world().json_object.as_ptr()) { diff --git a/crates/oxide_kernel/src/builtin.rs b/crates/oxide_kernel/src/builtin.rs index 80397b25..fcf81fc1 100644 --- a/crates/oxide_kernel/src/builtin.rs +++ b/crates/oxide_kernel/src/builtin.rs @@ -1378,14 +1378,17 @@ impl BuiltinWorld { let error_name_val = JsValue::perm_string(string_forge.string_ptr(error_si)); let name_shape = shape_forge.make_shape(proto.shape_id(), si_name); proto.set_shape_id(name_shape); - proto.ensure_hash_props().push(error_name_val); + let name_pos = proto.push_prop(error_name_val); + // 原型上的 name/message 按规范为非枚举数据属性,避免泄漏进 Object.keys/for-in。 + proto.set_data_meta(name_pos, oxide_types::object::PropAttributes::new(true, false, true)); let si_message = string_forge.intern("message").0; let empty_si = string_forge.intern("").0; let empty_val = JsValue::perm_string(string_forge.string_ptr(empty_si)); let msg_shape = shape_forge.make_shape(proto.shape_id(), si_message); proto.set_shape_id(msg_shape); - proto.ensure_hash_props().push(empty_val); + let msg_pos = proto.push_prop(empty_val); + proto.set_data_meta(msg_pos, oxide_types::object::PropAttributes::new(true, false, true)); self.set_subtype_proto_name(string_forge, shape_forge, &self.type_error_proto, "TypeError", si_name); self.set_subtype_proto_name(string_forge, shape_forge, &self.reference_error_proto, "ReferenceError", si_name); @@ -1404,7 +1407,9 @@ impl BuiltinWorld { let name_val = JsValue::perm_string(string_forge.string_ptr(name_si)); let name_shape = shape_forge.make_shape(proto.shape_id(), si_name); proto.set_shape_id(name_shape); - proto.ensure_hash_props().push(name_val); + let name_pos = proto.push_prop(name_val); + // 子类型原型上的 name 同 Error.prototype.name:非枚举数据属性。 + proto.set_data_meta(name_pos, oxide_types::object::PropAttributes::new(true, false, true)); } /// 把 String 家族方法安装到 String 构造器与原型上。 diff --git a/crates/oxide_types/src/object.rs b/crates/oxide_types/src/object.rs index a6a7bdb7..3522d1fb 100644 --- a/crates/oxide_types/src/object.rs +++ b/crates/oxide_types/src/object.rs @@ -650,6 +650,9 @@ impl JsObject { /// 装箱 Symbol 对象类型标签。 pub const OBJ_TYPE_SYMBOL_OBJ: u8 = 21; pub const OBJ_TYPE_ARGUMENTS: u8 = 20; + /// Error 家族对象(Error 及 NativeError 实例):`Object.prototype.toString` 返回 + /// `[object Error]`,与 [[ErrorData]] 内部槽对应。 + pub const OBJ_TYPE_ERROR: u8 = 22; /// `is_session_epoch` 字段中的 session 标记位。 pub const SESSION_EPOCH_BIT: u8 = 0x01; /// `is_session_epoch` 字段中的 GC 标记位。 @@ -757,6 +760,12 @@ impl JsObject { self.type_tag == Self::OBJ_TYPE_ARGUMENTS } + /// 是否 Error 家族对象(含 NativeError 实例)。 + #[inline] + pub fn is_error_obj(&self) -> bool { + self.type_tag == Self::OBJ_TYPE_ERROR + } + /// 构造无属性、可扩展的空对象(`new Object()` 的基础对象)。 pub fn new_empty(shape_id: ShapeId, proto: JsValue) -> Self { Self { diff --git a/crates/oxide_vm/src/bindings/bind_error.rs b/crates/oxide_vm/src/bindings/bind_error.rs index 2781a60a..0fae2712 100644 --- a/crates/oxide_vm/src/bindings/bind_error.rs +++ b/crates/oxide_vm/src/bindings/bind_error.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use oxide_kernel::builtin::ErrorMethods; use oxide_kernel::kernel::{KernelCore, KernelSession}; use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; -use oxide_types::object::{JsObject, NativeFnPtr}; +use oxide_types::object::{JsObject, NativeFnPtr, PropAttributes}; use oxide_types::value::JsValue; fn bind_error_subtype_constructor( @@ -24,13 +24,18 @@ fn bind_error_subtype_constructor( let si_prototype = sf.intern("prototype").0; let si_name = sf.intern("name").0; + let si_length = sf.intern("length").0; let name_si = sf.intern(name).0; let ctor_shape1 = sh.make_shape(EMPTY_SHAPE_ID, si_prototype); let ctor_shape2 = sh.make_shape(ctor_shape1, si_name); - ctor.set_shape_id(ctor_shape2); + let ctor_shape3 = sh.make_shape(ctor_shape2, si_length); + ctor.set_shape_id(ctor_shape3); ctor.ensure_hash_props().push(JsValue::from_js_object(proto_ptr)); ctor.ensure_hash_props().push(JsValue::perm_string(sf.string_ptr(name_si))); + ctor.ensure_hash_props().push(JsValue::int(1)); + // 构造器 length 按规范为不可写不可枚举(Function.length 属性描述符约定)。 + ctor.set_data_meta(2u32, PropAttributes::new(false, false, true)); let ctor_ptr = Box::into_raw(ctor); @@ -38,6 +43,9 @@ fn bind_error_subtype_constructor( let proto_ctor_shape = sh.make_shape(proto.shape_id(), sf.intern("constructor").0); proto.set_shape_id(proto_ctor_shape); proto.ensure_hash_props().push(JsValue::from_js_object(ctor_ptr)); + // 原型上的 constructor 按规范为非枚举数据属性(与 Error.prototype.constructor 一致)。 + let ctor_pos = proto.hash_props_vec().map_or(0, |v| v.len() as u32).saturating_sub(1); + proto.set_data_meta(ctor_pos, PropAttributes::new(true, false, true)); let global_shape = sh.make_shape(global.shape_id(), name_si); global.set_shape_id(global_shape); @@ -128,5 +136,14 @@ pub fn bind_error(core: &Arc, session: &KernelSession, global: &mut err_ctor.set_native_fn(Some(unsafe { NativeFnPtr::from_raw(oxide_builtins::error::error_constructor:: as *const ()) })); + // 主 Error 构造器补 length 槽(值 1,与子类型构造器一致)。 + let sf = core.perm_interner().as_ref(); + let sh = core.shape_forge().as_ref(); + let si_length = sf.intern("length").0; + let length_shape = sh.make_shape(err_ctor.shape_id(), si_length); + err_ctor.set_shape_id(length_shape); + err_ctor.ensure_hash_props().push(JsValue::int(1)); + let length_pos = err_ctor.hash_props_vec().map_or(0, |v| v.len() as u32).saturating_sub(1); + err_ctor.set_data_meta(length_pos, PropAttributes::new(false, false, true)); } } diff --git a/crates/oxide_vm/tests/builtins_error_tests.rs b/crates/oxide_vm/tests/builtins_error_tests.rs index a09de7b8..463a6438 100644 --- a/crates/oxide_vm/tests/builtins_error_tests.rs +++ b/crates/oxide_vm/tests/builtins_error_tests.rs @@ -327,6 +327,79 @@ fn error_to_string_non_object_throws() { } } +// ── 错误对象语义修复测试 ── + +#[test] +fn error_message_property_non_enumerable() { + let mut vm = make_vm(); + // message 描述符 enumerable=false(规范 CreateNonEnumerableDataPropertyOrThrow)。 + assert_eq!( + format!( + "{}", + eval_in(&mut vm, "Object.getOwnPropertyDescriptor(new Error('msg'), 'message').enumerable").unwrap() + ), + "false" + ); + // 可枚举自身键为空:Object.keys / JSON.stringify 不再泄漏 message。 + assert_eq!(format!("{}", eval_in(&mut vm, "Object.keys(new Error('msg')).length").unwrap()), "0"); + let r = eval_in(&mut vm, "JSON.stringify(new Error('secret'))").unwrap(); + assert_eq!(vm.lookup_str(r), Some("{}".to_string())); + // Error.prototype 的 name/message 同样非枚举。 + assert_eq!(format!("{}", eval_in(&mut vm, "Object.keys(Error.prototype).length").unwrap()), "0"); + // 子类型原型上的 name/constructor 非枚举,for-in 不泄漏。 + assert_eq!( + format!( + "{}", + eval_in(&mut vm, "var s=[]; for (var k in new TypeError('t')) s.push(k); s.length").unwrap() + ), + "0" + ); +} + +#[test] +fn error_to_string_tag_is_error() { + let mut vm = make_vm(); + // Error 家族(含子类型与用户子类)经 Object.prototype.toString 得 `[object Error]`。 + let r = eval_in(&mut vm, "Object.prototype.toString.call(new Error('x'))").unwrap(); + assert_eq!(vm.lookup_str(r), Some("[object Error]".to_string())); + let r = eval_in(&mut vm, "Object.prototype.toString.call(new TypeError('x'))").unwrap(); + assert_eq!(vm.lookup_str(r), Some("[object Error]".to_string())); + let r = eval_in( + &mut vm, + "class MyError extends Error {}; Object.prototype.toString.call(new MyError('x'))", + ) + .unwrap(); + assert_eq!(vm.lookup_str(r), Some("[object Error]".to_string())); + // Error.prototype 自身同样判定为 Error。 + let r = eval_in(&mut vm, "Object.prototype.toString.call(Error.prototype)").unwrap(); + assert_eq!(vm.lookup_str(r), Some("[object Error]".to_string())); +} + +#[test] +fn error_to_string_primitive_this_throws() { + let mut vm = make_vm(); + // 规范:Error.prototype.toString 对非对象 this 抛 TypeError(不兜底为 "Error")。 + let r = eval_in(&mut vm, "try { Error.prototype.toString.call(1); 'no-throw' } catch (e) { e.name }").unwrap(); + assert_eq!(vm.lookup_str(r), Some("TypeError".to_string())); +} + +#[test] +fn error_constructor_length_is_one() { + let mut vm = make_vm(); + assert_eq!(format!("{}", eval_in(&mut vm, "Error.length").unwrap()), "1"); + assert_eq!(format!("{}", eval_in(&mut vm, "TypeError.length").unwrap()), "1"); + assert_eq!(format!("{}", eval_in(&mut vm, "EvalError.length").unwrap()), "1"); +} + +#[test] +fn error_ctor_as_function_call_creates_new_object() { + let mut vm = make_vm(); + // Error.call(obj) 忽略传入 this,总是返回新 Error 对象。 + assert_eq!(format!("{}", eval_in(&mut vm, "var o = {x:1}; Error.call(o) === o").unwrap()), "false"); + let r = eval_in(&mut vm, "var o = {x:1}; TypeError.call(o).name").unwrap(); + assert_eq!(vm.lookup_str(r), Some("TypeError".to_string())); +} + // ── format_error_message 测试 ── #[test]