메뉴 여닫기
환경 설정 메뉴 여닫기
개인 메뉴 여닫기
로그인하지 않음
지금 편집한다면 당신의 IP 주소가 공개될 수 있습니다.

CMASan: Custom Memory Allocator-aware Address Sanitizer

noriwiki
Noribot (토론 | 기여)님의 2026년 10월 17일 (토) 08:08 판 (Nori: update draft.wikitext)
(차이) ← 이전 판 | 최신판 (차이) | 다음 판 → (차이)
CMASan: Custom Memory Allocator-aware Address Sanitizer
AuthorJunwha Hong, Wonil Jang, Mijung Kim, Lei Yu, Yonghwi Kwon, Yuseok Jeon
ConferenceIEEE Symposium on Security and Privacy (S&P)
Year2025



개요

이 논문은 Custom memory allocator(CMA)가 할당한 객체에서 AddressSanitizer(ASan)가 왜 메모리 버그를 놓치며, CMA를 표준 allocator로 교체하지 않고 객체의 경계와 수명을 추적하여 그 공백을 어떻게 줄일 수 있는지를 다룬다. CMASan은 CMA API의 반자동 식별, redzone 계측, 객체 메타데이터, instance별 quarantine 및 clear 처리를 결합한 보안·디버깅 도구이다.

Motivation

CMA는 cache 활용, 메모리 효율, 보안 등의 목적으로 사용된다. 논문이 GitHub star 순 상위 C/C++ 애플리케이션 100개를 조사한 결과, 44개에서 78개의 CMA를 발견했다. 문제는 ASan이 표준 allocator가 제공하는 객체 경계와 free를 추적하므로, 그 위에서 CMA가 정의한 더 작은 객체와 별도의 수명을 알지 못한다는 것이다.

  • Arena 패턴: 큰 chunk를 받아 작은 객체로 분할한다. ASan은 큰 chunk 외곽만 보호하므로 내부 객체 사이의 Buffer overflow를 놓칠 수 있다. 조사된 CMA의 69%가 이 패턴을 사용했다.
  • Recycler 패턴: 반환된 객체를 내부 free-list 등에 저장하고 재사용한다. 표준 free가 호출되지 않으면 ASan은 CMA 수준의 해제를 인식하지 못해 Use-after-free(UAF)와 Double free(DF)를 놓친다. 조사된 CMA의 45%가 이 패턴을 사용했으며 Arena와 중첩될 수 있다.

기존 Shim은 CMA를 표준 allocator로 대체하지만, 주소를 유지하는 in-place realloc이나 여러 객체를 한꺼번에 무효화하는 clear를 단순히 realloc/free에 대응시키기 어렵다. Manual poisoning API는 CMA를 유지할 수 있으나, 개발자가 객체 경계·수명에 맞추어 poisoning을 삽입하고 redzone 공간 및 재사용 지연을 구현해야 한다. 조사에서는 Shim을 사용하는 애플리케이션이 10개였지만 그중 5개는 일부 CMA만 지원했고, manual poisoning API를 사용하는 애플리케이션은 3개였다.

Importance

이 논문의 중요한 관점은 메모리 접근 검사 코드가 존재하는 것과 올바른 객체 경계·수명을 검사하는 것은 별개라는 점이다. ASan이 load/store를 검사하더라도 CMA 내부 객체의 경계와 해제가 shadow memory에 반영되지 않으면 오류를 발견할 수 없다.

CMASan은 기존 테스트 입력을 유지하면서 allocator에 대한 관측을 개선하는 접근이다. SQLite3와 PHP의 기존 테스트가 이미 실행하던 버그를 각각 9년, 2년 만에 발견했다는 결과는 입력 생성뿐 아니라 sanitizer의 객체 모델도 탐지 능력을 좌우한다는 근거가 된다. 또한 정확한 CMA 지원에는 alloc/free 외에 clear, size query, object collection 같은 보조 API의 의미가 필요함을 보여준다.

Main Idea

핵심 아이디어는 CMA의 내부 할당기를 유지하면서 CMA가 애플리케이션에 제공하는 객체 단위의 경계와 수명을 ASan에 연결하는 것이다. API를 식별한 뒤 요청 크기를 늘려 redzone 공간을 확보하고, 할당·해제에 맞추어 Shadow memory를 갱신하며, 표준 allocator의 메타데이터와 별개로 CMA 객체 정보를 관리한다.

Temporal bug는 해제를 인식하는 것만으로 충분하지 않다. 객체가 즉시 재사용되면 오래된 포인터의 접근도 정상 객체 접근처럼 보이므로, instance별로 실제 free 요청을 지연하여 poisoning이 유지되는 시간을 늘린다. clear에는 해당 instance의 할당 객체 집합을 이용한다. 이때 CMA 내부의 합법적인 메타데이터 접근과 중첩 호출은 별도로 처리하여 오탐을 줄인다.

Design

  1. CMA API 식별과 반자동 분류: CodeQL의 함수 이름·반환 포인터·size 인자 휴리스틱을 확장하고, size가 pointer arithmetic이나 내부 메타데이터 비교에 쓰이는지를 확인하여 후보를 추린다. 같은 source/header module의 함수를 family로 묶고, 규칙과 함수 본문을 사람이 검토하여 alloc/calloc/realloc/free/clear 및 보조 API, size·object·instance 인자를 분류한다. 평가에서 애플리케이션당 약 10분의 일회성 수작업이 필요했다. 소스의 수동 수정은 요구하지 않지만 API 인식 전체가 자동인 것은 아니다.
  2. 객체 경계와 수명 계측: alloc의 요청 크기에 redzone 크기를 추가하고 반환 객체 오른쪽을 poison한다. calloc/typed alloc은 개수와 granularity를 고려한다. 왼쪽에는 CMA 메타데이터가 있을 수 있어 별도의 redzone을 넣지 않으며, 인접 객체에서는 앞 객체의 오른쪽 redzone이 경계를 제공한다. free에서는 원래 객체 크기만큼 poison하고, 이미 FREED 상태이며 CMA ID가 같으면 DF를 보고한다. realloc은 반환 주소를 비교하여 제자리 resizing과 이동을 구분하고, 이동한 경우 이전 객체도 poison한다. redzone 확보를 위한 크기 증가는 CMA의 용량·최적화 판단에 영향을 줄 수 있다.
  3. Two-level 객체 메타데이터: 주소로 객체 시작점, size, redzone size, 할당 상태, CMA ID, alloc/free trace를 찾는다. CMA가 좁은 주소 범위를 반복 사용하는 특성을 이용하여 2차 테이블을 필요할 때 할당한다. 구현의 1차 테이블은 64MB, 각 2차 테이블은 128MB이다. arena가 반환되면 관련 메타데이터를 제거하고, 재할당이 기존 기록과 겹치면 새 객체 영역은 갱신하되 겹치지 않는 freed 영역의 기록은 남겨 오래된 객체 접근을 추적한다.
  4. Instance별 free-delaying quarantine: CMA free를 즉시 실행하지 않고 객체를 poison한 뒤 요청을 FIFO에 보관한다. 한도에 도달하면 오래된 요청을 해당 CMA에 전달한다. 서로 다른 CMA instance의 요청이 섞이지 않도록 분리하며, 기본 한도는 256KB와 8,192개 객체이다. 객체 재사용을 늦춰 UAF/DF 탐지 창을 늘리는 대신 메모리 사용량과 실제 해제 시점을 바꾼다. 내용 보존이 필요한 realloc에는 이 free 지연을 적용하지 않는다.
  5. Instance별 allocation zone과 clear 처리: 할당 객체를 instance별로 기록하고 clear 호출 시 해당 집합을 freed로 표시한다. 표준 free가 호출되지 않는 bulk reset도 추적할 수 있지만, 기본적으로 최근 10,000개 객체까지만 기록하므로 무제한 추적은 아니다.
  6. 네 가지 오탐 회피: CMA 내부 동작과 계측 사이의 충돌을 아래 방법으로 처리한다. 정확도 개선에는 검사 억제와 추가 runtime 비용이라는 대가가 있다.
Call stack-based suppression
오류 검출 시 stack에 CMA 관련 함수가 있으면 보고를 억제하여, freed 객체에 저장된 내부 메타데이터 접근 등을 허용한다. 공용 함수를 통째로 검사에서 제외하는 ignore list보다 실행 문맥을 구분하지만 CMA 내부 버그도 가릴 수 있다.
Call stack-based activation
중첩된 CMA API에서는 가장 바깥 호출의 계측만 활성화한다. realloc 내부 alloc/free를 중복 처리하여 redzone을 거듭 늘리거나 DF를 잘못 보고하는 문제를 막는다.
Size leakage avoidance
size query API가 redzone을 포함한 확장 크기를 노출하지 않도록 원래 애플리케이션이 요청한 크기를 반환한다.
Object-collecting API handling
GC 등 별도의 수거 API 호출 시 해당 quarantine을 비워, 수거된 객체를 나중에 다시 free하는 충돌을 피한다.

구현은 LLVM/ASan 15.0.6과 CodeQL 2.18.2를 기반으로 하며 약 3,000 LoC이다. CMA API가 ASan pass 이전에 inline되어 계측 지점이 사라지는 것을 막기 위해 Clang AST에서 inlining을 금지한다. Typed alloc의 granularity 추론에는 pointer type 정보가 필요하여 LLVM 15의 opaque pointer 기능을 끈 구성을 사용한다. Runtime에는 binary/library에서 CMA 함수 주소를 얻고 dlopen도 추적하여 call stack 판정에 사용한다.

Result

평가 범위와 적용 비용

성능·coverage·오탐 평가는 C와 C++ 각각 GitHub star 상위 30개 repository 중 Clang 15.0.6으로 빌드 가능하고 CMA 및 workload가 있는 12개를 대상으로 했다. 환경은 Intel Core i9-13900KF, RAM 128GB, RTX 4090, Ubuntu 22.04.1이며, ASan과 CMASan 모두 -O2, -fno-omit-frame-pointer, -fno-optimize-sibling-calls를 사용했다. API 분류에 드는 사람의 작업은 애플리케이션당 약 10분이었다(§5.1–5.2).

Table 1에서 ncnn의 CMA 객체 관련 load/store check는 전체의 87.15%, Redis는 42.93%, PHP는 30.02%였다. 이는 ASan이 검사 명령을 전혀 실행하지 않는다는 뜻이 아니라, 그 검사에 필요한 CMA 내부 객체 경계·수명 정보를 기본 ASan이 반영하지 못한다는 뜻으로 이해해야 한다.

비용

ASan 대비 CMASan의 추가 오버헤드(Table 2)
대상 실행시간 증가 메모리 증가
12개 애플리케이션 평균 9.63% 14.83%
PHP 21.76% 7.85%
TensorFlow 28.80% 9.80%
ncnn 22.44% 99.09%
Redis 9.50% 33.46%

메모리는 peak RSS를 기준으로 평가했으며, Appendix C.1은 workload 실행 중 각 process의 peak RSS를 합산한 값을 제시한다. 이는 모든 process가 동시에 점유한 최대 메모리와는 구분해야 한다. 논문은 PHP와 TensorFlow의 상대적으로 큰 시간 비용을 call stack 기반 오탐 억제와 연결하고, ncnn·Redis의 메모리 비용을 객체 메타데이터와 해제 지연으로 설명한다. 평균 추가 비용을 비계측 실행 대비 비용으로 읽으면 안 된다. 논문이 제시한 native 대비 전체 비용은 실행시간 약 1.92배, 메모리 약 2.31배이다(§5.3, §6).

오탐과 알려진 버그

네 가지 오탐 회피를 모두 끄면 12개 중 5개 애플리케이션에서 오탐이 발생했다. gRPC는 1,030개 workload 중 110개(10.68%), MicroPython은 922개 중 298개(32.32%)가 실패했고, Redis·PHP·TensorFlow는 테스트 전에 종료되었다. 기능을 활성화하면 이 평가의 오탐은 모두 제거되었다(Table 3). gRPC·PHP·RocksDB를 대상으로 한 추가 24시간 fuzzing에서도 오탐이 관측되지 않았다(Table 9). 이는 평가한 실행 범위에서의 결과이다.

PHP와 ImageMagick의 알려진 CMA 버그 10건은 Shim을 끈 ASan에서는 검출되지 않았으나, Shim을 끈 CMASan에서는 모두 검출되었다(Table 4). 이 비교는 수동 Shim 없이 CMA를 지원한다는 근거이며, 올바르게 설정된 Shim보다 언제나 우월하다는 비교는 아니다.

신규 버그와 메커니즘의 효과

Table 5의 신규 버그는 7개 프로젝트에서 총 19건으로, BOF 11건과 UAF 8건이다. MicroPython 9건, qhull 2건, RapidJSON 3건, SQLite3 1건, PHP 2건, Taichi 1건, ncnn 1건이며, 평가 조건에서 ASan과 Goshawk는 이들을 검출하지 못했다. Goshawk는 BOF를 지원하지 않으며 UAF 사례에서는 API 식별 실패, clear/realloc 모델링 한계, 복잡한 경로 등의 문제가 있었다.

논문 작성 시점의 Table 5 상태는 patched & CVE 6건, patched 1건, confirmed 5건, reported 7건이다. 따라서 수정된 버그는 7건이고 그중 6건에 CVE가 있으며, 수정된 건을 포함하여 확인된 것은 12건이다. MicroPython 버그와 PHP 버그 1건은 AFL++ fuzzing으로, 나머지는 unit test로 발견했다.

MicroPython의 CVE-2023-7152와 qhull의 Issue #136-2는 quarantine이 없으면 빠른 객체 재사용 때문에 검출되지 않았다. RapidJSON의 PR #2244와 PR #2256은 clear 이후 UAF로, allocation zone과 clear 처리의 필요성을 보여준다. SQLite3의 CVE-2023-7104와 PHP의 Issue #13230은 기존 테스트에서 각각 9년, 2년 동안 놓친 사례이다(§5.6).

Contribution

  1. 실제 CMA 사용 사례를 조사하여 Arena의 내부 객체 경계와 Recycler의 독립적인 객체 수명이 ASan의 주요 탐지 공백임을 설명했다.
  2. CMA를 표준 allocator로 대체하지 않고, API 계측과 별도 메타데이터로 CMA 객체의 공간·시간적 오류를 검사하는 sanitizer를 설계했다.
  3. Instance별 quarantine, clear용 allocation zone, 네 가지 오탐 회피로 CMA 특유의 수명 관리와 내부 접근을 처리했다.
  4. 12개 애플리케이션의 추가 비용·오탐 평가와 알려진 버그 10건 및 신규 버그 19건의 검출을 통해 실제 적용 가능성을 보였다.

Criticisms

  • API 식별의 완전성과 유지 비용: 이름 휴리스틱, 같은 module의 family 관계, 사람의 의미 분류에 의존한다. 특이한 이름이나 파일을 가로지르는 allocator 구조에서 누락이 없는지, 코드 변경 후 재분류 비용이 얼마나 드는지는 별도 검증이 필요하다. 약 10분이라는 수치는 평가 대상에서의 적용 비용이다.
  • 검사 억제의 범위: stack에 CMA 함수가 있으면 오류 보고를 억제하므로 CMA 내부 버그는 기본 탐지 범위 밖이다. 연구적 해석으로는 해당 문맥에서 실행되는 callback 등도 검사를 얼마나 잃는지 확인할 필요가 있다. 억제를 끄면 내부 오류를 탐색할 수 있지만 합법적인 메타데이터 접근과의 구분 문제가 다시 생긴다.
  • 유한한 temporal coverage: quarantine 퇴출 후 주소가 재사용되면 오래된 포인터를 항상 구별할 수 없고, clear 추적도 기본 최근 10,000개 객체로 제한된다. 따라서 모든 UAF/DF 검출을 보장하지 않는다. 기본 한도를 변화시킨 탐지율·메모리·성능의 관계가 더 필요하다.
  • Allocator 동작에 미치는 영향: CMA 알고리즘을 교체하지 않아도 요청 크기 증가, free 지연, inlining 금지는 실행을 바꾼다. 실제 RocksDB에서는 할당량 기반 최적화 조건이 바뀌어 일부 unit test가 실패했고 threshold 또는 redzone 조정이 필요했다. PHP에서도 ASan/CMASan의 실행 비용으로 timeout 조정이 필요했다(§6).
  • 평균값과 평가 범위: 평균 메모리 증가 14.83%와 달리 ncnn은 99.09%이다. 12개 프로젝트는 빌드와 workload 조건으로 선택된 집합이므로 모든 CMA에 비용을 일반화하기 어렵다. 오탐 회피 전체를 끈 비교와 사례별 분석은 있지만, 각 기능의 비용과 false negative 증가를 분리한 정량 평가에는 여지가 있다.
  • 언어·컴파일러 일반성: C/C++ 중심의 평가이며 typed alloc 처리는 opaque pointer를 끈 LLVM 구성에 의존한다. Appendix E도 ASan의 언어 지원만으로 CMA 식별까지 자동 지원되는 것은 아니며, Rust 등에는 추가 작업이 필요함을 인정한다.

Conclusion

CMASan은 sanitizer의 탐지 공백을 CMA가 정의한 객체 경계·수명과 ASan이 관측하는 경계·수명의 불일치로 설명한다. CMA API의 의미를 계측과 연결하고 재사용 및 bulk clear를 추적하면, 기존 테스트가 실행하던 오류도 새로 검출할 수 있음을 보였다. CMA 사용자 코드의 메모리 안전성 검사와 allocator-aware sanitizer의 관련 연구로 중요하며, API 분류의 정확도, 검사 억제 범위, 크기·수명 변경의 영향을 함께 고려해야 한다.