Debugging Data Corruption with Emscripten | Figma Blog (opens in new tab)
Figma encountered intermittent save-file corruption caused by an elusive C++ memory-safety bug. Conventional debugging tools failed because the web app’s asynchronous behavior made the problem nondeterministic. A keyboard-and-mouse fuzzer eventually produced reproducible failures, while understanding Emscripten’s C++-to-JavaScript memory model helped narrow the investigation.
Detecting the Corruption
- Invalid save files appeared occasionally and could not be reliably reproduced.
- Figma’s files used ZIP containers around Google FlatBuffers documents.
- The serialized bytes looked mostly valid, but some offsets were unexpectedly zeroed.
- Data being written to the wrong location suggested a memory-safety violation such as:
- Use before initialization
- Use after free
- Out-of-bounds access
Why C++ Made the Bug Difficult
- C++ was valuable for Figma because it provided:
- Access to libraries such as FreeType, HarfBuzz, and Skia
- Low-level control suitable for graphics software
- Mature debugging and optimization tools
- However, C++ offers no built-in protection against memory errors.
- The team tried avoiding deallocation, enabling malloc diagnostics, fixing Valgrind and Clang Analyzer findings, and upgrading the compiler, but none exposed the corruption.
Reproducing the Failure with Fuzzing
- The team planned to eliminate nondeterminism by recording user events and replaying them deterministically.
- Building a complete session recorder was too large a project, so they limited inputs to keyboard and mouse events.
- A fuzzer generated random event sequences and ran them against the application.
- After several days, it produced multiple save failures, providing reproducible cases for debugging.
Emscripten’s Emulated Memory Model
- Figma’s C++ editor ran in the browser through Emscripten, which compiled C++ into JavaScript.
- JavaScript typed arrays and shared
ArrayBufferstorage allowed Emscripten to emulate contiguous C++ memory. - In the generated code:
- Pointer loads became typed-array reads.
- Pointer stores became typed-array writes.
- Registers became local variables.
- Shared buffers enabled pointer reinterpretation between types.
- Emscripten generated asm.js-style JavaScript, using type annotations and operations optimized for JavaScript JIT compilers.
The combination of deterministic fuzzing and knowledge of Emscripten’s low-level memory representation provided the path toward isolating the corruption, even though the ultimate fix was reportedly only a three-line change.