LuaJIT 퍼징을 통한 안정성 및 기능 검증: VK Torantol 팀의 접근 방식

Fuzzing as the basis for effective development a case study of LuaJIT

작성자
HackerNews
발행일
2025년 05월 23일

핵심 요약

  • 1 VK Torantol 팀은 LuaJIT의 고질적인 문제점과 프로덕션 충돌을 해결하기 위해 C API, Lua API, 그리고 문법 기반 코드 생성기를 활용한 광범위한 퍼징 전략을 도입했습니다.
  • 2 LuaJIT의 단일 개발자 구조, 문서 및 회귀 테스트 부족 등의 어려움 속에서, 퍼징은 기존 테스트만으로는 발견하기 어려웠던 26개의 LuaJIT 버그와 6개의 참조 구현 버그를 발견하는 데 결정적인 역할을 했습니다.
  • 3 Google ClusterFuzz와 같은 자동화된 인프라를 통해 퍼징 테스트를 개발 프로세스에 통합함으로써, 버그 발견 시간을 단축하고 LuaJIT 코드 커버리지를 61%까지 끌어올려 시스템의 신뢰도를 크게 향상시켰습니다.

도입

본 발표는 VK Torantol 팀의 Sergey가 LuaJIT 프로젝트의 안정성 및 기능 검증을 위해 퍼징(fuzzing)을 활용한 경험과 그 성과에 대해 다룹니다. LuaJIT은 가볍고 다중 패러다임의 언어로, 주로 임베디드 시스템이나 비즈니스 로직 프로그래밍에 활용됩니다. 그러나 단일 개발자에 의한 유지보수, 공식 사양 부재, 회귀 테스트의 부족 등 여러 고유한 개발 및 운영상의 어려움을 겪고 있었습니다. Sergey는 이러한 문제점들을 극복하고 프로덕션 환경에서의 잦은 충돌을 해결하기 위해 퍼징 기법을 도입하게 된 배경과 그 과정을 상세히 설명합니다.

LuaJIT의 고유한 특성 및 개발 과제

LuaJIT은 1993년부터 개발된 Lua의 참조 구현인 C Lua와 달리 2005년부터 개발된 두 번째로 인기 있는 구현체입니다. 빠른 인터프리터와 트레이싱 JIT 컴파일러를 특징으로 하며, 다음의 사회적/기술적 특성으로 인해 개발에 어려움이 있었습니다.

  • 사회적 특성: 단일 개발자(Mike Pall)에 의한 유지보수, 소통 채널의 제한, 버그 리포트 관련 논쟁 및 사용자 차단 사례, 개발 계획의 불투명성.

  • 기술적 특성: 회귀 테스트의 부재, 공식 문서 부족, 난해한 코드 스타일(단일 문자 변수, 비일관적인 들여쓰기), 이로 인한 여러 포크(Torantol, OpenResty 등)의 발생.

LuaJIT 내부 구조 및 아키텍처

LuaJIT은 Lua 코드를 렉서(lexical analyzer)를 통해 토큰화하고 구문 분석하여 바이트코드로 변환합니다. 이 바이트코드는 인터프리터에서 실행되며, 자주 실행되는 “핫(hot)” 코드는 JIT 컴파일러에 의해 트레이스(trace)되어 중간 표현(IR)으로 변환됩니다. 이후 IR은 최적화 과정을 거쳐 머신 코드로 어셈블되어 실행됩니다. 이 과정에서 “가드(guard)”는 실행 경로의 제약을 나타내며, 경로가 이탈될 경우 인터프리터로 제어권이 다시 넘어갑니다. LuaJIT은 C 통합을 위한 FFI(Foreign Function Interface)와 표준 라이브러리도 포함합니다.

VK Torantol 팀의 문제 해결 노력과 퍼징 도입

VK Torantol 팀은 LuaJIT 지원에서 잦은 프로덕션 충돌로 인해 어려움을 겪었습니다. 기존에는 모든 테스트를 통합하고(76% 라인 커버리지 달성), 패치 백포팅 시 변경된 코드에 대한 테스트 커버리지를 의무화하며, upstream first 접근 방식을 채택했습니다. 하지만 이러한 노력에도 불구하고 프로덕션 충돌은 계속 발생했고, 결국 퍼징(fuzzing) 도입을 결정했습니다.

퍼징 테스트 설계 및 구현

  • C API 퍼징: LuaJIT C API의 함수 지표(function indicators)를 활용하여 함수의 동작(스택에서 가져오는 요소 수, 추가되는 요소 수, 예외 발생 여부)을 검증하는 테스트 오라클로 사용했습니다. 무작위 함수 호출과 인자 전달을 통해 C API의 정확성을 확인했습니다.

  • Lua API 퍼징: 약 200개의 LuaJIT 함수 각각에 대한 래퍼(wrapper)를 작성하고, 무작위 입력을 전달하는 네이티브 퍼징을 적용했습니다. 수학 함수와 같이 교환 법칙 등을 활용할 수 있는 경우 검증이 용이했지만, 문자열이나 테이블 관련 함수는 더 복잡한 검증이 필요했습니다.

  • Lua 코드 및 바이트코드 퍼징: 유효한 Lua 코드가 LuaJIT에서 올바르게 실행될 것이라는 가정하에, 문법 기반 프로그램 생성기를 개발했습니다. Protobuf를 사용하여 Lua 문법을 정의하고, 이를 통해 생성된 스키마로 Lua 프로그램을 생성하여 실행했습니다. 초기에는 구문 및 의미론적 오류(예: break, return의 잘못된 위치, 지원되지 않는 연산)와 재귀 호출/반복문으로 인한 타임아웃 문제가 발생했으며, 이를 해결하기 위해 문법 수정, 메타테이블 활용, 카운터 기반 타임아웃 방지 로직을 추가했습니다. 바이트코드 테스트를 위해서는 luadam 함수를 사용하여 Lua 코드를 바이트코드로 변환한 후 실행하는 방식을 채택했습니다.

개발 프로세스 통합 및 성과

이러한 퍼징 테스트는 Google ClusterFuzz 인프라에 통합되어 개발 프로세스에 자동화되었습니다. 이상 징후(assertion 실패, AddressSanitizer 오류) 발생 시 자동으로 버그 티켓이 생성되고, 패치 적용 후 문제가 재현되지 않으면 티켓이 자동으로 닫히는 시스템을 구축했습니다. 또한, 테스트 케이스 최소화 기능을 통해 버그 재현에 필요한 최소한의 코드를 파악할 수 있었습니다.

주요 성과: 인시던트 수 감소, 테스트 재활용성 증대, 개발 초기 단계에서 버그 발견(커밋 후 3일 이내 발견 사례), LuaJIT 코드 커버리지 61% 달성(모든 테스트 활용 시 90%까지 가능). 총 26개의 LuaJIT 버그와 6개의 참조 구현 버그를 발견했으며, 특히 reachable assertion 카테고리에서 많은 버그가 발견되었습니다. 이는 개발자들이 방어적 프로그래밍 스타일을 적극적으로 사용하여 코드 내 이상 징후를 더 쉽게 포착할 수 있었기 때문입니다.

결론

VK Torantol 팀의 LuaJIT 퍼징 프로젝트는 단일 개발자 중심의 폐쇄적인 개발 환경과 고질적인 기술적 문제로 인해 발생하던 프로덕션 충돌을 효과적으로 줄이는 데 성공했습니다. C API의 함수 지표 활용, Lua API의 래퍼 기반 퍼징, 그리고 문법 기반 Lua 코드 및 바이트코드 생성기를 통한 체계적인 접근 방식은 기존 회귀 테스트만으로는 발견하기 어려웠던 수많은 버그를 조기에 발견하는 데 기여했습니다. Google ClusterFuzz와의 통합을 통한 테스트 자동화는 개발 효율성을 높이고, 궁극적으로 LuaJIT 시스템의 전반적인 신뢰도를 크게 향상시켰습니다. 이 프로젝트는 오픈 소스로 공개되어 있으며, 향후 차등 퍼징(differential fuzzing)과 Lua 인터프리터의 효율적인 계측(instrumentation) 개선을 통해 더욱 발전할 계획입니다.

댓글 0

로그인이 필요합니다

댓글을 작성하거나 대화에 참여하려면 로그인이 필요합니다.

로그인 하러 가기

아직 댓글이 없습니다

첫 번째 댓글을 작성해보세요!