메모리 안전한 WebP 디코딩

Memory-Safe WebP Decoding

halide.cx ▲ 62 댓글 20 computerbuster

요약

헤일라이드가 libwebp보다 안전하고 빠른 Rust 기반 WebP 디코더 wpd를 공개했습니다.

압축 기술 회사 헤일라이드(Halide) 팀의 발표입니다. 2023년 libwebp 취약점 CVE-2023-4863이 Chrome, Firefox, Signal 등을 쓰는 기기 수십억 대를 위험에 빠뜨린 일이 계기가 됐습니다.

왜 중요한가

  • Chromium 통계로는 심각한 보안 버그의 약 70%가 메모리 안전 문제이고, wpd는 이 원인을 언어 차원에서 막습니다.
  • 안전한 Rust 디코더(image-webp)는 이미 있었지만, wpd는 속도에서도 libwebp를 앞선다는 점이 다릅니다.
  • 제약이 아주 적은 BSD 2조항 라이선스로 공개해 누구나 무료로 가져다 쓸 수 있습니다.

핵심 내용

  • 단일 스레드에서 libwebp보다 손실 압축은 1.19배, 무손실 압축은 2.74배 빠르다고 밝혔습니다.
  • 멀티스레드에서는 손실 2.68배, 무손실 3.19배로 차이가 더 벌어졌습니다.
  • 손실·무손실, 알파(투명도), 애니메이션 등 libwebp의 기능을 모두 갖췄습니다.
  • 스레드 수 지정과 애니메이션 프레임 병렬 디코딩 기능을 새로 더했습니다.
  • 손으로 짠 어셈블리를 빼고 빌드하면 안전하지 않은 코드는 검증된 zerocopy 크레이트(Rust 라이브러리)에만 남습니다.

작성자 설명

작성자는 본문에 소스 저장소 주소(https://github.com/halidecx/wpd)만 남겼습니다.

HN 반응

  • 구글의 파일 형식용 안전 언어 Wuffs와 견주는 댓글이 많았고, 개발자 측은 자체 측정에서 wpd가 2~8배 빠르다고 답했습니다.
  • Wuffs 작성자와 일부 이용자는 어셈블리가 안전성의 빈틈이라고 지적했고, 개발자 측은 경계를 검사하는 래퍼 뒤에 두었다고 반박했습니다.

댓글

20개 표시 · 전체 20개
  1. omoikane HN

    Wuffs 기반 WebP 디코더가 지금 어떤 상태인지는 잘 모르겠습니다만, 안전성과 속도라는 같은 목표를 갖고 있으니 비교해 볼 만한 또 다른 구현체일 것 같습니다.

    https://github.com/google/wuffs/tree/main/std/webp

  2. nigeltao HN

    Wuffs 작성자입니다.

    Halide 팀의 새 디코더 wpd를 축하드립니다. 성능 수치가 인상적이네요. wpd(Rust 라이브러리)가 쓰시는 환경에 맞는다면 좋은 일이니 그냥 쓰시면 됩니다!

    다만 Wuffs가 언급됐으니, 그래도 Wuffs를 고려해 볼 만한 이유를 몇 가지 말씀드리겠습니다.

    1. Wuffs 구현은 C 코드로 트랜스파일됩니다. 그렇게 만들어진 C 코드도 저장소에 함께 올라가 있고, 더 가벼운 google/wuffs-mirror-release-c GitHub 저장소에도 올라가 있습니다. 기존 프로젝트가 Rust가 아니라 C/C++라면 Wuffs를 의존성으로 추가하기가 아주 쉽습니다. 다른 서드파티 C 라이브러리를 추가하는 것과 똑같습니다. 다만 손으로 짠 .c 코드가 아니라, 손으로 짠 .wuffs 코드가 단일 파일 C 라이브러리로 트랜스파일된 것일 뿐입니다. STB 라이브러리만큼 통합하기 쉬우면서 메모리 안전합니다.

    1.a. 마찬가지로 Python 프로젝트든 Java든 뭐든, C 코드를 감쌀 수 있다면 Wuffs 라이브러리(C 형태)도 감쌀 수 있습니다. 빌드 과정에 새 툴체인을 추가하지 않고도 프로세스 안에서 메모리 안전한 이미지 디코딩을 할 수 있습니다.

    1. Wuffs는 무권한(zero-capability) 언어입니다. 계산만 하는 (안전한) 라이브러리를 쓰기 위한 언어이고, 애플리케이션을 쓰기 위한 언어가 아닙니다. Wuffs 언어로는 파일을 열 수도, 네트워크에 쓸 수도 없습니다. 동적으로 메모리를 할당하는 것조차 안 됩니다. 그래서 Wuffs 코드는 stdin에서 읽고 stdout에 쓰는 것 말고는 사실상 모든 것을 막는 SECCOMP_MODE_STRICT 샌드박스 안에서 돌릴 수 있습니다.

    2.a. 프로세스를 분리해 극도로 메모리 안전하게 이미지를 디코딩하려는 경우를 위해, Wuffs 저장소의 example/convert-to-nia/convert-to-nia.c 프로그램은 stdin으로 이미지(JPEG, PNG, WebP 등)를 읽어 stdout으로 NIA(Farbfeld와 비슷한 아주 단순한 이미지 포맷)를 씁니다. Wuffs 언어와 툴체인 자체를 감사하고 싶지 않으시더라도, 이 convert-to-nia 프로그램의 보안 검토는 그야말로 간단합니다. main 함수가 가장 먼저 하는 일 중 하나가 스스로 SECCOMP_MODE_STRICT 샌드박스를 거는 것이기 때문입니다.

    1. Wuffs도 C/C++처럼 SIMD에 intrinsic을 쓰지만, 모든 로드와 스토어에 메모리 안전성이 강제됩니다. 또 툴체인은 cpuid상 AVX2를 지원하지 않으면 Wuffs 일반 코드가 AVX2를 쓰는 Wuffs 코드를 호출하지 못하게 강제합니다. 이 모든 것의 메모리 안전성 측면은 "SIMD를 어셈블리로 unsafe 블록 안에서 한다"보다는 조금 낫습니다. Wuffs(언어)에는 "unsafe" 키워드 자체가 없습니다.
  3. computerbuster HN

    다 맞는 말씀이고, Wuffs 프로젝트가 특정 용도에서는 충분히 말이 된다고 생각합니다. 다만 wpd를 설명하면서 "SIMD를 어셈블리로 unsafe 블록 안에서 한다"고 하시는 건 조금 잘못 짚으신 것 같습니다. wpd는 넓은 unsafe 영역 안에서 Rust SIMD로 디코더를 구현하지 않습니다. 크레이트 전체에서 unsafe 코드는 금지되어 있고, 전용 어셈블리 바인딩 모듈에서만 허용됩니다. 어셈블리를 끄면 크레이트는 forbid(unsafe_code)를 씁니다.

    손으로 짠 SIMD 커널은 안전한 Rust 래퍼 뒤에 있고, 이 래퍼가 raw 포인터를 만들어 내기 전에 정확한 슬라이스 범위를 계산하거나 검증합니다. CPU별 루틴도 런타임에 기능을 감지한 뒤에만 설치됩니다. wpd에는 어셈블리 커널이 래퍼가 정해 준 범위 밖을 읽거나 쓰지 않는지 확인하는 가드 페이지 테스트까지 있습니다. 손으로 짠 SIMD는 이 모든 수고를 들일 만큼 가치가 있다고 판단했습니다(근거는 dav1d 관련 발표를 몇 개 보시면 되는데, 저희도 같은 견해입니다).

    Wuffs는 컴파일러가 SIMD 구현 내부의 로드와 스토어까지 검사한다는 점에서 더 강한 형식적 속성을 갖고 있는 게 맞습니다. 하지만 흥미로운 메모리 안전성 공격 표면(공격자가 통제하는 구조의 파싱과 디코딩) 대부분은 wpd에서도 전부 안전한 Rust에 있으니, wpd도 여전히 안전하고 libwebp보다는 분명히 크게 나아졌다고 생각합니다.

    추신: 원하시면 wpd의 어셈블리를 끌 수 있고, 그러면 안전하면서 Wuffs와 비슷한 성능이 나옵니다.

  4. computerbuster HN

    알려 주셔서 감사합니다. Wuffs에 WebP 디코더가 있는 줄은 몰랐네요. 방금 벤치마크를 돌려 봤는데(b2e6da3), 제 M5 Pro에서 wpd가 손실 압축 정지 이미지는 2~3배, 알파가 있는 손실 압축은 5~7배, 무손실은 7~8배 빠릅니다(싱글 스레드인데도요).

    그리고 Wuffs는 libwebp와 비트 단위로 똑같지 않고, 애니메이션 WebP는 아예 지원하지 않아서 WebP 디코딩의 현실적인 선택지인지 잘 모르겠습니다.

  5. nigeltao HN

    Wuffs 작성자입니다.

    2월에 손실 압축 WebP 지원을 추가하는 풀 리퀘스트( https://github.com/google/wuffs/pull/168 )가 들어왔습니다. 무손실 WebP는 (어떤 면에서는 WebP라는 "브랜드"만 가져다 쓴 완전히 다른 포맷인데) Wuffs 구현이 나온 지 벌써 몇 년 됐고요.

    아무튼 그 PR에는 SIMD 가속이 포함되어 있었고 성능은 libwebp(C 코드)와 맞먹었습니다.

    제가 보기에 그 PR의 코드는 상당 부분, 혹은 대부분 AI의 도움을 받아 작성된 것이었습니다. 기능 면에서는 좋은 일이지만, 저는 여전히 사람이 직접 짠 코드를 더 믿습니다.

    그래서 그 뒤로 PR을 손으로 다시 쓰는 작업을 해 왔습니다. 애니메이션 지원도 추가하고 싶고, 그래서 지난달에는 (비교 기준으로) 애니메이션 WebP를 디코딩하고 테스트할 수 있도록 애니메이션 PNG 관련 Wuffs 도구 변경분을 반영했습니다.

    수작업 재작성의 상당 부분은 이미 커밋했지만 SIMD 부분은 아직 들어가지 않았습니다. 그러니 (PR이 아니라) main 브랜치에 있는 것은 아직 성능이 libwebp만 못한 게 맞습니다. 다만 SIMD 부분이 들어가면 해결될 겁니다.

    wuffs는 libwebp와 비트 단위로 똑같지 않고

    처음 듣는 얘기입니다. PR 168에는 libwebp와 픽셀 단위로 똑같은 출력을 낸다고 적혀 있습니다. main 브랜치에 있는 것도 픽셀 단위로 똑같은 출력을 목표로 하고 있고요. 예를 들어 YUV를 RGB로 바꿀 때 libjpeg의 공식이 아니라 libwebp의 공식을 구현했습니다. 둘 다 BT.601을 쓰지만 libwebp는 스튜디오 레인지, libjpeg는 풀 레인지를 씁니다.

    비트 단위로 똑같지 않은 .webp 이미지 예시를 링크해 주실 수 있나요?

  6. computerbuster HN

    모든 구현체가 나아지도록 기꺼이 돕고 싶습니다. Halide로 이메일을 한 통 보내 주시면 따로 이야기해 볼 수 있을까요? 도울 수 있다면 기쁘겠습니다!

  7. jessa0 HN

    관련해서 Signal Messenger의 webpsan 크레이트가 있습니다. libwebp에 넘기기 전에 webp 컨테이너 문법을 검증해 주는 크레이트입니다. 다만 실제 픽셀 데이터를 디코딩하는 데까지는 가지 않습니다. webp는 픽셀 데이터를 온전히 디코딩하려면 캔버스 전체를 할당해야 하는 방식이라서, 거기까지 하면 webpsan이 결국 완전한 디코더가 되어 버리기 때문입니다.

    https://docs.rs/webpsan/latest/webpsan/

  8. thereisno HN

    파서와 분리된 검증기는 과거에 수많은 취약점으로 이어졌습니다. 검증기가 못 잡아낸 무언가가 결국 파서를 죽이는 일이 늘 있죠.

  9. nine_k HN

    unsafe 파서 앞에 검증기를 두는 건 unsafe 파서만 쓰는 것보다 분명히 낫습니다. 하지만 안전한 파서보다는 못합니다.

  10. computerbuster HN

    좋네요, 이건 몰랐습니다!

  11. ZeroGravitas HN

    구글이 곧 내놓을 Gemini 4 Argon LLM 출시 홍보 자료에서, 손으로 짠 SIMD를 대체하면서 몇 가지를 Rust로 다시 썼다고 밝혔습니다. 다만 webp가 언급된 것 같지는 않습니다.

    이렇게 말했습니다.

    대규모 코드베이스 마이그레이션과 최적화: Argon 에이전트들은 구글 전역에서 C/C++ 코드베이스를 Rust로 옮기는 작업을 하고 있습니다. re2, libgav1 같은 핵심 라이브러리의 수만 줄부터 Fuchsia Zircon 커널의 80만 줄 이상까지 규모가 다양합니다. 이런 시스템 중 상당수가 매우 중요한 만큼, 이렇게 큰 규모의 재작성은 운영 환경에 내보내기 전에 엄격한 자동 감사와 수동 감사, 에뮬레이션 테스트, 검토를 거치고 있습니다.

    구글의 오픈소스 영상 디코딩 소프트웨어인 libgav1에서는, Argon 에이전트가 기존 Rust 포팅본을 가져다가 프로파일 기반 실험을 여러 차례 반복하고 컴파일러 출력을 분석하면서 SIMD 코드 3만 2천 줄을 교체했습니다. 컴파일러가 자동으로 벡터화할 수 있게 안전한 Rust를 만든 것입니다. 그 결과 기존 Rust 포팅본보다 2.7배 빠르고 영상 출력은 동일한, 메모리 안전한 영상 디코더가 나왔으며, 최적화된 C++ 버전에 더 가까워졌습니다.

  12. kangalioo HN

    Wuffs는 어떻게 된 건가요? https://github.com/google/wuffs

    안전성과 성능을 갖춘, 파일 포맷 라이브러리 전용으로 만든 프로그래밍 언어인데요.

  13. burntcaramel HN

    저는 libwebp를 WebAssembly로 컴파일해서 쓸 수 있는 웹 페이지를 만들었습니다. https://qip.dev/webp-to-png

    같은 wasm을 커맨드 라인에서도 돌릴 수 있습니다.

      npx @qip.dev/qipx qip.dev run \
        image/webp/webp-to-ktx2-r8g8b8a8-srgb.wasm \
        image/ktx2/ktx2-r8g8b8a8-or-b8g8r8a8-srgb-to-png.wasm \
        < input.webp > output.png
    
  14. computerbuster HN

    libwebp와 비교한 성능 수치가 있나요?

  15. pyrolistical HN

    대부분을 wasm으로 두면 안 되나요? JS 없이 돌아가는 wasm 런타임도 있고, 구글이라면 자사 엔진을 거기에 맞춰 고칠 수 있을 텐데요. 그런 다음 반대편은 Rust와 연동하면 되지 않을까요.

  16. computerbuster HN

    wasm으로 컴파일하거나 wasm 디코더를 배포할 수는 있습니다. JS 디코더를 배포하는 것도 가능하고요. 하지만 속도와 리소스 소모가 중요해서 둘 다 선택지에서 빠집니다.

  17. F3nd0 HN

    글에서 직접 언급되지는 않았지만, 이 디코더는 자유 소프트웨어로 라이선스되어 있고(그러면서도) Claude를 많이 써서 개발한 것으로 보입니다.

    안전을 위해 손으로 짠 어셈블리 없이 컴파일할 수 있다는 점은 좋은데, 그 경우 성능은 어떤가요? 제시된 벤치마크에는 이 수치가 없는 것 같은데, 맞나요?

  18. pizlonator HN

    진짜 메모리 안전성을 원하신다면 webp는 fil-C로도 문제없이 빌드됩니다.

    이 프로젝트도 훌륭하지만 단서가 붙습니다.

    • 어셈블리 코드가 있습니다. 조심스럽게 짰을 수는 있어도 탈출구(escape hatch)입니다.

    • 의존성이 조금이라도 있다면 그것들이 연쇄적으로 unsafe 코드를 더 끌어들일 가능성이 높습니다.

  19. tialaramex HN

    Fil-C가 충분히 의미 있는 경우도 있지만, 이건 그런 경우가 아닙니다.

    WUFFS를 쓸 여유가 없다면 이 Rust 구현이 합리적인 선택입니다. Fil-C는 쓸 여유가 되는데 WUFFS는 쓸 여유가 안 되는 현실적인 경우는 없을 겁니다.

    비슷한 개발 노력을 들였을 때 WUFFS 코덱은 Fil-C보다 분명히 훨씬 빠를 뿐 아니라 컴파일 타임에 완전히 안전합니다. 반면 Fil-C에서는 실수가 있으면 운영 환경에서 잡히고, 그 결과는 서비스 거부(DoS)입니다.

    그런 면에서 Rust는 절충안입니다. WUFFS가 컴파일 중에 잡아내는 것을 전부 잡지는 못하지만, Fil-C보다는 훨씬 많이 잡아냅니다.

  20. computerbuster HN
    1. 어셈블리 없이 컴파일할 수 있고, SIMD도 실제로 하는 일을 제대로 생각하면 안전하게 짤 수 있다고 믿습니다.
    2. 유일한 의존성은 zerocopy인데, 안전하다고 여겨지는 여러 주요 프로젝트에서 쓰고 있습니다.

Hacker News에서 보기 ↗