feat: protect existing objects during read-only calls and track reads

This commit is contained in:
2026-10-21 14:17:38 +08:00
parent 44a02c9fd6
commit ff49f266a2
11 changed files with 249 additions and 6 deletions
+1 -1
View File
@@ -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、结果根转移与失败清理 |
## 常见修改入口
+15
View File
@@ -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<ObjectRef>), 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,
+1 -1
View File
@@ -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)。
+74
View File
@@ -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<NativeResult, VmError> {
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);
}
+1 -1
View File
@@ -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/比较参数与跳转读取接口 |
+76 -1
View File
@@ -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<Box<EndorObject>>,
}
#[derive(Debug)]
struct ReadOnly {
protected: BTreeSet<ObjectRef>,
reads: Mutex<BTreeSet<ObjectRef>>,
}
#[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<ReadOnly>,
}
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<Vec<ObjectRef>, 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<dyn ObjectPayload>,
@@ -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;
}
+13
View File
@@ -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<Vec<ObjectRef>, 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<WeakObjectRef, VmError> {
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)
}
+1 -1
View File
@@ -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)。
+58
View File
@@ -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);
}
+1 -1
View File
@@ -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 |
+8
View File
@@ -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 并存的可选协议。实现先只读验证