C++26의 안전 기능(오류 동작, 표준 라이브러리 하드닝, 계약)이 메모리 안전성 위기를 해결한다는 주장에 대해, 실제 사례와 데이터, 채택 현실을 근거로 한계를 짚는다.
A 인기 있는 컨퍼런스 발표가 화제가 되면서, C++26의 새로운 안전 기능—오류 동작(erroneous behavior), 표준 라이브러리 하드닝, 계약(contracts)—이 수십 년간의 메모리 안전성 비판에 대한 해답이라고 치켜세우고 있다. 청중은 박수치고, 커뮤니티는 공유하며, 모두가 C++가 상황을 처리하고 있다는 안도감을 느낀다.
그렇지 않다. 그리고 실제 근거를 주의 깊게 보면, 그 발표는 불안정한 도입부, 컴파일 타임 평가에 대한 과장된 주장, 그리고 “옵트인 안전”이 대규모에서 실제로 무엇을 제공하는지에 대한 근본적 오해 위에 세워져 있음을 알 수 있다. 개별 기능들은 분명 개선이다. 그것들이 메모리 안전성 위기에 대한 충분한 대응을 이룬다는 프레이밍은 그렇지 않다.
발표는 2024년 7월의 CrowdStrike 사건으로 시작한다. Falcon 센서의 결함 있는 업데이트로 인해 약 850만 대의 Windows 머신이 벽돌이 된 사건이다. 발표자는 이를 수사적 질문으로 던진다. “부주의였나? 탐욕이었나? 아니면 잘못된 구현 언어를 고르는 것 같은 나쁜 엔지니어링이었나? 누가 알겠나?”
하지만 우리는 알고 있다. CrowdStrike가 2024년 8월 6일 공개한 자체 Root Cause Analysis는 근본 원인이 Falcon 센서의 Content Interpreter 구성요소에서 발생한 아웃오브바운즈(out-of-bounds) 메모리 읽기였다고 확인했다. Channel File 291에 대한 Rapid Response Content 업데이트는 21개의 입력 필드를 제공했지만, Content Interpreter는 20개만을 기대했다. 21번째 필드는 범위를 벗어난 인덱스로 접근되어 잘못된 값을 읽었고, 그 값은 포인터로 역참조되었다. 그 결과 커널 모드 드라이버(csagent.sys)에서 처리되지 않은 예외가 발생하며 즉시 BSOD가 났다. 필드 개수 불일치를 잡아야 했던 CrowdStrike의 Content Validator에는 버그가 있어, 잘못 형성된 템플릿이 통과해버렸다. 이는 교과서적인 메모리 불안전성이다—경계 위반이며, 발표자가 같은 발표 후반에 인용하는 CWE Top 25에서 1번 자리에 있는 정확히 그 범주다.
그렇다면 왜 연극적인 “누가 알겠나?”일까? 발표자는 강하고 정직한 논지를 펼칠 기회가 있었다. “현실 세계의 재앙이 아웃오브바운즈 읽기 때문에 발생했다. C++26이 바로 이 종류의 버그를 막기 위해 무엇을 제공하는지 보자.” 대신 청중은 수사적 손짓만 받는다. 아이러니는 더 깊다. CrowdStrike 버그는 std::span에 라이브러리 하드닝이 켜져 있었다면 잡혔을 결함에 가깝다—CrowdStrike가 그걸 사용하고 있었다면. 하지만 발표자는 이 연결을 명시적으로 만들지 않아, 도입부 전체가 논증이라기보다 장식처럼 느껴지게 된다.
그리고 발표가 완전히 놓친 미묘한 점이 있다. std::span과 라이브러리 하드닝이 있더라도, Falcon 센서의 Content Interpreter는 경계 위반에서 트랩을 걸고 종료됐을 것이다. 여전히 머신은 부팅하지 못했을 것이다. “아웃오브바운즈 읽기로 인한 크래시”와 “경계 검사로 인한 통제된 트랩”의 차이는 보안에는 중요하다—공격자가 잘못된 메모리를 악용하지 못하니까—하지만 신뢰성 관점에서는 850만 대가 여전히 다운된다. CrowdStrike에서의 진짜 교훈은 프로그래밍 언어가 전혀 아니었다. 스테이지드 롤아웃도, 카나리 배포도, 폴백 메커니즘도 없이, 테스트되지 않은 구성 업데이트를 전체 플릿에 동시에 배포한 것이었다. Delta Airlines가 CrowdStrike를 상대로 소송을 제기한 이유도 바로 이것—프로그래밍 언어 선택이 아니라 배포에서의 중대한 과실이었다.
안전은(발표자가 올바르게 말하듯) 시스템 속성이다. 그런데 발표 전체는 그 뒤로 언어 수준 기능만을 논한다.
발표는 널리 알려진 통계를 인용한다. 보안 취약점의 약 70%가 C와 C++의 메모리 안전성 문제에서 나온다는 것. 여러 정부가 이 수치를 되풀이하는 문서를 발표했다. 발표자는 이를 C++26 안전 작업의 동기로 사용한다. 숫자가 실제로 어디서 왔는지 추적하기 전까지는 그럴듯해 보인다.
70% 수치는 Microsoft의 Windows CVE 분석(2019년 공개)과 Google의 Chromium 및 Android 버그 분석에서 비롯되었다. 둘 다 수백만 라인의 레거시 C/C++ 코드가 수십 년간 누적된 코드베이스를 대상으로 한다. Microsoft의 Matt Miller는 BlueHat IL 2019에서, 모든 Microsoft 제품 전반에 할당된 CVE를 분석하며 이 데이터를 발표했다. Google의 Project Zero와 Chrome 보안 팀도 자사 코드베이스에 대해 유사한 분석을 공개했다.
이들은 규모가 거대하고 레거시 비중이 높아, 코드 상당수가 현대 C++ 관행 이전의 것이다. raw new/delete, C 스타일 배열, char* 문자열 조작, 수동 버퍼 관리—C++11 이전 안티패턴의 전체 목록이 그대로 있다. 그리고 혼동이 있다. 연구들은 “C/C++”을 하나의 범주로 보고 보고한다. Windows 커널은 대부분 C이지 C++가 아니다. Android의 HAL과 네이티브 레이어도 C 비중이 크다. RAII, 스마트 포인터, 컨테이너를 쓰는 현대 C++는 malloc/free 중심의 C 코드와 근본적으로 다른데도, 통계는 이를 하나로 묶어버린다.
발표가 언급하지 않는 것이 있다. 2024년 9월의 Google 자체 데이터는 Android에서 메모리 안전성 취약점 비율이 6년 사이 76%에서 24%로 떨어졌음을 보여준다—기존 C++ 코드에 안전 기능을 덧대서가 아니라, 새로운 코드를 메모리 안전 언어(Rust, Kotlin, Java)로 작성함으로써. Google의 보안 블로그는 흥미로운 관찰을 한다. 취약점에는 반감기가 있다. 5년 된 코드는 시간이 지나며 버그가 발견되고 수정되기 때문에, 신규 코드보다 취약점 밀도가 3.4배에서 7.4배 낮다. 시사점은 강렬하다. 새로 불안전한 코드를 쓰는 것을 멈추기만 해도, 기존 C++를 한 줄도 건드리지 않고 전체 취약점 비율이 지수적으로 떨어진다는 뜻이다.
이는 “C++는 불안전하다”는 포괄적 서사에 대한 공정한 반박이며, 발표가 이 점을 말할 수도 있었다. 하지만 그러지 않았다. 발표자는 70% 수치를 그대로 제시하면서, 그 상당 부분이 C 코드나 현대 이전 C++ 관행에서 온 것임을 밝히지 않고, Google이 발견한 해법이 “옵트인 체크로 낡은 C++를 개조”가 아니라 “새 코드를 안전 언어로 작성”이었다는 사실도 인정하지 않는다.
C++ 위원회의 접근—언어에 옵트인 안전 기능을 추가하는 것—은 Google에서 효과가 있었던 것과 정반대다. Google의 전략은 안전이 기본이어서 안전 도구가 필요 없는 언어로 새 코드를 작성하라는 것이다. 위원회의 전략은 새 코드를 계속 C++로 쓰되, 이 새 안전 도구들을 사용하라는 것이다. Google은 접근이 효과가 있음을 보여주는 경험적 데이터를 갖고 있다. 위원회는 제안서들을 갖고 있다.
발표자는 상수 평가에 대해 가장 대담한 주장 중 하나를 한다. 컴파일러에 내장된 constexpr 인터프리터가 “모든 새니타이저보다 더 낫다”고—컴파일 타임에 항상 모든 미정의 동작(undefined behavior)을 잡아내기 때문이라고.
이는 기술적으로는 맞지만, 대다수 프로덕션 코드에는 عملي적으로 쓸모가 거의 없다. constexpr 인터프리터는 바깥 세계에 닿지 않는 코드만 평가할 수 있다—I/O 없음, 시스템 콜 없음, 네트워킹 없음, 파일 작업 없음, 런타임 입력에 의존하는 동적 메모리 패턴 없음, 멀티스레딩 없음. 발표자는 이 중 두 가지 제한을 잠깐 언급한다(“컴파일 타임 퍼징은 못 한다”, “컴파일 타임 멀티스레딩은 못 한다”)—하지만 이를 사소한 남은 공백처럼 프레이밍한다.
사소하지 않다. 다수다. 전형적인 C++ 애플리케이션이 실제로 하는 일을 생각해보라. 파일, 소켓, 하드웨어 레지스터에서 입력을 읽는다. 상태 머신을 통해 입력을 처리한다. 입력에 따라 동적으로 메모리를 할당한다. 결과를 다른 시스템과 통신한다. 스레드 간 작업을 조율한다. 이 모든 활동—버그가 실제로 살고 있고 공격자가 실제로 찌르는 지점—은 constexpr의 범위를 벗어난다.
CrowdStrike의 아웃오브바운즈 읽기는 런타임 구성 파일을 파싱하는 중에 일어났다. constexpr는 여기에 도움이 될 수 없다. Heartbleed는 런타임에 수신된 잘못 형성된 TLS heartbeat 메시지로 촉발된 버퍼 오버리드였다. constexpr는 이것도 도울 수 없다. 보안 취약점의 주된 공격 표면인 “신뢰할 수 없는 입력 처리” 전체 범주는 본질적으로 런타임 동작이다.
컴파일 타임 단위 테스트는 훌륭하다. 나도 쓴다. 실제 버그를 잡는다. 하지만 constexpr 평가를, 실제로 사고를 일으키는 취약점 종류에 대한 주된 방어 수단처럼 제시하는 것은, 그 도구가 원래 설계되지 않은 일을 하도록 과장 판매하는 것이다.
C++26의 표준 라이브러리 하드닝은 진정으로 유용하다. std::span, std::vector, std::string, std::array를 범위 밖에서 접근하면, 하드닝된 구현은 쓰레기 메모리를 읽는 대신 즉시 트랩을 건다. 이는 실제이고 측정 가능한 개선이다.
하지만 libc++의 자체 문서가 현재 상태에 대해 뭐라고 말하는지 보라. 기본 하드닝 모드는none이다. 사용자가 옵트인해야 한다. 프로덕션에 적합한 “fast” 모드는 두 가지 assertion 범주만 검사한다: valid-element-access와 valid-input-range. 이터레이터 경계 검사는 ABI 변경이 필요해 대부분의 벤더가 활성화하지 않았다. unordered 컨테이너(unordered_map, unordered_set 등)는 부분적으로만 하드닝됐다. vector<bool> 이터레이터는 전혀 하드닝되지 않는다. 그리고 이터레이터 무효화 검사—vector가 재할당된 뒤에도 이터레이터로 요소에 접근하는 것—는 하드닝을 켠 상태에서도 여전히 미정의 동작으로 이어진다.
또 다른 문제는 커버리지다. 전형적인 성능 민감 C++ 코드베이스가 실제로 std:: 컨테이너를 얼마나 쓰는가? HFT, 게임 엔진, 임베디드 시스템, 커널 모듈—C++를 선택하는 이유가 “제어가 필요해서”인 도메인에서는, 팀들이 커스텀 컨테이너, 커스텀 할당자, 링 버퍼, 락-프리 자료구조, 메모리 매핑 영역, 직접 포인터 산술을 일상적으로 사용한다. 라이브러리 하드닝은 그 어느 것도 커버하지 못한다. 또한 호출하는 C API도 전혀 커버하지 못한다: POSIX, 커널 인터페이스, 하드웨어 추상화 레이어, C 링키지를 가진 서드파티 라이브러리.
발표의 데모는 이를 잘 보여준다. 하드코딩된 std::span의 단순한 아웃오브바운즈 접근을 하는 사소한 프로그램. 물론 하드닝이 잡는다. 하지만 이건 상상 가능한 가장 쉬운 경우다. 실거래 시스템에서 풀 할당된 객체를 가리키는 raw 포인터를 통해 발생한 use-after-free를 하드닝이 잡는 모습을 보여달라. 못 한다. 그건 범위 밖이기 때문이다.
발표자는 P2900, C++ 계약 제안에 눈에 띄게 열광한다. 전제조건(preconditions), 사후조건(postconditions), 계약 어서션—프로그램의 특정 지점에서 무엇이 참이어야 하는지를 표현하는 개발자 주석이다. 발표자는 기존 assert() 매크로가 디버깅 전용이며 릴리스 빌드에서 컴파일로 제거된다는 점을 올바르게 지적한다. 계약은 완전한 최적화와 함께 동작함으로써 이를 해결한다.
좋다. 하지만 발표가 다루지 않는 구조적 문제가 있다. 계약은 개발자가 정확하고 완전한 주석을 작성한다는 것에 전적으로 의존한다. 이것이야말로 40년 동안 C++ 안전성의 중심 실패 모드였던 “규율 있는 프로그래머” 가정이다. 우리는 개발자들에게 const를 줬다. 늘 쓰지 않는다. 스마트 포인터를 줬다. 여전히 new를 쓴다. std::array를 줬다. 여전히 C 배열을 쓴다. C++ 역사에서 모든 옵트인 안전 기능은, 채택이 규율에 달려 있고 규율은 팀 간, 의존성 간, 수십 년 유지보수에 걸쳐 확장되지 않기 때문에, 채택이 불완전했다.
계약의 평가 의미론은 이를 더 악화시킨다. ignore(아예 검사하지 않음), observe(위반을 로그하고 계속), quick_enforce(즉시 트랩), enforce(로그 후 종료)를 선택할 수 있다. observe 의미론은 레거시 코드용으로 명시적으로 설계됐다—발표자는 “지난 10년간 잘 동작했는데 왜 지금은 안 되겠는가?”라고 말한다. 이는 실용적 타협이지만, 동시에 프로덕션에서 계약 위반을 다루고 싶지 않은 모든 팀이 빠져나갈 수 있는 탈출구를 만들어준다.
이를 Ada/SPARK의 계약 처리와 비교해보라. SPARK에서는 계약이 SMT 솔버(CVC4/Z3)를 쓰는 형식적 증명 엔진에 의해 정적으로 검증된다. 툴체인은 컴파일 타임에 모든 호출자가 전제조건을 항상 만족함을 증명한다. 증명할 수 없으면 코드는 리뷰를 통과하지 못한다. “observe하고 계속”은 없다—증명을 고치거나, 출하하지 않는다. C++ 계약은 선택적 강제력을 가진 런타임 체크다. SPARK 계약은 필수 만족을 요구하는 컴파일 타임 증명이다. 같은 범주의 도구가 아니다.
그리고 더 나빠진다. P2900은 클래스 불변식(class invariants)조차 지원하지 않는다—“미래 제안으로 미룬다.” Eiffel은 1988년에 클래스 불변식을 가졌다. D 언어는 2001년부터 in/out/invariant 계약을 제공해왔다—C++26보다 25년 앞선 것이다. Safe C++ 제안(P3390)의 저자 Sean Baxter가 말했듯: “계약은 버그를 검사한다. 버그를 예방하지는 않는다. 트랩하는 경계 검사 접근은 여전히 서비스 거부다.” Baxter는 Rust에서 영감을 받은 실제 borrow checker를 C++에 제안했지만 채택되지 않았다.
발표는 초기화되지 않은 변수 읽기에 대해, C++23에서 미정의 동작에서 오류 동작으로 바뀐 것을 큰 승리로 제시한다. 그리고 좁은 의미에서는 그렇다. 컴파일러는 더 이상 초기화되지 않은 읽기를 이용해 공격적으로 최적화할 수 없다. “프로그래머는 초기화되지 않은 변수를 읽지 않을 테니, 세 줄 뒤의 널 체크를 지워도 된다” 같은 가정을 할 수 없다. 이는 वास्तविक 개선이다.
하지만 발표자의 프레이밍—“현대 컴파일러로 이 코드를 재컴파일하기만 하면 암묵적 초기화 동작을 얻는다”—는 오해를 부른다. 오류 동작은 프로그램이 정의되어 있지만 잘못된 동작을 한다는 뜻이다. 변수는 여전히 불확정 값을 가진다. 여전히 쓰레기를 읽는다. 프로그램은 여전히 잘못된 결과를 낸다. 달라진 점은 컴파일러가 당신의 버그 코드를, 완전히 예측 불가능한 것으로 변형하지 않고 충실히 컴파일해야 한다는 것뿐이다.
이를 Rust, Go, Swift, 심지어 Java와 비교해보라. 변수는 선언 시 알려진 값으로 초기화되거나, 아니면 컴파일되지 않는다. 끝이다. 구조적으로 오류가 예방되기 때문에 “오류 동작” 범주가 없다. C++23에서는 여전히 int x; return x;를 쓸 수 있고, 컴파일되고 실행되어 쓰레기를 반환한다. 이제 그 쓰레기가 더 예측 가능해졌을 뿐이다.
발표자는 해결책으로 값 초기화(int x{};)를 권한다. 맞다. 하지만 이것은 C++11부터 가능한 해결책이었다—14년 전이다. 커뮤니티가 2011년에 값 초기화를 보편적으로 채택했다면, 2023년에 오류 동작 같은 완화책이 필요 없었을 것이다. 10년 넘게 있던 기능을 개발자들이 쓰지 않아 생긴 결과를 완화하기 위해 언어 변경이 필요했다는 사실 자체가, 옵트인 접근에 대한 반증이다.
발표자는 프로파일—안전한 동작 범주를 강제할 안전 프로파일—을 언급하며 “C++이 가질 수 있는 최고의 것”이라고 부른다. 그리고 곧바로 그것들이 빠르면 C++29로 밀렸다고 말한다. 이것이 발표 전체에서 가장 드러내는 순간이다.
현실적인 타임라인은 이렇고, 역사는 낙관적이지 않다. C++20은 2020년에 비준됐고, 모듈은 5년이 지난 지금도 프로덕션에서 완전히 사용 가능하지 않다. C++23은 2025년 초 기준으로 채택이 제한적이었다. C++26은 2026년에 비준된다. 주요 컴파일러가 합리적 준수를 달성하는 것은 2027-2028년이다. 2025년 초 기준으로, 주요 컴파일러(GCC, Clang, MSVC) 중 어느 것도 프로덕션 준비가 된 P2900 계약 지원을 제공하지 않는다. 대형 코드베이스가 C++26 기능을 채택하기 시작하는 시점은 2029-2030년이다. C++29의 프로파일은 2029년까지 비준되지 못하며, 컴파일러 지원은 2030-2031년, 실제 채택은 2032-2033년이 된다. 그리고 프로파일에는 동작하는 구현조차 없다—위원회 내부의 비판자들조차 실현 가능성을 의문시해왔다.
미국 정부의 메모리 안전 언어에 대한 가이던스는 2022년 11월 NSA에서 나왔고, 2023-2024년에는 국제 기관들과 협력한 CISA를 통해 이어졌으며, 백악관 ONCD는 2024년 2월 “Back to the Building Blocks”를 공개했다. 이 기관들은 소프트웨어 공급자들에게 메모리 안전 로드맵을 지금 제시하라고 요구하고 있다. C++ 프로파일을 2033년까지 기다려주지 않는다.
그 사이 Rust는 2015년에 1.0에 도달했다. 10년간 프로덕션 준비가 되어 있었다. Linux 커널은 2022년부터 Rust 코드를 받아들였다. Android의 Rust 채택은 2019년 즈음 시작됐다. Google의 데이터는 결과를 보여준다: Android에서 메모리 안전성 취약점이 6년 사이 76%에서 24%로 떨어졌다. C++에 옵트인 기능을 추가해서가 아니다. 기본적으로 안전한 언어로 새 코드를 작성함으로써다.
C++ 위원회의 접근이 틀렸다는 말은 아니다—이 기능들은 실제로 도움이 된다. 하지만 표준화와 채택의 속도를 고려할 때, 이를 메모리 안전성 위기에 대한 충분한 대응으로 포지셔닝하는 것은 신뢰를 무리하게 시험한다. 2032년에 의미 있는 안전 개선을 제공하는 전략은, 2015년부터 그것을 제공해온 전략과 경쟁하고 있다.
발표자는 분명 지식이 깊고 선의다. 소개된 기능들은 واقعی 개선이다. 하지만 2025년에 C++ 안전에 대해 정직하게 말하는 발표라면, 청중이 듣지 못한 몇 가지를 인정했어야 한다.
옵트인 안전 기능은 C++에서 40년간 불완전 채택의 실적을 갖고 있으며, 계약과 하드닝도 C++ 교육, 도구, 강제 방식이 근본적으로 바뀌지 않는 한 같은 패턴을 따를 것임을 인정해야 한다.
메모리 안전에 대해 우리가 가진 가장 설득력 있는 현실 세계의 증거인 Google의 Android 데이터를 제시하고, 왜 Google의 해법이 “기존 C++에 계약을 추가”가 아니라 “새 코드를 Rust로 작성”이었는지 정직하게 논의해야 한다.
(70% 통계가 설명하는) 기존 취약점의 재고(stock)와, (오늘 어떤 언어로 새 코드를 쓰는지에 달린) 신규 취약점의 흐름(flow)을 구분해야 한다.
CrowdStrike에서 가장 중요한 안전/보안 교훈은 언어가 아니라 배포 프로세스였음을 인정해야 한다—그리고 Falcon 센서가 완벽히 메모리 안전했더라도, CrowdStrike가 테스트 없이 잘못 형성된 구성을 밀어넣었다면 850만 대는 여전히 크래시했을 것임을 인정해야 한다.
그리고 타임라인에 대해 정직해야 한다. C++는 다른 언어들이 구조적으로 해결한 문제를, 실제이지만 점진적인 진전으로 따라잡고 있다. 그것은 دفاع 가능한 입장이다. “C++가 처리하고 있다”는 것은 아니다.
C++ 커뮤니티는 안심시키는 말보다 더 나은 것을 받을 자격이 있다. 이 도구들이 어디에서 도움이 되고, 어디에서는 도움이 되지 않으며, 현실적인 대안이 무엇인지에 대한 냉정한 평가가 필요하다. 왜냐하면 지금 언어 선택 결정을 내리는 조직들—NSA 권고와 CISA 가이던스를 읽는 조직들—은 C++29 프로파일을 기다리지 않을 것이기 때문이다. 그들은 오늘 선택한다. 그리고 “우리에겐 계획이 있다”는 “우리에겐 해결책이 있다”와 다르다.