Hotdry.

Article

Deno FFI 非阻塞调用中的 Rust unsafe 边界安全设计

解析 Deno 2.8 中 FFI 非阻塞调用的内存安全修复,探讨 Rust unsafe 边界处的生命周期管理与高性能外部调用优化策略。

2026-05-23systems

Deno 2.8 在 FFI(Foreign Function Interface)层引入了一项关键修复:非阻塞调用现在会保持参数 backing store 的引用直至原生调用完成。这看似简单的改动,实则触及了 JavaScript 运行时与 Rust 原生代码交互中最棘手的问题之一 —— 跨语言边界处的内存安全与生命周期管理。

非阻塞 FFI 的隐患:悬垂指针与 UAF

在 Deno 的 FFI 实现中,非阻塞调用通过 spawn_blocking 将原生函数执行转移到后台线程,避免阻塞 JavaScript 的事件循环。这种设计对于执行耗时操作(如图像处理、加密计算或文件 I/O)至关重要。然而,这里隐藏着一个微妙的内存安全问题。

当 JavaScript 代码通过 FFI 传递 ArrayBuffer 给原生代码时,Deno 需要提取其底层数据指针。问题在于:非阻塞调用会立即返回 JavaScript,而原生代码仍在后台线程执行。如果此时 V8 的垃圾回收器运行,且 JavaScript 端没有保留对 ArrayBuffer 的引用,backing store 可能被回收,导致原生代码持有的指针变为悬垂指针(dangling pointer)。这就是典型的 Use-After-Free(UAF)漏洞。

Deno 2.8 的修复方案是在调用前收集 SharedRef<BackingStore> 引用,与原始指针一起移入 spawn_blocking 闭包。这样,即使 JavaScript 端不再引用该 buffer,Rust 端仍持有 backing store 的共享引用,确保内存直到原生调用完成才被释放。

Rust unsafe 边界的设计原则

FFI 本质上就是 unsafe 的。当 Rust 代码通过 FFI 与 C 库或操作系统 API 交互时,编译器的借用检查器无法跨越语言边界。Deno 的解决方案体现了几个关键的 unsafe 边界设计原则:

显式生命周期管理:通过 BackingStoreHolder 模式,将内存生命周期与闭包执行绑定。SharedRef 在闭包内保持存活,闭包结束时自动释放,形成清晰的所有权边界。

最小化 unsafe 代码块:修复将引用收集逻辑封装在安全的抽象层中,只有真正需要提取原始指针的地方才使用 unsafe。这种 "unsafe 封装" 模式是 Rust FFI 的最佳实践。

防御性编程:修复包含了针对 buffer、struct 和 out_buffer 参数的完整处理,并在测试中使用已知模式填充 buffer,强制 GC 后验证校验和,确保没有数据损坏。

高性能外部调用的优化策略

除了安全修复,Deno 2.8 的 FFI 优化还体现了高性能外部调用的几个关键策略:

零拷贝数据传输ArrayBuffer 的 backing store 直接暴露给原生代码,避免额外的内存分配和数据复制。这对于处理大型二进制数据(如图像、音频或机器学习张量)至关重要。

异步边界分离:通过 spawn_blocking 将 CPU 密集型或阻塞操作移出 JavaScript 主线程,保持事件循环的响应性。这种分离需要仔细处理跨线程的数据生命周期。

批量参数处理:修复同时处理 buffer、struct 和 out_buffer 三种参数类型,说明 FFI 层需要统一处理多种数据传递模式,确保一致的安全保证。

可落地的工程实践

对于使用 Deno FFI 的开发者,以下实践可以帮助避免类似问题:

  1. 显式管理 buffer 生命周期:在非阻塞 FFI 调用期间,确保 JavaScript 端保持对 ArrayBuffer 的引用,或升级到 Deno 2.8+ 利用自动 backing store 保持功能。

  2. 避免跨边界传递栈指针:只传递堆分配的 buffer 或 JavaScript 创建的 ArrayBuffer,永远不要传递 Rust 栈变量的地址到异步 FFI 调用。

  3. 使用 out_buffer 模式:对于需要写入结果的调用,使用 Deno FFI 的 out_buffer 参数类型,让 JavaScript 端分配并持有输出 buffer。

  4. 测试并发场景:在测试中模拟 GC 压力,验证异步 FFI 调用的内存安全性。Deno 2.8 的测试用例展示了这种模式:填充已知模式、强制 GC、验证校验和。

结语

Deno 2.8 的 FFI 修复展示了在 JavaScript 与 Rust 的边界处处理内存安全的复杂性。通过 SharedRef<BackingStore> 和闭包生命周期管理,Deno 在不牺牲性能的前提下消除了 UAF 风险。对于需要与原生代码交互的 Deno 应用,理解这些边界安全机制是构建可靠系统的关键。

资料来源

systems

内容声明:本文无广告投放、无付费植入。

如有事实性问题,欢迎发送勘误至 i@hotdrydog.com