脚本隔离:独立 VM 不等于最小权限沙箱
自动关联目录:脚本隔离:独立 VM 不等于最小权限沙箱
核心判断:编辑、客户端和 DS 使用独立
lua_State,能隔离全局变量、模块缓存和对象注册表;但只要每个 VM 都打开相同标准库并注册整套引擎 API,它就只是状态隔离,还不是最小权限沙箱。
一、业务冲突:同一种语言要承担三种信任级别
创作者希望一套脚本同时覆盖编辑工具、客户端表现和 DS 玩法。共享语言和对象模型能降低学习成本,却不能共享权限:编辑 VM 可以写工程数据,客户端 VM 只能提交意图,DS VM 可以修改权威玩法状态,但不该拥有编辑文件与纯表现接口。
工程实践已经把三类运行时拆成独立 VM,由各自的 World/Subsystem 管理;VM 句柄包含角色和单调递增序号,旧句柄不会命中新 VM。这是正确的第一步,它解决了 _G、package.loaded、对象表和回调注册跨角色串台。
二、实际边界:物理隔离强,能力隔离仍偏晚
初始化路径仍统一打开标准库,随后由总注册器挂载场景、命令、工程读写、网络、HTTP、物理、相机、输入、编辑 UI、插件和发布等接口。部分 API 在内部再判断角色或目标是否可用。
这种“先暴露、调用时再拒绝”的问题有三点:
- API 枚举会向低权限脚本泄露不属于它的能力面。
- 每个绑定都要重复做角色、World、目标和参数校验,漏一个就是越权入口。
- 不可用能力常表现为
false或空结果,脚本难以区分权限拒绝、目标缺失和实现故障。
控制台执行路径反而做得更完整:它创建白名单环境,编辑态只在作者服务可用时叠加编辑能力;白名单核心缺失时直接失败,不回退到裸执行。这说明正确方向已经存在,只是还没有成为所有创作者脚本的统一准入模型。
三、目标设计:由 Capability Manifest 决定 API 是否存在
能力应在 VM 建立时一次冻结,而不是散落在函数内部:
struct RuntimeGrant {
Role role;
WorldId world;
Set<Capability> capabilities;
ResourceBudget budget;
Generation generation;
};
VM CreateVM(RuntimeGrant grant) {
VM vm = OpenRestrictedLibraries(grant.capabilities);
for (Binding b : BindingCatalog::All()) {
if (grant.capabilities.ContainsAll(b.required)) {
vm.Register(b.name, BindWithGrant(b, grant));
}
}
return vm;
}File.AuthorProject、Net.SendIntent、World.MutateAuthority、UI.PresentLocal 应是不同能力。脚本拿不到的 API 就不存在;即使拿到闭包,调用时仍校验 VM generation、World identity 和资源预算,形成注册期与调用期两道门。
四、取舍与验证
能力分表会增加绑定元数据、测试矩阵和兼容成本,也会暴露过去依赖“误注册但碰巧不可用”的脚本。迁移时可先记录每个角色实际调用的 API,再从观察模式切到拒绝模式。
测试不能只验证三个 VM 的 _G 不共享,还要枚举各角色 API 表、尝试保存旧闭包跨 World 调用,并证明编辑、客户端、DS 的拒绝原因稳定可诊断。最终判断是:VM 是故障域,Capability 才是安全边界。