Rust 4분 소요

Rust 빌림 검사기에 구멍이 있다면, 안전한 코드도 위험할까?

핵심 요약

  • 빌림 검사기는 데이터가 유효한 동안에만 참조를 쓰도록 검사합니다.
  • 검사기의 건전성에 결함이 있으면 safe Rust 코드에서도 메모리 안전이 깨질 수 있습니다.
  • 결함이 얼마나 오래 있었는지만으로 실제 피해 규모나 공격 가능성을 판단할 수는 없습니다.
  • Rust의 안전 보장은 컴파일러와 안전한 API를 제공하는 라이브러리가 올바르게 구현돼 있다는 전제 아래 성립합니다.

‘11년간 숨어 있던 Rust 빌림 검사 버그’라는 표현에서 먼저 눈길을 끄는 건 기간입니다. 하지만 개발자라면 그 기간보다 먼저 짚어야 할 문제가 있습니다. “컴파일러가 안전하다고 통과시킨 코드에서도 메모리 문제가 생길 수 있나?”라는 질문입니다.

빌림 검사기는 참조의 유효 기간을 확인합니다

프로그램은 데이터를 매번 복사하는 대신, 데이터가 있는 곳을 가리키는 ‘참조’를 자주 사용합니다. 책을 복사해서 나눠주는 대신 서가에서 책이 놓인 위치를 알려주는 것과 비슷합니다.

그런데 그 자리에 있던 책이 사라지면 어떻게 될까요. 메모리에서도 이미 해제된 데이터를 예전 참조로 접근하면 문제가 생깁니다. 이를 해제 후 사용, 영어로는 use-after-free라고 부릅니다.

Rust의 빌림 검사기는 이런 상황을 컴파일 단계에서 막습니다. 참조를 쓰는 동안 원본 데이터가 유효한지 확인하고, 데이터를 읽는 참조와 수정하는 참조가 서로 충돌하지 않는지도 검사합니다.

이때 알아야 할 개념이 수명(lifetime)입니다. 실행 중 초시계로 재는 시간이 아니라, 코드에서 참조를 유효하게 쓸 수 있는 범위와 관계를 뜻합니다. 수명을 표기한다고 해서 데이터가 더 오래 살아남는 것은 아닙니다.

예를 들어 함수 안에서 만든 데이터의 참조를 함수 밖으로 돌려준다고 해보겠습니다. 함수가 끝날 때 데이터가 사라진다면, 돌려받은 참조는 사용할 수 없습니다. 빌림 검사기는 이 관계를 파악하고 해당 코드를 거부해야 합니다.

‘건전성 버그’는 안전 판정의 오류입니다

컴파일러 버그도 무엇을 잘못 처리하느냐에 따라 문제가 달라집니다. 안전한 코드를 잘못 거부하면 개발에 지장을 줍니다. 반대로 위험한 코드를 안전하다고 받아들이면 컴파일러가 보장하는 안전을 믿기 어려워집니다.

이처럼 위험한 코드를 통과시키는 오류가 건전성(soundness) 문제입니다. 쉽게 말해 “통과시켜 준 코드가 약속한 안전 조건을 지키는가”를 따지는 겁니다.

빌림 검사기가 수명 관계를 잘못 판단하면, 이미 유효하지 않은 참조를 쓰는 코드가 검사를 통과할 수 있습니다. 이런 결함이 있다면 개발자가 unsafe를 직접 쓰지 않아도 메모리 안전이 깨질 수 있습니다.

그 결과로 생길 수 있는 ‘정의되지 않은 동작’은 단순히 오류 메시지가 뜬다는 뜻이 아닙니다. 언어가 실행 결과를 보장하지 못하는 상태를 말합니다. 프로그램이 종료되거나 잘못된 값을 읽을 수 있고, 겉보기에는 정상적으로 작동할 수도 있습니다.

다만 safe Rust에서 재현된다는 설명만으로 빌림 검사기를 원인으로 단정할 수는 없습니다. 안전하게 호출할 수 있도록 공개한 라이브러리 함수라도, 내부의 unsafe 코드를 잘못 구현했다면 비슷한 일이 생깁니다. 컴파일러가 잘못 판정했는지, 라이브러리를 잘못 구현했는지 구분해야 누가 수정하고 어떻게 대응할지도 분명해집니다.

‘11년’이라는 숫자와 실제 위험은 구분해야 합니다

‘11년 된 버그’라는 표현을 읽을 때는 그 기간을 어디서부터 셌는지 살펴야 합니다. 결함이 처음 들어간 시점부터인지, 처음 보고된 시점부터인지에 따라 의미가 달라집니다.

결함이 오래 있었다고 해서 모든 프로그램에서 그 결함이 있는 코드를 실행하는 것은 아닙니다. 특정한 수명 관계나 코드 조합에서만 문제가 드러날 수 있습니다. 어떤 컴파일러 버전이 영향을 받고, 어떤 조건에서 문제가 재현되는지 확인해야 하는 이유입니다.

실제 공격에 쓸 수 있는지도 따로 따져야 합니다. 잘못된 참조를 만드는 코드를 재현했다고 해서, 외부 입력으로 실제 서비스의 메모리를 손상시킬 수 있다는 증거까지 확보한 것은 아닙니다.

물론 재현 조건이 까다로워도 건전성 결함을 가볍게 볼 수는 없습니다. 개발자는 safe Rust의 규칙을 지키면 메모리 안전을 보장받는다고 믿고 코드를 작성합니다. 드물게 일어나는 일이라도 그 전제가 깨진다면 충분히 수정할 이유가 됩니다.

저는 오래된 건전성 버그를 볼 때 ‘몇 년 동안 놓쳤나’와 함께 수정 과정도 살펴야 한다고 생각합니다. 어떤 코드가 영향을 받는지 제대로 설명하는지, 수정한 뒤 같은 문제가 다시 생기지 않도록 검증하는지가 더 직접적인 판단 기준입니다.

메모리 안전을 보장하려면 구현도 정확해야 합니다

Rust가 메모리 안전을 보장하려면 언어의 규칙부터 안전해야 합니다. 컴파일러는 그 규칙을 정확히 검사해야 하고, 안전한 API 내부에서 동작하는 unsafe 구현도 정해진 조건을 지켜야 합니다.

일반적인 Rust 개발자는 이 조건들이 지켜진다는 전제 아래 작업합니다. 덕분에 메모리를 다루는 모든 연산을 매번 손으로 검토하는 부담을 줄일 수 있습니다. 그만큼 언어나 컴파일러, 라이브러리에 있는 결함은 개별 애플리케이션의 실수보다 더 넓은 범위에 영향을 줄 수 있습니다.

실무에서는 재현 코드가 있다는 것과 자기 서비스가 영향을 받는다는 것을 구분해야 합니다. 사용하는 컴파일러 버전뿐 아니라 문제가 생기는 코드 조건도 함께 확인해야 합니다. 기존 실행 파일에 컴파일러 수정 사항을 반영하려면 수정된 컴파일러로 다시 빌드해야 합니다.

테스트로 확인할 수 있는 범위에도 한계가 있습니다. 테스트는 실행해 본 조건에서 문제가 드러나는지 확인하는 데 도움이 됩니다. 하지만 테스트를 통과했다는 사실만으로 언어 차원의 안전 보장을 대신할 수는 없습니다.

Rust는 메모리 안전을 강하게 보장하지만, 이를 구현하는 컴파일러도 검증하고 고쳐야 합니다. 빌림 검사기의 결함을 살필 때는 얼마나 오래됐는지와 함께 정확히 어떤 보장이 깨졌고, 수정으로 어떻게 회복되는지 확인해야 합니다. 팀에서 애플리케이션 의존성의 보안 업데이트를 꾸준히 살피듯, 컴파일러의 안전성 수정도 챙길 필요가 있습니다.

Rust 메모리 안전 빌림 검사기

댓글

    댓글을 불러오는 중...