From ff49f266a23801898321d780dbf0879e9f479b11 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Wed, 21 Oct 2026 14:17:38 +0800 Subject: [PATCH] feat: protect existing objects during read-only calls and track reads --- crates/core/src/README.md | 2 +- crates/core/src/eval.rs | 15 ++++++ crates/core/tests/README.md | 2 +- crates/core/tests/native_context.rs | 74 +++++++++++++++++++++++++++ crates/objects/src/README.md | 2 +- crates/objects/src/heap.rs | 77 ++++++++++++++++++++++++++++- crates/objects/src/store.rs | 13 +++++ crates/objects/tests/README.md | 2 +- crates/objects/tests/store.rs | 58 ++++++++++++++++++++++ docs/README.md | 2 +- docs/runtime/native-calls.md | 8 +++ 11 files changed, 249 insertions(+), 6 deletions(-) diff --git a/crates/core/src/README.md b/crates/core/src/README.md index 44a6209..90ea377 100644 --- a/crates/core/src/README.md +++ b/crates/core/src/README.md @@ -7,7 +7,7 @@ | [lib.rs](lib.rs) | VM 类型导出、objects 类型兼容重导出、宿主调用与字节码 rustdoc 示例 | | [vm.rs](vm.rs) | Vm 拥有 ObjectStore 与错误位置记录;对象操作和宿主 new_class 注册显式委托给 objects | | [frame.rs](frame.rs) | EndorFrame、栈条目、执行位置元数据、ExecutionLimits | -| [eval.rs](eval.rs) | execute/call、参数绑定、帧切换、取指分派、MakeFunction、结果根转移与失败清理 | +| [eval.rs](eval.rs) | execute/call、只读调用与依赖结果、参数绑定、帧切换、取指分派、MakeFunction、结果根转移与失败清理 | ## 常见修改入口 diff --git a/crates/core/src/eval.rs b/crates/core/src/eval.rs index 36a97d7..1c37112 100644 --- a/crates/core/src/eval.rs +++ b/crates/core/src/eval.rs @@ -48,6 +48,21 @@ impl Vm { self.call_with_limits(function, arguments, ExecutionLimits::default()) } + /// Allow result allocation while rejecting mutations of objects that were + /// already alive on entry. All reads share the call's normal frame budget. + /// Returned read handles are borrowed dependency identities, not owned roots. + pub fn call_read_only_with_limits( + &mut self, + function: ObjectRef, + arguments: &[Value], + limits: ExecutionLimits, + ) -> Result<(Value, Vec), VmError> { + let scope = self.objects.begin_read_only(); + let result = self.call_with_limits(function, arguments, limits); + let reads = self.objects.end_read_only(scope)?; + result.map(|value| (value, reads)) + } + pub fn call_with_limits( &mut self, function: ObjectRef, diff --git a/crates/core/tests/README.md b/crates/core/tests/README.md index f2ba7cc..4495768 100644 --- a/crates/core/tests/README.md +++ b/crates/core/tests/README.md @@ -8,7 +8,7 @@ | [builtins.rs](builtins.rs) | 无损 Number、Unicode 字符串、Dict 顺序与键、Cell、函数环境/共享代码、异常数据 | | [execution.rs](execution.rs) | wordcode 与校验、控制流、参数/默认值、嵌套帧、字节码函数创建/默认值/环境捕获、闭包、返回值存活、失败清理、限额 | | [types.rs](types.rs) | 名义身份、继承与实例关系、类型边保活/循环回收、只读类型元数据与外来句柄;仅远端执行 | -| [native_context.rs](native_context.rs) | 原生作用域根、分配失败清理、委托、弱引用过期与有界变更;仅远端执行 | +| [native_context.rs](native_context.rs) | 原生作用域根、分配失败清理、委托、只读结果分配/写入拒绝/依赖与失败恢复、弱引用过期及有界变更;仅远端执行 | 核心对象现由 objects 定义,本目录保留既有 endor_core 导入与 Vm 方法的兼容性检查。独立对象存储检查见 [objects/tests](../../objects/tests/README.md)。堆内部伪造句柄与计数溢出检查在 objects 的 src/heap.rs,公开示例由 src/lib.rs rustdoc 提供。文件职责见 [src](../src/README.md),检查方式和结果边界见 [验证](../../../docs/operations/validation.md)。本地不执行测试;存在测试文件不代表某次提交已通过。返回 [core](../README.md)。 diff --git a/crates/core/tests/native_context.rs b/crates/core/tests/native_context.rs index 79526ba..9b66526 100644 --- a/crates/core/tests/native_context.rs +++ b/crates/core/tests/native_context.rs @@ -148,3 +148,77 @@ fn notification_overflow_requires_rescan_instead_of_losing_changes_silently() { assert_eq!(vm.take_changes().changes.len(), 1); vm.release(object).unwrap(); } + +#[derive(Debug)] +struct ReadResult { + source: ObjectRef, + mutate: bool, +} +impl NativeCallable for ReadResult { + fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result { + let value = cx.store().get_attr(self.source, "value")?; + if self.mutate { + cx.set_attr(self.source, "value", Value::Number(99_i64.into()))?; + } + let result = cx.new_object()?; + cx.set_attr(result, "copied", value)?; + Ok(NativeResult::Return(Value::Object(result))) + } +} + +#[test] +fn read_only_calls_track_borrowed_dependencies_allow_results_and_restore_after_error() { + let mut vm = Vm::new().unwrap(); + let source = vm.new_object().unwrap(); + vm.set_attr(source, "value", Value::Number(7_i64.into())) + .unwrap(); + let function = vm + .allocate(ContextNativeFunction::new( + "read result", + ReadResult { + source, + mutate: false, + }, + vec![source], + )) + .unwrap(); + vm.take_changes(); + let (value, reads) = vm + .call_read_only_with_limits(function, &[], ExecutionLimits::default()) + .unwrap(); + assert!(reads.contains(&source)); + assert!(vm.take_changes().changes.is_empty()); + let Value::Object(result) = value else { + panic!("result") + }; + assert_eq!( + vm.get_attr(result, "copied").unwrap(), + Value::Number(7_i64.into()) + ); + vm.release(result).unwrap(); + vm.replace_payload( + function, + ContextNativeFunction::new( + "illegal write", + ReadResult { + source, + mutate: true, + }, + vec![source], + ), + ) + .unwrap(); + assert!( + vm.call_read_only_with_limits(function, &[], ExecutionLimits::default()) + .is_err() + ); + assert_eq!( + vm.get_attr(source, "value").unwrap(), + Value::Number(7_i64.into()) + ); + vm.set_attr(source, "value", Value::Number(8_i64.into())) + .unwrap(); + vm.release(function).unwrap(); + vm.release(source).unwrap(); + assert_eq!(vm.object_count(), 0); +} diff --git a/crates/objects/src/README.md b/crates/objects/src/README.md index ae5dfae..9b31d8d 100644 --- a/crates/objects/src/README.md +++ b/crates/objects/src/README.md @@ -11,7 +11,7 @@ | [changes.rs](changes.rs) | 合并变更批次、变更键与有界通知额度 | | [number.rs](number.rs) | Integer/Decimal/Rational/BinaryFloat/Complex 的无损表示和装箱载荷 | | [builtins.rs](builtins.rs) | String、Dict、Cell、Function、BaseException 载荷与 trace 边 | -| [heap.rs](heap.rs) | 私有 Heap、不可伪造 ObjectRef、稳定 Box、引用事务、迭代释放、循环回收及私有边界测试 | +| [heap.rs](heap.rs) | 私有 Heap、不可伪造 ObjectRef、稳定 Box、引用事务、迭代释放、循环回收、只读变更屏障/读取跟踪及私有边界测试 | | [store.rs](store.rs) | ObjectStore、空堆创建、对象操作、retain/release 和回收入口 | | [store_builtins.rs](store_builtins.rs) | 载荷校验、代码/函数工厂、Dict/Cell 操作及键语义 | | [instruction.rs](instruction.rs) | Opcode、Instruction、wordcode 编解码、MakeFunction/比较参数与跳转读取接口 | diff --git a/crates/objects/src/heap.rs b/crates/objects/src/heap.rs index 5415499..117ecad 100644 --- a/crates/objects/src/heap.rs +++ b/crates/objects/src/heap.rs @@ -1,5 +1,6 @@ -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use std::fmt; +use std::sync::Mutex; use std::sync::atomic::{AtomicU64, Ordering}; use crate::changes::MAX_PENDING_CHANGES; @@ -34,6 +35,12 @@ struct Slot { object: Option>, } +#[derive(Debug)] +struct ReadOnly { + protected: BTreeSet, + reads: Mutex>, +} + #[derive(Debug)] pub(crate) struct Heap { id: u64, @@ -42,6 +49,7 @@ pub(crate) struct Heap { live: usize, changes: BTreeMap<(ObjectRef, ChangeKey), u64>, changes_overflow: bool, + read_only: Vec, } impl Heap { @@ -56,9 +64,61 @@ impl Heap { live: 0, changes: BTreeMap::new(), changes_overflow: false, + read_only: vec![], }) } + pub(crate) fn begin_read_only(&mut self) -> usize { + let protected = self + .slots + .iter() + .enumerate() + .filter_map(|(index, slot)| { + slot.object.as_ref().map(|_| ObjectRef { + heap_id: self.id, + index, + generation: slot.generation, + }) + }) + .collect(); + self.read_only.push(ReadOnly { + protected, + reads: Mutex::new(BTreeSet::new()), + }); + self.read_only.len() + } + + pub(crate) fn end_read_only(&mut self, scope: usize) -> Result, VmError> { + if scope == 0 || scope != self.read_only.len() { + return Err(VmError::InvalidArgument("read-only scope nesting mismatch")); + } + Ok(self + .read_only + .pop() + .unwrap() + .reads + .into_inner() + .expect("read tracker poisoned") + .into_iter() + .collect()) + } + + pub(crate) fn is_read_only(&self) -> bool { + !self.read_only.is_empty() + } + + pub(crate) fn record_read(&self, reference: ObjectRef) { + for scope in &self.read_only { + if scope.protected.contains(&reference) { + scope + .reads + .lock() + .expect("read tracker poisoned") + .insert(reference); + } + } + } + pub(crate) fn allocate( &mut self, payload: Box, @@ -268,6 +328,16 @@ impl Heap { } pub(crate) fn prepare_change(&self, reference: ObjectRef) -> Result<(), VmError> { + self.get(reference)?; + if self + .read_only + .iter() + .any(|scope| scope.protected.contains(&reference)) + { + return Err(VmError::UnsupportedOperation( + "mutation of existing object during read-only call", + )); + } self.get(reference)? .header .change_version @@ -280,6 +350,11 @@ impl Heap { let object = self.get_mut(reference).expect("validated change target"); object.header.change_version += 1; let version = object.header.change_version; + // Fresh result construction has no existing observers. Keep versions, + // but do not notify application dependencies about derived allocations. + if self.is_read_only() { + return; + } if self.changes_overflow { return; } diff --git a/crates/objects/src/store.rs b/crates/objects/src/store.rs index 14aa597..5bd5db8 100644 --- a/crates/objects/src/store.rs +++ b/crates/objects/src/store.rs @@ -13,6 +13,18 @@ pub struct ObjectStore { } impl ObjectStore { + /// Existing objects are immutable; fresh result graphs may be constructed. + /// Scopes must end in LIFO order. Read handles are borrowed, never GC roots. + pub fn begin_read_only(&mut self) -> usize { + self.heap.begin_read_only() + } + pub fn end_read_only(&mut self, scope: usize) -> Result, VmError> { + self.heap.end_read_only(scope) + } + pub fn is_read_only(&self) -> bool { + self.heap.is_read_only() + } + pub fn downgrade(&self, reference: ObjectRef) -> Result { self.object(reference)?; Ok(WeakObjectRef(reference)) @@ -107,6 +119,7 @@ impl ObjectStore { } pub fn object(&self, reference: ObjectRef) -> Result<&EndorObject, VmError> { + self.heap.record_read(reference); self.heap.get(reference) } diff --git a/crates/objects/tests/README.md b/crates/objects/tests/README.md index 1d055e8..8b8e905 100644 --- a/crates/objects/tests/README.md +++ b/crates/objects/tests/README.md @@ -5,6 +5,6 @@ | 文件 | 覆盖 | | --- | --- | | [attribute_storage.rs](attribute_storage.rs) | 可转移字段存储、原类型身份、替换释放、属性写删和非法路径拒绝 | -| [store.rs](store.rs) | 稳定地址、跨存储句柄拒绝与更新原子性、Dict/Cell 循环回收、函数校验失败无泄漏、共享代码与常量生命周期 | +| [store.rs](store.rs) | 只读嵌套、容器与类型写入屏障、稳定地址、跨存储句柄拒绝与更新原子性、Dict/Cell 循环回收、函数校验失败无泄漏、共享代码与常量生命周期 | 非法句柄索引、槽位代数与计数溢出仍在 src/heap.rs 的私有单元测试内,不开放句柄构造器。VM 委托 API 与求值行为继续由 [core/tests](../../core/tests/README.md) 检查。随工作区在指定远端运行,本地不测试;现有测试源码不代表已通过,见 [验证](../../../docs/operations/validation.md)。返回 [objects](../README.md)。 diff --git a/crates/objects/tests/store.rs b/crates/objects/tests/store.rs index 82ede20..307f844 100644 --- a/crates/objects/tests/store.rs +++ b/crates/objects/tests/store.rs @@ -94,3 +94,61 @@ fn standalone_function_factories_validate_and_own_shared_executable_data() { store.release(second).unwrap(); assert_eq!(store.object_count(), 0); } + +#[test] +fn read_only_scopes_block_existing_container_and_type_writes_but_allow_fresh_results() { + let mut store = objects::ObjectStore::new().unwrap(); + let roots = store.bootstrap_types().unwrap(); + let record = store.new_object().unwrap(); + let dict = store.new_dict().unwrap(); + let cell = store + .new_cell(Some(objects::Value::Number(1_i64.into()))) + .unwrap(); + let scope = store.begin_read_only(); + assert!( + store + .set_attr(record, "value", objects::Value::Nil) + .is_err() + ); + assert!(store.attach_type(record, roots.object).is_err()); + assert!( + store + .dict_set( + dict, + objects::Value::Text("key".into()), + objects::Value::Nil + ) + .is_err() + ); + assert!( + store + .cell_set(cell, objects::Value::Number(2_i64.into())) + .is_err() + ); + assert_eq!( + store.cell_get(cell).unwrap(), + objects::Value::Number(1_i64.into()) + ); + let fresh = store.new_object().unwrap(); + store.set_attr(fresh, "value", objects::Value::Nil).unwrap(); + let nested = store.begin_read_only(); + assert!( + store + .set_attr(fresh, "value", objects::Value::Bool(true)) + .is_err() + ); + store.end_read_only(nested).unwrap(); + store + .set_attr(fresh, "value", objects::Value::Bool(true)) + .unwrap(); + let reads = store.end_read_only(scope).unwrap(); + assert!(reads.contains(&cell)); + store + .set_attr(record, "value", objects::Value::Nil) + .unwrap(); + for object in [record, dict, cell, fresh, roots.object, roots.type_type] { + store.release(object).unwrap(); + } + store.collect_cycles(); + assert_eq!(store.object_count(), 0); +} diff --git a/docs/README.md b/docs/README.md index 37343c3..c18ce87 100644 --- a/docs/README.md +++ b/docs/README.md @@ -5,7 +5,7 @@ | 引擎与宿主边界 | [架构](architecture/README.md) | [crates](../crates/README.md) | | 解释执行、资源修订与引擎升级 | [执行与版本](architecture/execution-and-versions.md) | frontend/compiler/core | | EndorObject、句柄、默认操作 | [对象模型](runtime/objects.md) | objects/src | -| 原生可变上下文、作用域函数工厂、委托、弱引用与变更通知 | [原生调用](runtime/native-calls.md) | objects/src/native.rs、changes.rs、core/src/eval.rs | +| 原生上下文、只读调用、读取依赖、委托与变更通知 | [原生调用](runtime/native-calls.md) | objects/src/native.rs、changes.rs、core/src/eval.rs | | 托管类型、宿主类注册、原生分配继承与字段存储 | [名义类型](runtime/types.md) | objects/src/types.rs | | Number、String、Dict、Cell、Function | [基本对象](runtime/builtins.md) | objects/src | | 源码读取与来源 | [Reader](runtime/reader.md) | frontend/src/reader.rs | diff --git a/docs/runtime/native-calls.md b/docs/runtime/native-calls.md index 3c17df6..44359be 100644 --- a/docs/runtime/native-calls.md +++ b/docs/runtime/native-calls.md @@ -25,6 +25,14 @@ function 和每个对象参数在移交前取得根;VM 完成调用准备后 分派计入同一指令预算,避免无界原生委托链。预算仍不能抢占一次长时间原生操作。 委托函数的结果就是该次调用结果,当前不提供任意原生调用后续 continuation。 +## 只读调用与读取依赖 + +Vm::call_read_only_with_limits 沿用显式帧与预算,返回拥有式结果及借用式读取对象身份列表。进入时现存对象的属性、载荷、容器、Cell、类型边与共享赋值写入均在 prepare_change 屏障前拒绝;调用内新分配的结果图可以正常构造。作用域正常返回或报错后恢复;这不提供 panic 恢复或外部 I/O 沙箱。 + +ObjectStore 的 begin_read_only/end_read_only 提供同样的 LIFO 嵌套机制,is_read_only 供宿主协议检查。保护集合与读取集合不持有 GC 根,代际句柄避免槽位复用误判。读取是对象粒度;宿主须把借用身份转为版本依赖并验证存活,不能据此保活整个对象图。 + +只读结果的构造仍维护自身变更版本,但不推送应用 ChangeBatch 通知;现有对象不会在此模式成功写入,因此没有遗漏已有观察者的修改。通过普通调用保存结果到旧对象时,保存动作仍照常通知。可信 Rust 的内部可变状态、外部资源与 I/O 需要宿主遵守只读契约;本机制只强制堆的公开变更入口。 + ## 托管共享引用赋值 assignment_cell(source, store) 是与原生 assign_from 并存的可选协议。实现先只读验证