Emscripten으로 데이터 (새 탭에서 열림)
Figma의 저장 파일이 간헐적으로 손상되는 문제는 재현이 어려운 비결정적 이벤트와 C++의 메모리 안전성 문제 때문에 장기간 해결되지 않았다. 일반적인 메모리 디버깅 도구로 원인을 찾지 못한 뒤, 키보드·마우스 입력을 생성하는 퍼저로 이벤트를 반복 실행해 문제를 재현하는 데 성공했다. 최종 수정은 FlatBuffers의 잘못된 메모리 접근을 고치는 세 줄짜리 커밋이었지만, 원인을 추적하는 과정에서 Emscripten과 브라우저의 메모리 모델까지 분석해야 했다. ## 간헐적으로 발생한 저장 파일 손상 - Figma는 때때로 다시 읽을 수 없는 잘못된 저장 파일을 생성했다. - 당시 저장 형식은 다음 구조였다. - ZIP 파일 - 내부에 Google FlatBuffers로 인코딩된 문서 - 파일의 전체 바이트 구조는 대체로 정상처럼 보였지만, 뒤쪽 데이터를 가리키는 일부 오프셋이 0으로 변해 있었다. - 데이터가 원래 위치가 아닌 곳에 기록된 현상은 다음과 같은 메모리 안전성 위반을 의심하게 했다. - 초기화되지 않은 메모리 사용 - 해제된 메모리 접근(use-after-free) - 배열 범위를 벗어난 읽기·쓰기 ## C++ 메모리 오류의 추적 난이도 - Figma 에디터는 C++로 작성되어 있었다. - C++는 다음과 같은 장점 때문에 그래픽 소프트웨어에 적합하다. - FreeType, HarfBuzz, Skia 같은 저수준 라이브러리 활용 - 하드웨어와 직접 연결되는 언어 기능 - 성숙한 디버깅 및 최적화 도구 - 높은 성능과 세밀한 메모리 제어 - 반면 C++는 언어 자체가 메모리 안전성을 보장하지 않는다. - 복잡한 언어 설계, 오류가 발생하기 쉬운 표준 라이브러리 API, C에서 물려받은 레거시가 결합되어 대규모 프로젝트에서 메모리 오류를 완전히 피하기 어렵다. ## 일반적인 디버깅 방법의 한계 개발팀은 다음과 같은 방법을 시도했지만 문제를 발견하지 못했다. - 메모리를 해제하지 않아 use-after-free 가능성 제거 - macOS의 malloc 디버깅 옵션 활성화 - Valgrind가 보고한 모든 문제 수정 - 컴파일러 업그레이드 - Clang 정적 분석기가 발견한 문제 수정 이런 도구들은 많은 오류를 찾아내지만, 실행 조건에 따라 드물게 발생하거나 특정 메모리 배치에서만 나타나는 문제까지 항상 잡아내지는 못한다. ## 이벤트 기록과 퍼징으로 재현성 확보 - 문제의 핵심은 비동기 타이머와 네트워크 이벤트 등 웹 앱의 비결정성이었다. - 해결 전략은 다음과 같았다. - 사용자 이벤트를 기록한다. - 동일한 이벤트를 순서대로 재생한다. - 문제가 발생할 때까지 세션을 반복 실행한다. - 이벤트를 무작위로 제거하면서도 문제가 유지되는 최소 테스트 케이스를 만든다. - 실제 사용자 세션에는 너무 다양한 이벤트가 포함되므로, 범위를 키보드와 마우스 이벤트로 제한했다. - 실제 세션을 수집하는 대신 퍼저가 무작위 키보드·마우스 입력을 생성하도록 했다. - 며칠 동안 실행한 결과 여러 건의 저장 실패를 재현할 수 있었고, 이후 반복 가능한 입력 시퀀스를 기반으로 본격적인 디버깅이 가능해졌다. ## Emscripten이 C++를 브라우저에서 실행하는 방식 - Figma는 플러그인 없이 브라우저에서 실행되는 웹 앱이다. - C++ 코드는 Emscripten을 통해 JavaScript로 컴파일된다. - JavaScript는 기본적으로 메모리 안전성과 가비지 컬렉션을 제공하지만, WebGL과 Typed Arrays 덕분에 C의 메모리 모델을 흉내 낼 수 있다. - Typed Array의 특징은 다음과 같다. - 고정된 크기를 가진다. - 모든 원소가 같은 타입이다. - 여러 Typed Array가 하나의 ArrayBuffer를 공유할 수 있다. - 같은 메모리를 `Float32Array`와 `Uint8Array` 등 서로 다른 타입으로 해석할 수 있기 때문에 C/C++의 포인터 캐스팅과 유사한 동작을 구현할 수 있다. ## Emscripten의 포인터와 메모리 변환 Emscripten은 C++의 주요 요소를 JavaScript 구조로 변환한다. - 포인터 읽기: Typed Array 읽기 - 포인터 쓰기: Typed Array 쓰기 - 레지스터: JavaScript 지역 변수 - 스택: `STACKTOP`이라는 스택 포인터로 관리 - 타입 변환: 동일한 ArrayBuffer를 공유하는 Typed Array를 통해 구현 - 생성된 코드는 asm.js라는 제한적인 JavaScript 부분집합을 사용해 JIT 컴파일러가 타입을 추론하고 빠르게 최적화하도록 한다. - 예를 들어 `+$value`는 값을 double로 취급하도록 JIT에 힌트를 주며, 비트 연산은 정수 연산으로 최적화될 가능성을 높인다. ## 실용적인 결론 재현이 어려운 데이터 손상 문제는 도구를 하나씩 추가하는 것만으로 해결되지 않을 수 있다. 비결정적 입력을 통제하고, 퍼징과 이벤트 재생으로 실패 조건을 반복 가능하게 만드는 것이 핵심이며, 컴파일된 실행 환경에서는 원본 언어뿐 아니라 Emscripten의 메모리 모델과 런타임 동작까지 함께 이해해야 한다.