内存安全与安全:一则说明
为什么 SuperJ 没有借用检查器,以及为什么对大多数软件而言这是正确的权衡。
简短版
Rust 的借用检查器让一类 bug 不可能发生 — 内存安全 bug(use-after-free、double-free、缓冲区溢出、数据竞争)— 并为此收取永久的逐行税:每一个生命周期标注、每一次借用图博弈、每一次当检查器无法证明显而易见之事时你伸手去拿的 Rc<RefCell<>>,在第一次写时以及后续每次编辑时、由每个工程师、在项目整个生命周期内付出。这个税是复利的 — 它拖慢上手、拖慢迭代、把小重构变成借用考古,并驱使人们放弃本会做出的改动。
SuperJ 不收这笔税。你写的是 Java。作为交换,内存 bug 是可能的 — 它们在运行时崩溃,而不是在编译期被拒绝。问题在于:让一类 bug 不可能发生,是否值得为你写的一切永久收税。
答案,对大多数软件而言:不值得
借用检查器消除的那类 bug — 以内存损坏作为通往代码执行的路径 — 是严重的一类,但它也是窄的一类。它是驱动了二十年浏览器与内核安全补丁的那一类,对于那种软件它值得这笔税:
- 浏览器 — 攻击者控制的输入在你的进程里运行;数十亿用户;一个内存 bug = 远程代码执行 = 你在向十亿人分发恶意软件。
- 内核 — 一个内存 bug = 所有运行它的人获得 root,而且你无法在不重启的情况下热补丁一个内核。
- 沙箱边界 / 不可信二进制格式的解析器 — 同理,规模更小。
这就是一个内存 bug 即灾难性且 bug 面无界的软件清单。这是一个真实的清单。它也只是被编写的软件的一小部分。
对于其余一切 — 服务器、服务、API 后端、数据流水线、网络守护进程,即生产软件的实际主体 — 威胁模型不是"攻击者通过内存损坏获得代码执行"。而是"逻辑返回了错误答案"或"服务在负载下崩溃"。内存安全对两者都无关。CVE 数据库偏向内存 bug 只是因为内存 bug 是那些会被分配 CVE 编号的;逻辑 bug 造成的事件从未被归档为漏洞。如果你按在生产中实际伤害用户的 bug 加权,逻辑 bug 占主导,而借用检查器对它们无能为力。
SuperJ 反而给你什么:确定性
这是交易的另一半,也是标准框定遗漏的那一半。SuperJ 的单线程 + SEDA 模型给你确定性:相同事件序列产生相同状态,逐位一致。这买到的是没有任何内存安全并发系统能提供的属性:
- 每个 bug 都可复现。重放事件日志,得到同样的崩溃,修复它,然后它保持修复 — 因为相同输入给出相同行为。系统向正确性收敛。
- 行为空间是一维的(以输入参数化),而非 n 维的(以输入 × 每种可能的交错参数化)。这让形式化验证、模型检查与穷尽模糊变得可驾驭 — 你不需要在一个你无法控制的调度器上做量化。
- 没有依赖时序的行为,因此没有时序攻击面。在一个并发系统中,一个能影响调度的攻击者 — 通过给核加负载、发送信号、在锁上竞争 — 能影响你产生哪个输出。这是一个真实的漏洞类别(TOCTOU、基于调度器的竞争、跨线程缓存时序侧信道)。Rust 的内存安全对此只字未提;它所启用的并发模型反而制造了它。
尖锐的版本:**内存安全是一个否定性属性("这类 bug 不可能发生"),通过引入非确定性获得,而非确定性是一个不安全性的正向来源("行为现在取决于攻击者能影响的东西")。**借用检查器移除一种失败模式,而并发模型打开另一种、更大的失败模式。确定性由构造关闭第二种,并让第一种可复现、可修复。
经济对比
| Rust | SuperJ | |
|---|---|---|
| 逐行代价 | 高、永久、每次编辑都付 | 低 — Java 人体工学 |
| 它保证什么 | 一类 bug 不可能(内存) | 一个属性存在(确定性) |
| 它不帮什么 | 逻辑 bug、死锁、其余约 90% | 内存 bug(直到你修复它们,而你能修) |
| 收敛模型 | "它编译了,所以没有内存 bug"(对其余一切沉默) | "它能复现,所以你能修任何 bug" |
这种不对称正是要点。Rust 花费巨大努力消除一个少数派失败模式,并引入非确定性让多数派失败模式更难修。SuperJ 在少数派模式上不花力气,却给你一个让多数派模式可驾驭的工作流。
何时选哪个
- Rust — 当单个内存 bug 即灾难性且 bug 面无界时:浏览器、内核、沙箱、不可信二进制格式的解析器。逐行税买的是你确实拥有的威胁模型的保险。
- SuperJ — 对于约 95% 不是那样的软件:服务器、服务、数据系统、网络守护进程。借用检查器的税买的是你大多并不拥有的威胁模型的保险,而确定性买的是修复你实际发布的 bug 的工作流。
业界把内存安全当作普遍值得的原因是,"内存安全"听起来像一个显而易见的好东西,而"人体工学代价"听起来像一句抱怨 — 但事实上前者是一个窄保证,后者是一种普遍税。在大多数情况下,你为一点付出很多。SuperJ 的论点是:Rust 让之不可能的那一类 bug,对大多数软件而言不值得让它不可能的代价 — 而确定性几乎不花代价,却给你一个能修复其余一切的工作流。
理想形态(以及为什么没人交付它)
单线程 Rust 同时具有两种属性:借用检查器的内存安全,且没有并发引入的非确定性。这严格优于任一纯选项。它也是没人谈论的配置,因为它击败了营销 — "无畏并发"在你不使用并发时就失去了意义。SuperJ 的赌注是:对大多数软件你不需要并发,而为了人体工学放弃内存安全,是比为了一个只对你并不拥有的威胁模型有意义的保证而放弃人体工学更好的权衡。