01 Summary
ARM/AArch64 NEON 최적화가 paletted PNG의 partial tail을 처리할 때 reverse pointer를 잘못 미리 이동하고 loop bound도 잘못 계산해 row buffer 앞쪽을 읽고 썼습니다.
구분 | 확인 내용 |
Component | libpng ARM/AArch64 NEON palette expansion |
Attack Input | tail width가 남는 crafted paletted PNG |
Preconditions | 영향받는 NEON 경로가 활성화된 ARM/AArch64 환경 |
Sink | reverse pointer pre-adjust와 full-chunk loop bound의 불일치 |
Verified | RGBA width 5·RGB width 9 harness에서 ASan OOB read/write 확인 |
Impact | process crash 및 memory corruption 가능성 |
Fix Principle | vector loop는 완전한 chunk만 처리하고 scalar tail 전에 pointer 복원 |
목차 · 섹션으로 이동
Attack Chain
검증 결론: 서로 다른 RGB·RGBA tail harness가 같은 pointer/loop 결함을 재현해 최적화 경로의 out-of-bounds primitive를 확인했습니다.
Disclosure Timeline
Date | Event | Evidence |
2026-03-26 | CVE 공개 | |
2026-03-26 | libpng 1.6.56 및 1.8.0 수정 정보 공개 | |
2026-08-23 | Docker/로컬 원본 대조와 GitHub push 감사 |
02 Vulnerability
libpng는 팔레트 인덱스 한 바이트를 RGB 또는 RGBA 픽셀로 확장합니다. ARM/AArch64 NEON 최적화 구현은 행의 뒤에서 앞으로 고정 크기 묶음을 처리했습니다. RGBA는 4픽셀, RGB는 8픽셀 단위였습니다.
행 너비가 처리 단위의 배수가 아니면 마지막 반복에 충분한 입력이 남지 않았지만 루프는 그대로 실행됐습니다. 역방향 처리 때문에 단순한 tail 초과가 아니라 입력 포인터와 출력 포인터가 모두 행 시작 전으로 이동했습니다.
공격자가 제어하는 값 | PNG 팔레트 행 너비와 픽셀 데이터 |
취약 경로 | arm/palette_neon_intrinsics.c |
아키텍처 | ARM/AArch64에서 NEON 최적화가 활성화된 경우 |
영향 | 행 버퍼 앞쪽 OOB read와 OOB write |
03 Approach
이미지 decoder의 SIMD 경로를 볼 때 scalar 구현과 벡터 구현의 tail 처리 차이를 우선 비교했습니다. 고정 폭 벡터 연산은 마지막 블록이 꽉 차지 않을 때 별도 처리나 루프 상한이 필요합니다.
NEON 코드는 출력 버퍼 끝에서 시작하도록 포인터를 미리 15바이트 또는 23바이트 조정한 뒤, 루프마다 4개 또는 8개의 인덱스를 읽었습니다.
i < row_width 조건은 남은 픽셀 수가 한 블록보다 적은 경우를 막지 못했습니다.행 끝의 partial chunk에서도 NEON 루프가 고정 크기 load와 store를 실행하는가?
04 Root Cause
RGBA 너비가 5이면 첫 반복은 4픽셀을 정상 처리하지만 두 번째 반복에는 1픽셀만 남습니다. 코드는 여전히 4픽셀을 읽고 16바이트를 쓰기 위해 포인터를 뒤로 이동하므로
row - 3 부근을 읽고 행 시작 전 출력 위치에 씁니다.RGB 너비 9에서도 같은 구조가 8픽셀 단위로 나타났습니다. 이 문제는 generic C, SSE, VSX, LSX 구현 전체의 공통 문제가 아니라 ARM NEON 경로의 역방향 포인터 산술과 loop bound의 조합이었습니다.
palette_neon_intrinsics.c · 취약 루프의 구조
// RGBA: 4 pixels per iteration for (i = 0; i < row_width; i += 4) { sp -= 4; dp -= 16; // fixed-size load and 16-byte store } // row_width=5일 때 두 번째 반복도 실행됨
입력에서 영향까지의 경로
- 공격자 PNG가 8-bit palette row를 제공합니다.
- libpng가 ARM NEON palette expansion을 선택합니다.
- 전체 블록을 처리한 뒤 partial tail이 남습니다.
- 고정 크기 load/store가 다시 실행됩니다.
- 입력과 출력 포인터가 row 시작 전을 참조합니다.
05 Reproduction & Validation
x86 호스트에서 코드만 모사하지 않고 AArch64 바이너리를 만들어 QEMU user-mode에서 실행했습니다. 일반 harness로 crash를 확인한 뒤 ASan용 harness로 guard 영역을 둬 read와 write 위치를 구분했습니다.
RGBA 너비 4와 RGB 너비 8을 정상 대조군으로, 각각 한 픽셀 더 긴 5와 9를 경계 입력으로 두었습니다. 폭이 정확히 처리 단위일 때는 통과하고 한 픽셀을 더했을 때 실패하는지 확인했습니다.
$ qemu-aarch64 ./neon_palette_asan_harness rgba 5 $ qemu-aarch64 ./neon_palette_asan_harness rgb 9
검사 | 입력·조건 | 관찰 | 의미 |
RGBA 대조군 | width 4 | 정상 종료 | 완전한 4-pixel block은 정상 |
RGBA 경계 | width 5 | row 앞쪽 접근 | 1-pixel tail에서 OOB |
RGB 대조군 | width 8 | 정상 종료 | 완전한 8-pixel block은 정상 |
RGB 경계 | width 9 | row 앞쪽 접근 | 1-pixel tail에서 OOB |
AArch64 재현에서 고정한 경계 조건
RGBA chunk = 4 pixels width 4 -> complete block width 5 -> second iteration with 1 pixel remaining read begins before row / write begins before row RGB chunk = 8 pixels width 8 -> complete block width 9 -> second iteration with 1 pixel remaining
06 Impact & Fix
공격자가 만든 PNG를 정상 decode하는 과정에서 ARM NEON 경로가 행 버퍼 바깥을 읽고 쓸 수 있습니다. 이미지 처리 프로세스의 crash와 인접 메모리 훼손이 확인된 영향입니다.
확인한 범위
- AArch64 NEON 경로의 partial tail 실행
- RGBA와 RGB 양쪽의 row-before-start 접근
- 정상 폭과 경계 폭의 차이
제외한 범위
- x86 SIMD 경로의 영향
- 안정적인 정보 유출
- 원격 코드 실행 exploit
수정
수정은 전체 블록만 NEON 루프에서 처리하고 남은 tail은 안전한 경로로 넘겼습니다. 동시에 역방향 destination pointer의 사전 조정을 되돌려, loop bound와 pointer 기준을 함께 맞췄습니다.
07 Turning Points
- 처음에는 loop condition만 바꾸면 된다고 봤지만 reverse destination pointer의 사전 보정까지 함께 맞춰야 수정이 완전함을 확인했습니다.
- scalar와 다른 SIMD 구현을 대조해 일반 libpng 문제가 아니라 ARM/AArch64 NEON 활성화 조건으로 범위를 좁혔습니다.
- OOB write를 확인했어도 exploitability를 입증하지 않았으므로 원격 코드 실행 주장은 제외했습니다.
Stage | Prior Belief | New Evidence | Revised Judgment | Trigger |
Tail 처리 | loop condition만 i + chunk <= row_width로 바꾸면 충분 | destination pointer가 사전에 15 또는 23바이트 이동 | loop bound와 pointer 기준을 함께 수정해야 함 | pointer 산술 검토 |
아키텍처 범위 | ASan 경고를 decoder 전체 문제로 확대할 수 있음 | scalar와 다른 SIMD 구현 대조 | ARM/AArch64 NEON 활성화 조건으로 한정 | 구현 비교 |
영향 범위 | OOB write 확인 | 보존 결과는 OOB read/write와 crash, exploitability 미입증 | 원격 코드 실행을 결론에서 제외 | 증거 수준 검토 |
Investigation Log
전체 판단 변화 원문
Tail 처리
처음에는 단순히 loop condition을
i + chunk <= row_width로 바꾸면 끝날 것으로 봤습니다. 하지만 이 함수는 역방향 처리를 위해 destination pointer를 미리 15 또는 23바이트 당겨 둡니다. 루프만 줄이면 남은 픽셀을 처리할 포인터 기준 자체가 달라져 수정이 불완전했습니다.아키텍처
ASan 경고를 일반적인 libpng decoder 전체의 문제로 확대하지 않았습니다. scalar와 다른 SIMD 구현을 대조한 결과 문제의 포인터 이동은 NEON 파일에 국한됐습니다. 영향 조건에 ARM/AArch64와 NEON 활성화를 명시했습니다.
코드 실행
OOB write가 있다는 사실만으로 원격 코드 실행을 주장하지 않았습니다. 보존된 결과는 경계 밖 읽기·쓰기와 crash였습니다. 할당 배치와 호출 환경을 통제해 exploitability를 입증하지 않았기 때문에 그 범위는 결론에서 제외했습니다.
Role & Credit
- Credit — Public Attribution
08 Provenance
항목 | 최종 감사 결과 |
소스 기준 | libpng checkout 28cb99fe65f09e79703ac2c3008649e14c7b0844 |
원본 검증 | AArch64 QEMU guard harness에서 RGBA 4·RGB 8 정상 종료, RGBA 5·RGB 9 SIGSEGV 재확인 |
GitHub 보존 상태 | Historical audit commit c04cacf 당시 evidence 6개와 root manifest.json에 size·SHA-256을 고정했습니다. 현재 main은 evidence 5개이며 root manifest.json은 제거됐습니다. |
무결성 | 사례별 SHA256SUMS 검증 통과, root manifest의 모든 file hash와 실제 파일 일치, README 상대 링크 확인 완료 |
보존 경계·제약 | 보존 ASan binary는 정상 대조군도 QEMU에서 실패해 이번 fresh control로 채택하지 않음. 원 공개 advisory의 ASan 증거와 fresh guard 관찰을 구분하며 core·binary는 제외 |
감사 완료 | 2026-08-23 · Docker/로컬 원본 대조 후 GitHub push 완료 |
링크 기준: 현재 main에 남아 있는 사례·evidence 링크는 main을 유지하고, 현재 main에서 삭제된
SHA256SUMS와 SOURCE-PROVENANCE.md 링크는 검증된 historical snapshot 322da5d0e45b3e13e6487c4a8c9eb04124233f56에 고정했습니다.이전 소개문 · 원문 보존
8-bit 팔레트 행의 남은 픽셀이 NEON 처리 단위보다 짧을 때 역방향 포인터가 행 앞쪽으로 넘어갔습니다. AArch64 harness와 ASan으로 읽기와 쓰기 경계를 따로 확인했습니다.
Taegu Ha · libpng · CWE-125 · CWE-787 · CVSS 7.6
2026년 3월 26일 공개 · libpng 1.6.56 및 1.8.0에서 수정 · 공식 기록
09 Artifacts
2026-08-23 최종 감사
AArch64용 일반·guard harness, fresh QEMU 경계 관찰 기록, 공개 advisory와 CVE 레코드를 포함했습니다. 보존 ASan binary의 정상 대조군 문제를 명시했고 core dump·binary는 제외했습니다.
- neon_palette_asan_harness.c — guard 영역으로 row 앞쪽 접근을 구분한 AArch64 harness
- neon_palette_harness.c — NEON palette expansion 최소 실행 harness
- github-advisory.json — 공개 보고서와 수정 정보
- cve-record.json — CVEProject 공개 레코드
10 References
11 Takeaways
Original Takeaways
SIMD 취약점은 loop condition 한 줄보다 포인터가 어느 방향으로 움직이고 사전에 얼마나 보정됐는지를 함께 봐야 했습니다. 특히 ‘마지막 블록이 짧다’는 익숙한 문제도 역방향 구현에서는 buffer end가 아니라 start 아래로 나타날 수 있었습니다.
수정안을 검토할 때도 crash를 없애는 입력 하나가 아니라 정상 폭, 한 픽셀 tail, RGB와 RGBA를 같이 고정해야 했습니다. 이후 벡터화 코드는 chunk 크기와 tail policy를 표로 만든 뒤 포인터 산술을 추적하는 습관이 생겼습니다.