首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

Firefox是怎样解决内存安全的?

对于像Firefox这样复杂且高度优化的系统,内存安全是最大的安全挑战之一。Firefox主要是用C和C++编写的。众所周知,这些语言很难安全地使用,因为任何错误都有可能导致程序完全崩溃。

Firefox软件工程师Nathan Froyd写道,“我们努力寻找并消除内存风险,但也在改进Firefox代码库,以便在更深的层次上解决这些问题。”

截至目前,Firefox主要关注两项技术:

  1. 将代码分解成多个沙箱进程,减少特权
  2. 用一门安全的语言去重写代码,比如Rust

一种新方法

“虽然我们继续在Firefox中使用沙箱和Rust,但它们各有局限性。对已有的大型组件,进程级沙箱很有效,但这会消耗大量系统资源,因此必须谨慎使用。”Nathan Froyd写道。

虽然Rust是轻量级的,但是重写现有的数百万行C++代码是一件“劳神费力”的事。

Graphite字形库为例,Firefox用它来正确呈现某些复杂字体。它太小了,不适合“放入”自己的进程中。

然而,如果发现内存风险,即使是站点隔离的进程架构也无法阻止恶意字体破坏加载它的页面。同时,重写和维护这种领域专用的代码并不是Firefox有限工程资源的理想用法。

如今,Firefox在“军火库”中加入第三种方法。

加利福尼亚大学、圣地亚哥大学、德克萨斯大学、奥斯汀分校和斯坦福大学的研究人员开发出一种新的沙箱技术,叫RLBox

Nathan Froyd表示,“它让我们能快速有效地将现有Firefox组件转换为在一个WebAssembly沙箱中运行。我们已经成功地将该技术集成到我们的代码库中,并将其用于沙箱化Graphite。”

据悉,这种隔离将提供给Firefox 74的Linux用户和Firefox 75的Mac用户,不久之后还将提供Windows支持。

构建一个wasm沙箱

Wasm沙箱背后的核心实现思想是,你可以将C/C++编译成wasm代码,然后将该wasm代码编译成实际运行程序的机器的本机代码。

这些步骤与在浏览器中运行C/C++应用程序的步骤类似,但是,“我们在构建Firefox本身之前,就执行本地代码到wasm的转换。这两个步骤都各自依赖于重要的软件,我们还添加了第三个步骤,以使沙箱转换更简单、更不易出错。”Nathan Froyd写道。

首先,你要将C/C++编译成wasm代码。作为WebAssembly工作的一部分,在Clang和LLVM中添加一个wasm后端。光有一个编译器是不够的;你还需一个C/C++的标准库。该组件是通过wsi -sdk提供的。一旦拥有这些组件,你就有足够能力将C/C++转化成wasm代码。

其次,你需要将wasm代码转换为本机对象文件。Nathan Froyd说,“当我们第一次实现wasm沙箱时,经常有人问我们,‘为什么需要这个步骤?’你可以分发wasm代码,并在Firefox启动时在用户的机器上动态编译它。我们本可以做到这一点,但这种方法要求针对每个沙箱实例重新编译wasm代码。“

在每个源都位于单独进程中的情况下,每个沙箱都编译代码是不必要的重复。他们选择的方法支持在多个进程间共享已编译的本机代码,从而能节省大量内存。

这种方法还提高了沙箱的启动速度,这对于细粒度的沙箱非常重要,例如,将每次字体访问或图像加载的相关联代码置入沙箱。

通过Cranelift实现预编译

这种方法并不意味着必须自己编写将wasm代码编译成本机代码的编译器。

“我们用相同的编译器后端实现这种提前编译”,它最终将通过字节码联盟的Lucet编译器和运行时来支持Firefox JavaScript引擎的wasm组件:Cranelift

这种代码共享可确保JavaScript引擎和wasm沙箱编译器共享改进所带来的好处。由于工程原因,这两段代码目前使用不同版本的Cranelift。

然而,随着沙箱技术的成熟,“我们希望修改它们以使用完全相同的代码库”。

现在,Firefox工程师已将wasm代码转换为本机对象代码,“我们需要能从C++调用沙箱代码”。如果沙箱代码在单独的虚拟机中运行,这一步将涉及到在运行时查找函数名以及管理与虚拟机相关的状态。

但是,通过上面设置,沙箱代码是符合wasm安全模型的本机编译代码。因此,可以使用与调用常规本机代码相同的机制来调用沙箱函数。

“我们必须注意所涉及的不同机器模型:wasm代码使用32位指针,而我们最初的目标平台x86-64 Linux使用64位指针。但是,还有其他障碍需要克服,这就把我们带到转换过程的最后一步。”Nathan Froyd写道

确保沙箱正确

使用与常规本机代码相同的机制调用沙箱代码很方便,但它隐藏了一个重要细节。“我们不能相信任何来自沙箱的东西,因为对手可能已经损害沙箱”。

例如,有个沙箱函数:

/* 返回0到16之间的值。  */

int return_the_value();

不能保证这个沙箱函数遵循它的契约。因此,“要确保返回的值落在我们期望范围内”。

类似地,对于一个返回指针的沙箱函数:

extern const char* do_the_thing();

Nathan Froyd表示,“我们不能保证返回的指针实际上指向沙箱控制的内存。对手可能会强迫返回的指针指向应用程序沙箱之外的某个地方。因此,我们在使用指针前要验证它。”

在阅读源代码时,还有一些其他的运行时约束并不明显。

例如,上面返回的指针可能指向沙箱中动态分配的内存。在这种情况下,应该由沙箱释放指针,而不是由主机应用程序释放。“我们可以依靠开发人员始终记住哪些值是应用程序值,哪些值是沙箱值”。

经验表明,这种方法是不可行的。

污染数据

上面两个例子说明一个普遍原则:从沙箱返回的数据应该被明确标识。有了这个标识,我们就可以确保以适当方式处理数据。

我们将与沙箱相关的数据标记为“污染”。污染数据可以自由地操作(例如指针运算、访问字段),生成更多污染数据。

但是,当我们将污染数据转换为非污染数据时,我们希望这些操作尽可能明确。污染数据不仅对管理从沙箱返回的内存很有价值,它对于识别从沙箱中返回的可能需要额外验证的数据也很有价值,例如指向某个外部数组的索引。

因此,我们将沙箱中所有公开的函数建模为返回污染数据。这些函数还将污染数据作为参数,因为它们所操作的任何东西在某种程度上都必须属于沙箱。

一旦函数调用有了这个接口,编译器就变成了一个污染检查器。当污染数据在需要非污染数据的上下文中使用时,编译器将发生错误,反之亦然。

这些上下文正是需要传播污染数据和/或需要验证数据的地方。RLBox处理污染数据的所有细节,并提供一些特性,可以直接将库的接口增量转换为沙箱接口。

下一步工作

有了wasm沙箱的核心基础结构,我们就可以集中精力提高它在Firefox代码库中的影响力了——既可以将它带到所有支持的平台上,也可以将它应用到更多的组件上。

由于这种技术是轻量级的,并且易于使用,我们希望在接下来的几个月里对Firefox的更多部件进行快速沙箱化。

我们最初的努力集中在与Firefox绑定的第三方库上。此类库通常具有定义良好的入口点,并且不会与系统的其他部分广泛共享内存。然而,在未来,我们也计划将这项技术应用于甲方代码。

关于作者

Nathan Froyd是Firefox的软件工程师。在业余时间,他喜欢奥林匹克举重和阅读。

英文原文:

Securing Firefox with WebAssembly

  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/zzB4a6QpWbcrfsPKTSFg
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券