figma

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 ArrayBuffer storage 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.