거대한 약속을 깨뜨린 작은 Safe Code 패턴
Rust의 근본적인 약속, 즉 Borrow Checker가 Use-After-Free를 방지한다는 사실이 최근 중대한 도전에 직면했습니다. 지난주에 제기된 Issue #164250은 이전에 불가능하다고 여겨졌던, 완전히 Safe Rust 코드에서 재현 가능한 메모리 안전성 버그를 상세히 다루고 있습니다. 이 결함은 2015년 Rust 1.4.0 버전 이후 11년 동안 감지되지 않은 채 지속되었습니다.
이 익스플로잇은 특정 패턴에 의존합니다. struct Q<T: 'static>(Option<T>);와 같은 제네릭 구조체는 타입 파라미터 T에 대해 'static 라이프타임 요구 사항을 선언합니다. 결정적으로, 이 구조체는 포함된 데이터가 프로그램 전체 기간 동안 유지될 것이라고 가정하며 이 'static 바운드를 암묵적으로 신뢰하는 사용자 정의 Drop 함수를 구현합니다.
놀라운 단계는 할당 과정에서 발생합니다. 개발자가 let mut q = Q(None);과 같이 구조체를 비어 있는 상태로 인스턴스화한 다음, q.0 = Some(&l);과 같이 공변 필드(covariant field)에 &l과 같은 수명이 짧은 지역 변수에 대한 참조를 할당합니다. 여기서 컴파일러는 할당된 참조에 대해 외부 타입의 'static 라이프타임 의무를 강제하는 데 실패합니다.
이러한 간과로 인해 Drop 구현은 'static이라고 믿는 참조를 받게 되지만, 실제로는 지역 변수가 범위를 벗어나면 할당 해제된 메모리를 가리키게 됩니다. AddressSanitizer와 Miri에 의해 검증된 이 시나리오는 stack-use-after-scope 또는 dangling reference use-after-free를 초래하며, Rust의 핵심 메모리 안전성 보장을 직접적으로 위반합니다.
라이프타임 약속이 어떻게 Dangling Reference로 변했는가
실패의 핵심은 Rust 컴파일러가 특정 할당 중에 라이프타임 약속을 처리하는 방식에 있습니다. 구조체는 'static 라이프타임 바운드를 사용하여 보유한 모든 참조가 프로그램 전체 기간 동안 유지되어야 한다고 선언할 수 있습니다. 이 구조체에 대한 사용자 정의 Drop 구현은 저장된 참조가 유효할 것이라고 가정하며 이 약속을 암묵적으로 신뢰합니다.
문제는 수명이 제한된 지역 변수에 대한 참조가 그러한 구조체 내의 필드에 할당될 때 발생합니다. 컴파일러는 할당에 대한 타입 검사를 수행하지만, 결정적으로 새로 할당된 참조에 대해 구조체의 전체 라이프타임 요구 사항을 재검증하는 것을 생략합니다. 이는 구조체의 소멸자는 'static 참조를 가지고 있다고 믿지만, 실제 데이터는 곧 만료될 지역 변수라는 단절을 만듭니다.
구조체가 Drop될 때, Drop 구현이 실행되면서 이제는 할당 해제된 메모리를 가리키는 참조를 조작하게 됩니다. 의도적으로 제작된 Drop은 이 dangling reference를 숨기거나 사용하려고 시도할 수 있으며, 결과적으로 use-after-free 또는 stack-use-after-scope 메모리 오류가 발생합니다. 예를 들어, Drop은 나중에 액세스하기 위해 유효하지 않은 참조를 별도의 스레드에 전달할 수 있습니다.
예제 패턴이 unsafe 블록을 직접 포함하지는 않지만, 결과적으로 발생하는 메모리 손상은 컴파일러 검사의 허점을 보여줍니다. Miri 및 AddressSanitizer(ASan)와 같은 도구는 이러한 유효하지 않은 메모리 액세스를 감지하여 stack-use-after-scope 또는 dangling reference use-after-free와 같은 문제를 플래그 지정함으로써 Issue #164250의 익스플로잇 가능성을 확인합니다.
11년의 세월—그리고 우연히 발생하기 어려운 버그
한 Rust 컴파일러 기여자가 Issue #164250의 메모리 안전성 버그를 2015년 10월에 출시된 Rust 1.4 버전까지 추적했습니다. 이는 해당 결함이 약 11년이나 된 것으로, 메모리 안전성을 핵심 약속으로 내세우는 언어의 취약점으로서는 놀라울 정도로 긴 수명입니다. 이 발견으로 인해 제보자는 "도대체 어떻게 지금까지 발견되지 않았을까?"라며 놀라움을 표했습니다.
이 이슈는 그 연식과 Rust의 근본적인 보장에 미치는 영향에도 불구하고 P-critical이 아닌 I-unsound 및 P-high로 분류되었습니다. 이러한 분류는 건전성(soundness)을 가볍게 여겨서가 아니라, 익스플로잇 패턴이 매우 인위적이라는 점을 반영한 것입니다. P-critical 등급은 일반적으로 일반적인 코드에서 실수로 쉽게 유발될 수 있는 버그에 부여됩니다.
일반적인 애플리케이션 코드에서 이 결함에 우연히 노출될 가능성은 매우 낮다고 판단됩니다. 이 버그를 악용하려면 다음과 같은 요소들이 의도적으로 결합되어야 합니다:
- 'static 라이프타임 바운드가 있는 제네릭 타입
- 라이프타임 검증을 우회하는 허용적인 필드 할당
- 불일치를 악용하도록 설계된 커스텀
Drop구현(주로 dangling 참조를 전역 변수나 다른 스레드로 이동시키는 방식)
Rust Types Team 멤버인 lcnr은 이 버그가 11년 동안 잠들어 있었다는 사실은 이 버그를 유발하기가 쉽지 않음을 시사한다고 언급했습니다. 이 특정 패턴은 핵심 불변성을 깨뜨리기는 하지만, use-after-free를 유발하려면 악의적이거나 매우 이례적인 Drop 구현이 필요합니다. 더 자세한 기술적 내용은 GitHub 이슈를 참조하십시오: struct's lifetime requirements can be bypassed via covariant field assignment · Issue #164250 · rust-lang/rust.
이 글이 마음에 드셨나요? 매일 아침 이런 글을 메일로 받아보세요.
하루 한 통 · 두 번의 클릭으로 구독 취소 · 제3자 추적 없음
수정 사항과 헤드라인 뒤에 숨겨진 교훈
Issue #164250에 대한 해결책은 PR #164277을 통해 신속하게 제시되었습니다. 이 수정 사항은 MIR local types에 대한 컴파일러의 올바른 형식(well-formedness) 검증을 강화하는 것을 포함하며, 특히 할당 및 암시적 서브타이핑이 중요한 라이프타임 바운드 검사를 우회하지 않도록 보장합니다. 이는 'static 라이프타임 약속을 깨뜨릴 수 있었던 검증 누락 문제를 직접적으로 해결합니다.
이번 사건은 놀랍기는 하지만, 더 넓은 진실을 강조합니다. 즉, Rust와 같은 언어에서도 메모리 안전성은 근본적으로 컴파일러가 강제하는 보장에 의존한다는 것입니다. 컴파일러는 복잡한 소프트웨어 시스템이며, 모든 복잡한 코드와 마찬가지로 미묘한 엣지 케이스와 예상치 못한 상호작용을 포함할 수 있습니다. 엄격한 테스트, 지속적인 코드 리뷰, 그리고 꾸준한 soundness work는 필수적입니다.
Safe code에서 메모리 안전성을 보장한다는 Rust의 핵심 약속은 여전히 견고하지만, 11년 된 이 버그는 냉혹한 경각심을 일깨워줍니다. 이는 왜 그 약속을 이행하는 데 있어 언어 설계만큼이나 컴파일러의 건전성이 중요한지를 보여줍니다. Rust의 safe-code 보장을 실제로 위반한 사례이기는 하지만, Rust가 전반적으로 안전하지 않다는 것을 의미하지는 않습니다. 오히려 철저한 컴파일러 개발의 필요성을 다시 한번 강조합니다.
자주 묻는 질문(FAQ)
Rust 메모리 안전성 버그란 무엇이었나요?
Issue #164250은 필드 할당이 어떻게 구조체의 라이프타임 요구 사항을 우회할 수 있는지, 그리고 이를 통해 안전한 코드에서 특수하게 작성된 소멸자를 사용하여 dangling 참조를 생성할 수 있는지를 설명합니다.
일반적인 Rust 코드가 실수로 이 버그를 유발할 수 있나요?
입증된 패턴은 이례적인 조합을 필요로 하며, 특히 거짓으로 긴 라이프타임을 가진 참조를 의도적으로 신뢰하고 사용하는 Drop 구현이 필수적입니다.
이 버그가 Rust가 더 이상 메모리 안전하지 않다는 것을 의미하나요?
아닙니다. 이는 심각한 컴파일러 건전성 결함이지만, Rust의 전반적인 메모리 안전성 이점을 무효화하거나 일반적인 Rust 프로그램이 위험에 노출되어 있다는 것을 의미하지는 않습니다.
Rust 버그는 어떻게 수정될 예정인가요?
제안된 수정 사항은 MIR local types가 올바른 형식(well-formedness) 요구 사항을 충족하는지 확인하여, 할당이 라이프타임 제약을 우회하지 못하도록 방지합니다.

