CVE의 70%는 메모리 안전성 버그다. buffer overflow, use-after-free, double free. 공격자가 변형된 JPEG에서 루트 접근권까지 확대할 수 있는 취약점이다.

1975년부터 하드웨어에서 이것들의 대부분을 막는 방법을 알고 있었다. 케임브리지 CAP 컴퓨터는 캐퍼빌리티 기반 주소 지정을 사용했다. 카네기 멜론의 하이드라(Hydra)도 마찬가지였다. 아이디어는 단순했다. 모든 memory access은 자신만의 허가증을 지닌다. 하드웨어가 이를 강제한다. 허가증이 없으면 접근할 수 없다.

그리고 캐퍼빌리티 하드웨어는 다음 40년 동안 평면 메모리 모델에 패배하며 보냈다. ARM의 Morello 보드와 CHERI 명령어 집합이 등장한 지금에서야 캐퍼빌리티 아키텍처가 믿을 만한 재기를 하고 있다.

문제는 캐퍼빌리티가 작동하는지가 아니다. 작동한다. 문제는 업계가 왜 40년이나 걸려서야 관심을 갖게 되었는가이다.

캐퍼빌리티 포인터는 단순한 주소가 아닌 위조 불가능한 티켓이다

일반 포인터는 정수다. 주소다. 길이도 없고, 권한도 없고, 출처도 없다. 주소를 맞히면 접근할 수 있다. 그것은 기능이 아니다. C가 PDP-11에서 물려받은 설계상 결함이다.

반면 캐퍼빌리티 포인터는 하드웨어에 의해 강제되는 튜플이다. 가상 주소, 허가된 영역의 베이스와 길이, 읽기/쓰기/실행 권한, 그리고 1비트 유효성 태그를 포함한다. 태그는 일반 메모리 바깥, 전용 하드웨어 메타데이터에 저장되므로 캐퍼빌리티는 위조할 수 없다. 정수로부터 유효한 캐퍼빌리티를 구성할 수 없다. 이미 권한을 가진 무언가로부터만 받을 수 있다.

CHERI에서는 64비트 머신에서 이것이 128비트 포인터처럼 보인다. 추가 64비트가 경계와 권한을 담는다. 태그 비트는 구현에 따라 섀도우 테이블이나 여분의 ECC 비트에 저장된다.

다음은 CHERI 시스템에서 캐퍼빌리티를 축소하는 실제 모습이다:

#include <cheriintrin.h>
#include <stdio.h>

int main(void) {
    char buffer[64];

    // In the purecap ABI, &buffer[0] is already a capability
    // carrying 64-byte bounds and read/write permissions.
    char *cap = buffer;

    printf("Original length: %zu\n", cheri_length_get(cap));

    // Narrow authority to the first 16 bytes.
    char *narrow = cheri_bounds_set(cap, 16);
    printf("Narrowed length: %zu\n", cheri_length_get(narrow));

    // This access is safe and hardware-authorized.
    narrow[15] = 'x';

    // This would trap with a CHERI bounds violation:
    // narrow[20] = 'y';

    return 0;
}

공격자가 캐퍼빌리티 포인터를 손상시키면 태그 비트가 지워진다. 하드웨어는 다음 역참조 시 예외를 발생시킨다. 익스플로잇 체인은 첫 번째 홉에서 멈춘다.

이것은 ASLR이나 스택 카나리와 같은 속도 저하 장치보다 근본적으로 더 나은 보안 모델이다.

iAPX 432가 한 세대의 우물을 독살했다

그렇다면 우리는 왜 40년 동안 포인터를 단순한 정수인 척했던 것일까.

인텔은 1981년에 iAPX 432를 출하했다. 하드웨어 캐퍼빌리티, 객체 지향 메모리 세그먼트, 심지어 하드웨어 지원 가비지 컬렉션까지 갖추고 있었다. 동시에 8086보다 대략 5배에서 10배 느렸고, 엄청나게 복잡했으며, 기존의 모든 컴파일러와 비호환적이었다.

IBM은 System/38에서 더 나은 운을 거머쥐었다. 이 시스템은 싱글 레벨 스토어에 캐퍼빌리티를 사용했다. 작동했다. 안정적이었다. 하지만 이것은 또한 형성되고 있던 개방형 생태계로의 경로가 없는 독점적인 중규모 시스템이었다. UNIX 혁명이 도래했을 때, 이는 평면 주소 공간, C 포인터, 그리고 포인터가 레지스터에 들어간다는 가정을 가져왔다. 그 가정은 전 산업의 ABI가 되었다.

1990년대가 되자 캐퍼빌리티 하드웨어는 연구적 기이함에 불과했다. 상업적 필연은 역호환성과 순수한 성능이었다. 메모리 안전성은, 만약 그것이 관심사였다면, 언어 수준의 문제였다. C와 C++은 단순히 이것을 프로그래머의 문제라고 선언했다.

인터넷이 모든 buffer overflow을 원격에서 악용 가능하게 만들기 전까지는 이것이 충분히 잘 작동했다.

CHERI의 하이브리드 모드가 호환성 deadlock을 깬다

현대의 캐퍼빌리티 하드웨어, 특히 CHERI는 iAPX 432의 교훈을 배웠다. 전부 아니면 전무인 이주를 강요하지 않는다.

CHERI는 레거시 정수 포인터와 캐퍼빌리티 포인터가 동일한 프로세스 내에서 공존하는 하이브리드 ABI를 지원한다. 네트워크 파서나 샌드박스화된 라이브러리만 캐퍼빌리티로 컴파일하고, 나머지 애플리케이션은 일반 포인터를 사용할 수 있다. 운영체제(CheriBSD 또는 CHERI Linux 포트)가 전환을 관리한다.

이것이 중요한 이유는 진정한 장벽이 실리콘 면적이 결코 아니었기 때문이다. 소프트웨어의 관성이었다. sizeof(void *) == 8을 가정하는 수십억 줄의 C와 C++ 코드가 있다. 이를 모두 다시 쓰는 것을 요구하는 캐퍼빌리티 아키텍처는 도착하자마자 사망한다. 점진적으로 도입될 수 있는 것에야 비로소 기회가 있다.

하드웨어 메커니즘은 우아하다. 캐퍼빌리티 인식 로드와 스토어는 주소 변환과 병렬로 태그와 경계를 검사하는 전용 명령어를 사용한다. 정수 포인터는 검사를 완전히 우회한다. 안전성의 대가는 사용하는 곳에서만 치른다.

진정한 비용은 TLB 압력과 포인터 비대화이지, 사이클 수가 아니다

캐퍼빌리티는 공짜가 아니다. 오버헤드는 세 가지 범주로 나뉜다.

첫째, 포인터 크기. purecap ABI에서 모든 포인터는 128비트다. 이는 포인터 중심 데이터 구조에 대한 캐시 압력을 두 배로 만든다. 연결 리스트, 트리, 가상 함수 테이블이 모두 커진다.

둘째, 경계 세분성. CHERI는 바이트 세분성으로 경계를 강제하므로, 메모리 서브시스템은 모든 역참조 시 경계를 검사해야 한다. 검사 자체는 빠르지만, 많은 작은 보호 영역을 생성하면 미세한 캐퍼빌리티가 TLB와 캐시 압력을 증가시킬 수 있다.

셋째, 그리고 가장 중요한 것은 소프트웨어 변경이다. 컴파일러는 캐퍼빌리티 인식 프롤로그를 생성해야 한다. ABI가 바뀐다. 메모리 할당기는 원시 포인터가 아닌 경계가 제한된 캐퍼빌리티를 반환해야 한다. 디버거는 128비트 레지스터를 이해해야 한다.

CHERI 하드웨어에서 측정된 런타임 오버헤드는 대부분의 워크로드에서 일반적으로 한 자리수 퍼센트 범위이며, 포인터 중심 벤치마크에서는 10~15%에 달한다. 이는 메모리 태깅이나 샌드박싱과 같은 많은 현대적 완화책의 오버헤드보다 적으며, 더 강력한 보장을 제공한다.

트레이드오프는 성능이 아니다. 트레이드오프는 생태계의 격변이다.

오늘 개발 보드에서 실행할 수 있다

캐퍼빌리티 하드웨어를 시험해보고 싶다면 타임머신이나 연구 보조금이 필요 없다.

ARM의 Morello 보드는 Neoverse N1 코어에서 CHERI 확장을 구현한다. CheriBSD는 바로 실행된다. 하드웨어가 없다면 QEMU-CHERI가 전체 캐퍼빌리티 아키텍처를 에뮬레이트한다.

툴체인은 표준 LLVM이다. cheri-clang-march=morello+cheri 플래그로 컴파일한다. FreeBSD와 Linux 포트가 존재한다. LLVM 통합 어셈블러, 링커, 디버거는 모두 캐퍼빌리티를 이해한다.

단일 함수부터 시작하라. 파서나 역직렬화 루틴을 축소된 캐퍼빌리티로 감싸라. 퍼저를 돌려보아라. 경계 외 쓰기가 힙을 손상시켰을 것이라면, 대신 깔끔한 하드웨어 예외를 얻게 될 것이다.

캐퍼빌리티 하드웨어는 메모리 안전성에 대한 이론적 수정책이 아니다. 인센티브가 잘못되어 업계가 40년간 무시해 온, 작동하고 출하되는 기술이다. 평면 메모리는 더 빠르게 구축되고, 더 쉽게 포팅되며, 원격 악용이 기본 위협 모델이 되기 전까지 충분히 좋았다.

실리콘은 작동한다. 컴파일러는 작동한다. 운영체제는 작동한다. 남은 것은 코드베이스의 어느 부분을 먼저 보호할지 결정하는 것뿐이다.