FlawOpen

flawopen.com/메모리 안전성/Rust unsafe

Rust의 unsafe 블록은 메모리 안전한가요?

CWE-787: 범위를 벗어난 쓰기참고 페이지
핵심 답변

unsafe는 Rust의 검사를 끄지 않습니다. 컴파일러가 안전 조건을 증명할 수 없는 특정 연산을 허용하며, 그 조건을 충족할 책임은 프로그래머에게 있습니다. 따라서 건전하지 않은 unsafe 추상화는 호출자가 unsafe를 작성하지 않아도 일반 안전 코드에서 정의되지 않은 동작(UB)을 일으킬 수 있습니다.

핵심 용어

Unsafe 연산:
원시 포인터 역참조처럼 컴파일러가 안전 조건을 증명할 수 없는 연산.
건전한 추상화:
안전한 호출자가 정의되지 않은 동작을 일으킬 수 없는 안전 API.
size_hint():
최적화를 위한 이터레이터 크기 추정값으로, 정확한 개수나 메모리 안전성 증명은 아닙니다.
정의되지 않은 동작(UB):
Rust가 동작을 정의하지 않는 프로그램 동작입니다. unsafe 코드는 안전한 호출자가 이를 유발하지 못하게 해야 합니다.

건전하지 않은 안전 추상화가 실패하는 방식

1단계

안전 API가 이터레이터를 받음

호출자가 unsafe 사전 조건이 없는 함수에 일반 이터레이터를 전달합니다.

2단계

힌트의 하한만큼만 예약

함수는 보고된 하한만큼만 공간을 예약하며, 실제 생성되는 항목 수는 더 많을 수 있습니다.

3단계

검사 없는 쓰기가 여유 용량을 초과

구현은 할당을 늘리거나 매 항목의 용량을 확인하지 않은 채 원시 포인터로 모든 항목을 씁니다.

4단계

안전한 호출이 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를 노출한 추상화에 결함이 있습니다.

실무 규칙

자주 묻는 질문

unsafe를 전부 금지할 수 있나요?

#![forbid(unsafe_code)]는 설정된 crate에서 unsafe 구문을 금지할 수 있어 애플리케이션에 유용합니다. 의존성에는 적용되지 않으며 일부 구현, FFI 경계, 라이브러리에는 unsafe 연산이 필요합니다.

unsafe 코드가 trait을 신뢰하게 할 수 있나요?

가능합니다. unsafe trait은 구현자가 지켜야 할 의무를 문서화하며 unsafe 코드는 이를 신뢰할 수 있습니다. TrustedLen은 정확한 size hint를 약속하는 이터레이터용 표준 라이브러리의 불안정 trait입니다. 일반 Iterator::size_hint에는 이 보장이 없습니다.

이 점이 Rust의 장점을 약화하나요?

아닙니다. Rust의 안전 모델은 메모리 안전 의무가 발생하는 범위를 좁히고 unsafe 경계에서 이를 명시합니다. unsafe 코드, 의존성, FFI 계약, unsafe 구현 위의 안전 API를 감사할 필요는 여전히 있습니다.

참고 자료

함께 보기 strncpy()가 strcpy()보다 안전한가요? CWE와 CVE: 무엇이 다른가 Mass Assignment: 위험과 안전한 필드 허용 목록 Serverless 상태 누출: 사용자 데이터를 요청별로 분리 PHP ==와 Magic Hash 비교