Finding panics with Call Graph Analyzer
Call Graph Analyzer로 Rust 패닉 경로 찾기
Call Graph Analyzer(CGA)는 monomorphized MIR를 순회해 바이너리에서 도달 가능한 함수와 패닉 경로를 찾는 Rust 도구입니다. 의존성까지 분석하고 경로를 진단하지만, nightly 컴파일러 의존성과 최적화 전 분석에 따른 오탐 등 제약도 있습니다.
- 주제
에디터 노트
CGA의 장점은 표준 라이브러리와 의존성까지 포함한 패닉 경로를 호출 위치와 함께 보여준다는 점입니다. HIR 분석이나 링커 검사로 놓치는 경로를 찾으려는 목적에 MIR 순회가 잘 맞습니다. 다만 nightly rustc에 묶이고 분석에 cargo check보다 2~3배 더 걸리며, 최적화 뒤 사라질 경로도 보고할 수 있습니다. 따라서 결과는 배포 바이너리의 패닉을 완전히 증명하는 목록이라기보다 설정을 고정해 변경을 비교하는 분석 자료로 쓰는 편이 타당합니다. 글이 제시한 opt-level=2 권고도 오탐을 줄이는 설정이지, 모든 프로젝트에서 결과가 최종 바이너리와 일치한다는 뜻은 아닙니다. 차단 목록 역시 Rust 내부 함수 호출을 검사할 뿐 외부 오브젝트나 어셈블리까지 제한하지 않습니다.
AI 요약
Rust에서 패닉을 모두 없애려 하면 오류를 Result로 호출 스택 위쪽에 전달할 뿐, 마지막에는 프로세스를 중단하는 상황이 생길 수 있습니다. Tweede Golf는 무조건 패닉을 제거하기보다 의도하지 않은 패닉 경로를 드러내고 변경을 추적하는 편이 유용하다고 설명합니다. 다만 임베디드 장치처럼 코드 크기가 중요하거나, 외부 입력으로 서비스가 중단될 수 있는 경로, 패닉 처리 코드처럼 실패를 제한해야 하는 곳에서는 패닉을 줄일 이유가 있습니다.
기존 분석 방식과 CGA의 선택
기존 도구는 링커에서 패닉 핸들러 연결을 검사하거나, ELF의 DWARF 디버그 정보, Rust의 HIR·MIR, rust-analyzer 분석 결과를 이용합니다. 링커 방식은 바이너리에 패닉이 하나도 없어야 하고 라이브러리에는 적용하기 어렵습니다. ELF 분석은 인라인 처리나 링크 단계에서 사라진 호출 정보를 놓칠 수 있습니다. HIR 분석은 의존성 코드를 보지 못하고, rust-analyzer는 복잡한 proc_macro가 있는 프로젝트를 분석하지 못할 때가 있습니다.
CGA는 rustc-driver로 동작하며 monomorphized MIR에서 호출 그래프를 만듭니다. 컴파일된 코드의 진입점에서 시작해 호출되는 함수를 따라가므로 사용되지 않는 함수는 분석에서 빠지고, 제네릭 함수는 구체적인 타입별 인스턴스를 따로 추적합니다. 의존성과 표준 라이브러리에서 발생하는 패닉 경로도 분석 대상에 들어갑니다. dyn 객체의 실제 호출 대상을 특정하기 어려우면 해당 vtable의 모든 항목을 호출 가능하다고 가정합니다. 함수 포인터도 만들어지면 나중에 호출될 수 있다고 간주합니다. 이런 가정은 빠뜨리는 경로를 줄이는 대신 분석 그래프와 경고를 늘릴 수 있습니다.
설치와 실행, 진단 결과
CGA는 특정 nightly Rust 컴파일러와 rustc-private에 의존합니다. crates.io에서 일반 crate처럼 설치하지 못하고 저장소를 받아 cargo install --path .로 설치해야 합니다. cargo cga check는 cargo check처럼 컴파일하면서 그래프를 수집하며, 일반적인 crate에서 cargo check보다 약 2~3배 오래 걸립니다. 이 과정은 실제 배포 바이너리와 다른 rustc 버전, 표준 라이브러리, 컴파일 옵션을 사용할 수 있어 결과 그래프가 최종 바이너리와 정확히 같지는 않습니다.
분석은 crate의 진입점에서 MIR를 따라가며 패닉 경로를 찾습니다. 경고에는 문제가 된 호출 위치와 경로가 포함되며, dyn 호출의 vtable 해석 여부도 표시합니다. 여러 경로가 한 호출 지점에 연결되면 휴리스틱 점수로 대표 경로 하나를 고릅니다. cargo cga extract는 호출 그래프를 JSON이나 GraphViz dot 파일로 내보냅니다. 그래프가 커지면 읽기 어려워질 수 있습니다.
경고가 많은 프로젝트는 설정 파일에서 허용 목록과 차단 목록을 정리할 수 있습니다. 허용 목록에 등록한 함수가 포함된 패닉 경로는 무시하고, 차단 목록은 특정 함수 호출을 찾아냅니다. 파일 시스템이나 프로세스 관련 함수를 차단하는 식으로 쓸 수 있지만, 외부 오브젝트 파일이나 어셈블리 호출까지 막는 보안 경계는 아닙니다.
한계와 컴파일 옵션
MIR은 안정화된 인터페이스가 아니므로 Rust 컴파일러 업데이트에 따라 구조가 달라질 수 있습니다. vtable, 컴파일러가 삽입하는 shim, coroutine, 암묵적 검사까지 추적해야 해 분석이 깨지거나 부정확해질 여지가 있습니다. CGA가 보는 MIR에는 이후 최적화로 제거될 패닉 경로가 남을 수도 있습니다. 예를 들어 1 + 1의 오버플로 검사는 opt-level에 따라 MIR에 남거나 앞선 최적화에서 사라집니다. 글은 일반적으로 opt-level=2를 CGA 분석에 권합니다. 이 설정이면 도달 가능한 패닉 경로를 놓치지 않으면서 오탐을 줄일 수 있다고 설명합니다. 프로젝트가 build-std로 빌드되지 않거나 build.rs가 일반 rustc만을 기대하는 경우에도 CGA 실행이 실패할 수 있습니다.
원문: Tweede Golf / 번역·요약: Trawling