flawopen.com/메모리 안전성/Rust unsafe
Rust의 unsafe 블록은 메모리 안전한가요?
unsafe는 Rust의 검사를 끄지 않습니다. 컴파일러가 안전 조건을 증명할 수 없는 특정 연산을 허용하며, 그 조건을 충족할 책임은 프로그래머에게 있습니다. 따라서 건전하지 않은 unsafe 추상화는 호출자가 unsafe를 작성하지 않아도 일반 안전 코드에서 정의되지 않은 동작(UB)을 일으킬 수 있습니다.
핵심 용어
- Unsafe 연산:
- 원시 포인터 역참조처럼 컴파일러가 안전 조건을 증명할 수 없는 연산.
- 건전한 추상화:
- 안전한 호출자가 정의되지 않은 동작을 일으킬 수 없는 안전 API.
- size_hint():
- 최적화를 위한 이터레이터 크기 추정값으로, 정확한 개수나 메모리 안전성 증명은 아닙니다.
- 정의되지 않은 동작(UB):
- Rust가 동작을 정의하지 않는 프로그램 동작입니다. unsafe 코드는 안전한 호출자가 이를 유발하지 못하게 해야 합니다.
건전하지 않은 안전 추상화가 실패하는 방식
안전 API가 이터레이터를 받음
호출자가 unsafe 사전 조건이 없는 함수에 일반 이터레이터를 전달합니다.
힌트의 하한만큼만 예약
함수는 보고된 하한만큼만 공간을 예약하며, 실제 생성되는 항목 수는 더 많을 수 있습니다.
검사 없는 쓰기가 여유 용량을 초과
구현은 할당을 늘리거나 매 항목의 용량을 확인하지 않은 채 원시 포인터로 모든 항목을 씁니다.
안전한 호출이 UB에 도달
항목이 수용량보다 많으면 안전한 호출도 범위 밖에 쓸 수 있습니다. Vec::extend 등 안전하게 확장되는 구현을 사용하세요.
// UNSOUND: reserve uses only the lower hint, // then unchecked writes consume every item. fn append_unsound(dst: &mut Vec<u8>, iter: impl Iterator<Item = u8>) { let (lower, _) = iter.size_hint(); dst.reserve(lower); let start = dst.len(); let mut written = 0; unsafe { let out = dst.as_mut_ptr().add(start); for item in iter { // UB once writes exceed spare capacity. out.add(written).write(item); written += 1; } dst.set_len(start + written); } }
// SOUND: Vec::extend handles capacity // growth and length updates safely. fn append(dst: &mut Vec<u8>, iter: impl Iterator<Item = u8>) { dst.extend(iter); }
Rust의 보장이 실제로 의미하는 것
정확한 보장은 다음과 같습니다. 의존하는 unsafe 코드가 건전하다면 안전한 Rust는 정의되지 않은 동작을 일으킬 수 없습니다.Rust Reference는 경계에서 soundness를 정의합니다. unsafe 코드는 안전한 클라이언트가 UB를 일으키지 못하도록 해야 합니다. 컴파일러는 unsafe 연산 내부의 모든 증명 의무를 검증하지 않으므로 프로그래머와 리뷰어가 이를 충족해야 합니다.
C나 C++와 비교해도 이는 Rust의 중요한 실무상 장점입니다. Rust는 메모리에 민감한 연산과 안전 의무를 더 잘 드러내므로 리뷰어가 unsafe 경계와 그 위에 구축된 안전 추상화에 집중할 수 있습니다. 경계에는 unsafe 블록, unsafe 함수 계약, unsafe trait 구현, FFI 선언, unsafe 속성이 포함되며 항상 작은 블록 하나에 국한되지는 않습니다. 실제 사례인 smallvec의 CVE-2021-25900에서는 SmallVec::insert_many가 이터레이터 size hint의 하한만큼 예약한 뒤 할당량보다 많은 항목을 기록했습니다. CVSS는 9.8입니다.
기준은 주의가 아니라 soundness
안전한 코드가 API를 사용해 정의되지 않은 동작을 일으킬 수 있다면 그 API는 건전하지 않습니다. 예를 들어 Iterator::size_hint는 최적화에 유용한 경계값이지 정확한 항목 수가 아닙니다. 유효한 이터레이터도 하한보다 더 많이 낼 수 있고, 잘못된 이터레이터는 추정치를 위반할 수 있습니다. 표준 문서는 이 힌트를 근거로 unsafe 코드의 범위 검사를 생략하지 말라고 명시합니다. 안전 trait 동작 오류가 논리 오류나 panic을 일으킬 수 있지만 그것만으로 UB가 되지는 않습니다. 그래도 호출자 코드가 예상과 달리 동작할 때 unsafe 코드는 메모리 불변식을 지켜야 합니다.
따라서 unsafe 코드의 영향은 비국소적입니다. 한곳에서 세운 메모리 불변식을 상위의 안전 API가 신뢰할 수 있습니다. 불변식이 잘못되었다면 계약대로 사용한 안전 호출자가 아니라 안전 API를 노출한 추상화에 결함이 있습니다.
실무 규칙
- ✓안전 trait 메서드를 메모리 안전성 증명으로 간주하지 마세요.
size_hint()는 최적화에만 사용하고 검사를 유지하거나 필요하면 용량을 늘리세요. - ✓패닉 중에도 불변식을 유지하세요.
Iterator::next, 소멸자, 클로저 같은 호출자 코드가 언와인딩할 수 있습니다. 부분 초기화 상태가 유효하고 올바르게 drop되도록 하세요. - ✓
// SAFETY:주석을 작성하세요. 필요한 조건과 충족되는 이유를 설명합니다. 설명 없는 unsafe는 리뷰할 수 없습니다. - ✓블록을 좁게 유지하세요. 키워드 반복을 피하려고 루프 전체를 감싸면 리뷰어가 추론할 범위가 커집니다.
- ✓실행되는 테스트 경로에 Miri를 실행하세요. 실행 경로의 범위 밖 접근, 해제 후 사용, 잘못된 aliasing 등 일부 UB를 찾지만 soundness를 증명하지는 않습니다.
- ✓의존성을 감사하세요.
cargo audit는 알려진 RustSec 권고를 확인하고cargo geiger는 unsafe 사용 현황을 파악하는 데 도움이 됩니다. 둘 다 배포 코드의 unsafe 계약 검토를 대신하지 않습니다.
자주 묻는 질문
#![forbid(unsafe_code)]는 설정된 crate에서 unsafe 구문을 금지할 수 있어 애플리케이션에 유용합니다. 의존성에는 적용되지 않으며 일부 구현, FFI 경계, 라이브러리에는 unsafe 연산이 필요합니다.
가능합니다. unsafe trait은 구현자가 지켜야 할 의무를 문서화하며 unsafe 코드는 이를 신뢰할 수 있습니다. TrustedLen은 정확한 size hint를 약속하는 이터레이터용 표준 라이브러리의 불안정 trait입니다. 일반 Iterator::size_hint에는 이 보장이 없습니다.
아닙니다. Rust의 안전 모델은 메모리 안전 의무가 발생하는 범위를 좁히고 unsafe 경계에서 이를 명시합니다. unsafe 코드, 의존성, FFI 계약, unsafe 구현 위의 안전 API를 감사할 필요는 여전히 있습니다.
참고 자료
- The Rust Reference — unsafe code and soundness
- Rust Reference — the unsafe keyword and proof obligations
- Rust standard library — Iterator::size_hint contract
- Rustonomicon — working with unsafe
- RustSec Advisory RUSTSEC-2021-0003 — smallvec buffer overflow
- GitHub Security Advisory GHSA-43w2-9j62-hq99 — CVE-2021-25900
- The Rustonomicon — the guide to unsafe Rust
- RustSec Advisory Database
- Miri — an interpreter that detects undefined behaviour
- CWE-787 — Out-of-bounds Write
- Case study: the smallvec overflow (CVE-2021-25900)