Electron 和 V8 内存笼
Electron 21 及更高版本将启用 V8 内存笼,这将对某些原生模块产生影响。
¥Electron 21 and later will have the V8 Memory Cage enabled, with implications for some native modules.
要跟踪 Electron 21+ 中关于原生模块使用情况的持续讨论,请参阅 electron/electron#35801。
¥To track ongoing discussion about native module usage in Electron 21+, see electron/electron#35801.
在 Electron 21 中,我们将在 Electron 中启用 V8 沙盒指针,紧随 Chrome 的 决定在 Chrome 103 中也采取同样的措施 之后。这对原生模块有一些影响。此外,我们之前在 Electron 14 中启用了相关技术 指针压缩。当时我们并没有过多讨论指针压缩,但指针压缩确实会影响 V8 的最大堆大小。
¥In Electron 21, we will be enabling V8 sandboxed pointers in Electron, following Chrome's decision to do the same in Chrome 103. This has some implications for native modules. Also, we previously enabled a related technology, pointer compression, in Electron 14. We didn't talk about it much then, but pointer compression has implications for the maximum V8 heap size.
启用这两项技术后,将显著提升安全性、性能和内存使用率。然而,启用它们也有一些缺点。
¥These two technologies, when enabled, are significantly beneficial for security, performance and memory usage. However, there are some downsides to enabling them, too.
启用沙盒指针的主要缺点是指向外部("off-heap")内存的 ArrayBuffer 不再被允许。这意味着依赖于 V8 中此功能的原生模块需要重构才能继续在 Electron 20 及更高版本中工作。
¥The main downside of enabling sandboxed pointers is that ArrayBuffers which point to external ("off-heap") memory are no longer allowed. This means that native modules which rely on this functionality in V8 will need to be refactored to continue working in Electron 20 and later.
启用指针压缩的主要缺点是 V8 堆的最大大小限制为 4GB。具体细节有点复杂 - 例如,ArrayBuffers 与 V8 堆的其余部分分开计数,但都有各自的 自身的限制。
¥The main downside of enabling pointer compression is that the V8 heap is limited to a maximum size of 4GB. The exact details of this are a little complicated—for example, ArrayBuffers are counted separately from the rest of the V8 heap, but have their own limits.
Electron 升级工作组 认为指针压缩和 V8 内存保护机制利大于弊。这样做主要有三个原因:
¥The Electron Upgrades Working Group believes that the benefits of pointer compression and the V8 memory cage outweigh the downsides. There are three main reasons for doing so:
-
它使 Electron 更接近 Chromium。Electron 在复杂的内部细节(例如 V8 配置)上与 Chromium 的差异越小,我们就越不可能意外引入错误或安全漏洞。Chromium 的安全团队非常强大,我们希望确保充分利用他们的成果。此外,如果一个 bug 仅影响 Chromium 中未使用的配置,那么修复它可能不会成为 Chromium 团队的首要任务。
¥It keeps Electron closer to Chromium. The less Electron diverges from Chromium in complex internal details such as V8 configuration, the less likely we are to accidentally introduce bugs or security vulnerabilities. Chromium's security team is formidable, and we want to make sure we are taking advantage of their work. Further, if a bug only affects configurations that aren't used in Chromium, fixing it is not likely to be a priority for the Chromium team.
-
性能提升。指针压缩可将 V8 堆大小减少高达 40%,并将 CPU 和 GC 性能提升 5% 至 10%。对于绝大多数不会遇到 4GB 堆大小限制且不使用需要外部缓冲区的原生模块的 Electron 应用来说,这些改进将带来显著的性能提升。
¥It performs better. Pointer compression reduces V8 heap size by up to 40% and improves CPU and GC performance by 5%–10%. For the vast majority of Electron applications which won't bump into the 4GB heap size limit and don't use native modules that require external buffers, these are significant performance wins.
-
更安全。一些 Electron 应用运行不受信任的 JavaScript(希望遵循我们的 安全建议!),对于这些应用,启用 V8 内存笼可以保护它们免受大量 V8 漏洞的攻击。
¥It's more secure. Some Electron apps run untrusted JavaScript (hopefully following our security recommendations!), and for those apps, having the V8 memory cage enabled protects them from a large class of nasty V8 vulnerabilities.
最后,对于确实需要更大堆大小的应用,有一些解决方法。例如,你可以在你的应用中包含一个 Node.js 副本,该副本在构建时禁用了指针压缩,并将占用大量内存的工作移至子进程。虽然有些复杂,但如果你决定根据特定用例进行不同的权衡,也可以构建一个禁用指针压缩的自定义 Electron 版本。最后,在不久的将来,wasm64 将允许使用 WebAssembly 在 Web 和 Electron 中构建的应用使用超过 4GB 的内存。
¥Lastly, there are workarounds for apps that really need a larger heap size. For example, it is possible to include a copy of Node.js with your app, which is built with pointer compression disabled, and move the memory-intensive work to a child process. Though somewhat complicated, it is also possible to build a custom version of Electron with pointer compression disabled, if you decide you want a different trade-off for your particular use case. And lastly, in the not-too-distant future, wasm64 will allow apps built with WebAssembly both on the Web and in Electron to use significantly more than 4GB of memory.
常见问题
¥FAQ
我如何知道我的应用是否受到此变更的影响?
¥How will I know if my app is impacted by this change?
在 Electron 20+ 版本中,尝试用 ArrayBuffer 封装外部内存会导致运行时崩溃。
¥Attempting to wrap external memory with an ArrayBuffer will crash at runtime in Electron 20+.
如果你的应用中未使用任何原生 Node 模块,则不会出现此崩溃 - 纯 JS 代码无法触发此崩溃。此更改仅影响在 V8 堆外分配内存(例如使用 malloc
或 new
),然后用 ArrayBuffer 封装外部内存的原生 Node 模块。这是一个相当罕见的用例,但有些模块确实使用了这种技术,这些模块需要重构才能与 Electron 20+ 兼容。
¥If you don't use any native Node modules in your app, you're safe—there's no way to trigger this crash from pure JS. This change only affects native Node modules which allocate memory outside of the V8 heap (e.g. using malloc
or new
) and then wrap the external memory with an ArrayBuffer. This is a fairly rare use case, but some modules do use this technique, and such modules will need to be refactored in order to be compatible with Electron 20+.
如何测量我的应用使用了多少 V8 堆内存,以了解是否接近 4GB 的限制?
¥How can I measure how much V8 heap memory my app is using to know if I'm close to the 4GB limit?
在渲染器进程中,你可以使用 performance.memory.usedJSHeapSize
,它将以字节为单位返回 V8 堆的使用情况。在主要流程中,你可以使用 process.memoryUsage().heapUsed
,它与 Chromium 相当。
¥In the renderer process, you can use performance.memory.usedJSHeapSize
, which will return the V8 heap usage in bytes. In the main process, you can use process.memoryUsage().heapUsed
, which is comparable.
什么是 V8 内存笼?
¥What is the V8 memory cage?
一些文档将其称为 "V8 沙盒",但该术语很容易与 Chromium 中的 其他类型的沙盒 混淆,因此我坚持使用 "内存限制"。
¥Some documents refer to it as the "V8 sandbox", but that term is easily confusable with other kinds of sandboxing that happen in Chromium, so I'll stick to the term "memory cage".
有一种相当常见的 V8 漏洞利用方式如下:
¥There's a fairly common kind of V8 exploit that goes something like this:
-
在 V8 的 JIT 引擎中发现一个 bug。JIT 引擎会分析代码,以便能够省略缓慢的运行时类型检查,并生成快速的机器码。有时逻辑错误会导致分析错误,从而忽略实际需要的类型检查 - 比如,它会认为 x 是一个字符串,但实际上它是一个对象。
¥Find a bug in V8's JIT engine. JIT engines analyze code in order to be able to omit slow runtime type checks and produce fast machine code. Sometimes logic errors mean it gets this analysis wrong, and omits a type check that it actually needed—say, it thinks x is a string, but in fact it's an object.
-
利用这种混淆来覆盖 V8 堆中的部分内存,例如,指向 ArrayBuffer 开头的指针。
¥Abuse this confusion to overwrite some bit of memory inside the V8 heap, for instance, the pointer to the beginning of an ArrayBuffer.
-
现在,你拥有一个指向任意位置的 ArrayBuffer,因此你可以在进程中读写任何内存,即使是 V8 通常无法访问的内存。
¥Now you have an ArrayBuffer that points wherever you like, so you can read and write any memory in the process, even memory that V8 normally doesn't have access to.
V8 内存笼是一种旨在彻底阻止此类攻击的技术。实现此漏洞的方法是不在 V8 堆中存储任何指针。所有对 V8 堆内其他内存的引用都存储为相对于某个保留区域起始位置的偏移量。这样一来,即使攻击者设法破坏了 ArrayBuffer 的基址(例如通过利用 V8 中的类型混淆错误),他们所能做的最坏的事情也不过是读取和写入 Cage 内的内存,而他们很可能已经这样做了。关于 V8 内存笼的工作原理,还有很多资料可供参考,所以我就不在这里赘述了 - 最好的入门资料可能是 Chromium 团队的 高级设计文档 模块。
¥The V8 memory cage is a technique designed to categorically prevent this kind of attack. The way this is accomplished is by not storing any pointers in the V8 heap. Instead, all references to other memory inside the V8 heap are stored as offsets from the beginning of some reserved region. Then, even if an attacker manages to corrupt the base address of an ArrayBuffer, for instance by exploiting a type confusion error in V8, the worst they can do is read and write memory inside the cage, which they could likely already do anyway. There's a lot more available to read on how the V8 memory cage works, so I won't go into further detail here—the best place to start reading is probably the high-level design doc from the Chromium team.
我想重构一个 Node 原生模块以支持 Electron 21+。我该怎么做?
¥I want to refactor a Node native module to support Electron 21+. How do I do that?
重构原生模块以兼容 V8 内存框架有两种方法。第一个方法是将外部创建的缓冲区复制到 V8 内存中,然后再传递给 JavaScript。这通常是一个简单的重构,但当缓冲区很大时可能会很慢。另一种方法是使用 V8 的内存分配器来分配最终要传递给 JavaScript 的内存。这虽然有点复杂,但可以避免复制,这意味着大缓冲区的性能会更好。
¥There are two ways to go about refactoring a native module to be compatible with the V8 memory cage. The first is to copy externally-created buffers into the V8 memory cage before passing them to JavaScript. This is generally a simple refactor, but it can be slow when the buffers are large. The other approach is to use V8's memory allocator to allocate memory which you intend to eventually pass to JavaScript. This is a bit more involved, but will allow you to avoid the copy, meaning better performance for large buffers.
为了更具体说明,以下是一个使用外部数组缓冲区的 N-API 模块示例:
¥To make this more concrete, here's an example N-API module that uses external array buffers:
// Create some externally-allocated buffer.
// |create_external_resource| allocates memory via malloc().
size_t length = 0;
void* data = create_external_resource(&length);
// Wrap it in a Buffer--will fail if the memory cage is enabled!
napi_value result;
napi_create_external_buffer(
env, length, data,
finalize_external_resource, NULL, &result);
当内存笼启用时,这将崩溃,因为数据是在内存笼外分配的。重构后,将数据复制到 Cage 中,我们得到:
¥This will crash when the memory cage is enabled, because data is allocated outside the cage. Refactoring to instead copy the data into the cage, we get:
size_t length = 0;
void* data = create_external_resource(&length);
// Create a new Buffer by copying the data into V8-allocated memory
napi_value result;
void* copied_data = NULL;
napi_create_buffer_copy(env, length, data, &copied_data, &result);
// If you need to access the new copy, |copied_data| is a pointer
// to it!
这会将数据复制到 V8 内存笼内新分配的内存区域中。N-API 还可以选择提供指向新复制数据的指针,以便你事后需要修改或引用它。
¥This will copy the data into a newly-allocated memory region that is inside the V8 memory cage. Optionally, N-API can also provide a pointer to the newly-copied data, in case you need to modify or reference it after the fact.
重构后使用 V8 的内存分配器会稍微复杂一些,因为它需要修改 create_external_resource
函数以使用 V8 分配的内存,而不是使用 malloc
。这可能或多或少可行,取决于你是否控制 create_external_resource
的定义。我们的想法是首先使用 V8(例如使用 napi_create_buffer
)创建缓冲区,然后将资源初始化到 V8 分配的内存中。保留 napi_ref
到 资源生命周期 的 Buffer 对象非常重要,否则 V8 可能会对 Buffer 进行垃圾回收,并可能导致释放后使用错误。
¥Refactoring to use V8's memory allocator is a little more complicated, because it requires modifying the create_external_resource
function to use memory allocated by V8, instead of using malloc
. This may be more or less feasible, depending on whether or not you control the definition of create_external_resource
. The idea is to first create the buffer using V8, e.g. with napi_create_buffer
, and then initialize the resource into the memory that has been allocated by V8. It is important to retain a napi_ref
to the Buffer object for the lifetime of the resource, otherwise V8 may garbage-collect the Buffer and potentially result in use-after-free errors.