feat: protect existing objects during read-only calls and track reads
This commit is contained in:
@@ -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、结果根转移与失败清理 |
|
||||
|
||||
## 常见修改入口
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)。
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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/比较参数与跳转读取接口 |
|
||||
|
||||
@@ -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,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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)。
|
||||
|
||||
@@ -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
@@ -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 |
|
||||
|
||||
@@ -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 并存的可选协议。实现先只读验证
|
||||
|
||||
Reference in New Issue
Block a user