Rust와 C/C++의 메모리 안전성 CVE는 왜 다르게 집계되는가

2 hours ago 3
  • Rust와 C/C++의 CVE 숫자를 그대로 비교하면, 메모리 안전성 취약점을 “라이브러리 문제”로 보는 기준 차이를 놓치기 쉬움
  • C/C++에서는 잘못된 API 호출로 UB나 세그폴트가 나도 대개 사용자 코드의 오용으로 처리되며, 가능성 자체를 전부 CVE로 등록하지 않음
  • libcurl의 curl_getenv(NULL) 호출은 경고 없이 빌드되고 실행 시 세그폴트가 날 수 있지만, 보통 curl 취약점으로 보지는 않음
  • Rust에서는 사용자 코드에 unsafe가 없는데 안전한 API 호출만으로 메모리 버그가 발생하면, 라이브러리의 soundness bug로 간주함
  • 그래서 Rust의 일부 CVE는 C/C++보다 더 엄격한 기준으로 기록되며, 원시 CVE 수 비교만으로 메모리 안전성을 판단하기 어려움

CVE 숫자 비교가 흔들리는 이유

  • CVE는 소프트웨어 보안 취약점을 분류하고 보고하는 데이터베이스임
  • 취약점은 단순한 프로그램 로직 버그에서 생길 수도 있고, 익스플로잇으로 이어지기 쉬운 메모리 안전성 문제에서 비롯될 수도 있음
  • Rust와 C/C++의 CVE 수를 비교하며 Rust가 “실제로는 메모리 안전하지 않다”거나 “도입할 가치가 없다”는 주장도 나옴
  • 하지만 메모리 안전성과 관련된 잠재 취약점을 두 생태계가 다루는 방식에는 큰 차이가 있음

Rust에서도 취약점은 가능함

  • Rust 프로그램도 UB와 메모리 안전성 버그를 일으킬 수 있음
  • 대부분의 경우 이런 문제에는 unsafe 키워드가 필요함
  • Rust 프로그램이 UB를 전혀 겪을 수 없다는 주장은 틀림
  • 메모리 안전성과 무관한 일반 취약점도 Rust에서 가능함
    • 관리자 대시보드 접근 권한 검사를 빠뜨리는 문제는 어떤 언어에서도 생길 수 있음

C 라이브러리 예시: curl_getenv(NULL)

  • curl은 널리 쓰이고 잘 관리되는 C 기반 네트워킹 라이브러리임
  • libcurl의 curl_getenv는 여러 운영체제에서 환경 변수 값을 가져오는 이식성 있는 추상화 함수임
  • 다음 C 프로그램은 curl_getenv에 NULL 포인터를 넘김
#include <curl/curl.h> int main(void) { curl_getenv(NULL); }
  • 이 프로그램은 gcc test.c -otest -lcurl -Wall -Wextra로 경고 없이 컴파일될 수 있음
  • 실행하면 세그폴트가 발생할 수 있고, 이는 메모리 안전성 버그이자 잠재 취약점으로 볼 수 있음
  • 하지만 이런 예시는 보통 curl의 취약점으로 보고할 사안이 아님

C/C++에서는 오용 가능성만으로 CVE를 만들지 않음

  • curl_getenv(NULL)처럼 문제가 생기는 경우는 일반적으로 API의 잘못된 사용으로 간주됨
  • 결함의 위치도 라이브러리나 API가 아니라 애플리케이션 코드 쪽으로 봄
  • 이런 관행에는 두 가지 이유가 있음
    • C의 제한적인 타입 시스템으로는 API의 계약, 불변식, 사전조건, 사후조건을 정밀하게 표현하기 어려움
    • 가능한 모든 잘못된 사용을 문서화하는 것도 실용적이지 않음
  • 실제로 curl_getenv 문서는 NULL 호출이 금지되며 세그폴트로 이어질 수 있다고 말하지 않음
  • C/C++에서는 UB를 우연히 유발하기가 매우 쉬워, 모든 잠재적 취약 가능성을 CVE로 보고하면 대부분의 라이브러리가 막대한 수의 CVE에 휩쓸릴 수 있음
  • 따라서 C/C++에서는 보통 “오용 가능한 API의 존재”가 아니라 특정 오용 사례를 중심으로 CVE를 만듦

Rust에서는 안전한 API의 책임 경계가 다름

  • Rust에서 hyper::foo(None) 같은 안전한 호출만으로 프로그램이 세그폴트난다고 가정하면, 이는 hyper의 CVE가 될 수 있음
  • 사용자 프로그램에 unsafe 블록이 없는데 메모리 버그가 발생했다면, 해당 라이브러리에 soundness bug가 있어야 하기 때문임
  • Rust에서는 안전한 라이브러리 API를 어떤 방식으로든 사용했을 때 메모리 버그가 날 수 있다면, 이를 사용자 코드가 아니라 라이브러리 버그로 봄
  • 이런 API는 unsound하거나 soundness hole이 있다고 말함
  • 실제 프로그램에서 아직 문제가 발견되지 않았더라도, 안전한 API 사용만으로 메모리 버그를 일으킬 수 있으면 CVE가 만들어질 수 있음

safe와 unsafe가 책임을 드러냄

  • Rust에서는 “이 함수를 메모리 안전성 관점에서 올바르게 쓰고 있는가”에 대한 답이 C/C++보다 명확함
    • 호출하는 함수가 unsafe로 표시되지 않았다면 안전하게 사용할 수 있어야 함
    • 호출하는 함수가 unsafe라면 호출 지점에 unsafe 블록이 필요하고, 코드 리뷰와 코드베이스에서 위험 지점이 분명해짐
  • 이 구분이 Rust의 메모리 안전성을 실무적으로 확장 가능하게 만드는 요소임
  • 사용자 코드가 unsafe를 쓰지 않고 컴파일러 버그도 없다면, 잠재적 메모리 안전성 원인을 사용자 코드 책임으로 보기 어려움
  • 라이브러리가 unsafe 인터페이스를 노출하지 않는다면, 사용자는 그 라이브러리를 메모리 버그를 일으키는 방식으로 사용할 수 없어야 함
  • 라이브러리가 내부적으로 unsafe를 사용하다 버그를 내더라도, 수정은 라이브러리 안에서 이루어지고 사용자는 다시 메모리 버그로부터 안전해짐

원시 CVE 수만으로는 메모리 안전성을 비교하기 어려움

  • 같은 논리를 C에 적용하면 curl_getenv도 curl의 CVE로 표시해야 하지만, C에는 Rust의 safe와 unsafe 같은 구분이 없음
  • 사실상 모든 C 코드는 암묵적으로 unsafe에 가깝기 때문에, Rust식 기준을 그대로 적용하기 어렵음
  • C/C++ 라이브러리 개발자가 안전하고 견고한 라이브러리를 만들더라도, 이를 사용하는 수많은 C 프로그램은 API를 잘못 다루는 방식으로 쉽게 메모리 안전성 문제를 만들 수 있음
  • 이 차이는 curl뿐 아니라 거의 모든 C/C++ 라이브러리와 두 언어의 표준 라이브러리에도 적용됨
  • Rust와 C/C++의 코드 라인당 CVE 수 같은 원시 숫자 비교는 메모리 안전성을 평가할 때 오해를 부를 수 있음
Read Entire Article