본문으로 건너뛰기
피드

Rust가 못 잡는 버그들: uutils 44개 CVE에서 나온 시스템 코드 체크리스트

security 약 13분

Canonical이 Ubuntu 25.10부터 기본 포함한 Rust 기반 GNU coreutils 재구현체 uutils에서 44개 CVE를 공개했다. 이 버그들은 버퍼 오버플로 같은 메모리 안전 문제가 아니라, 경로 TOCTOU, Unix 바이트 처리, panic, GNU 호환성 차이처럼 Rust 컴파일러가 잡아주지 않는 시스템 경계의 문제였다.

  • 1

    Rust는 메모리 안전 버그를 크게 줄였지만 파일시스템, 경로, 권한, 입력 바이트 같은 외부 세계의 의미까지 보장하진 않음

  • 2

    `Path`를 두 번 syscall에 넘기면 심볼릭 링크 교체로 TOCTOU 취약점이 생길 수 있음

  • 3

    Unix 도구는 UTF-8 문자열이 아니라 raw byte를 다뤄야 하며 `from_utf8_lossy`는 데이터 손상을 만들 수 있음

  • 4

    GNU coreutils 재구현은 보기 좋은 의미론보다 bug-for-bug 호환성이 보안 기능이 될 수 있음

Rust 컴파일러가 막아준 것과 못 막아준 것

  • Canonical이 2026년 4월 uutils에서 44개 CVE를 공개함

    • uutils는 GNU coreutils를 Rust로 다시 구현한 프로젝트고, Ubuntu 25.10부터 기본 탑재됐음
    • 26.04 LTS를 앞두고 외부 감사를 진행하면서 다수 문제가 나온 것으로 보임
    • cp, mv, rm이 Ubuntu 26.04 LTS에서 아직 GNU 버전으로 남는 이유도 이 문제 묶음과 관련 있음
  • 중요한 포인트는 “Rust라서 안전하다”가 틀렸다는 게 아니라, Rust가 책임지는 안전의 범위가 다르다는 것임

    • 이번 버그들은 borrow checker, Clippy, cargo audit이 잡아줄 종류가 아니었음
    • 대신 파일 경로, syscall 순서, Unix byte 처리, panic, exit code, GNU 호환성 같은 시스템 경계에서 터짐
    • 반대로 버퍼 오버플로, use-after-free, double-free, 데이터 레이스, 널 포인터 역참조, 미초기화 메모리 읽기는 없었음

중요

> Rust는 C에서 흔한 메모리 폭발을 많이 없애줬지만, “두 syscall 사이에 공격자가 경로를 바꿀 수 있다” 같은 현실 세계의 시간 문제는 컴파일러가 대신 해결해주지 않음.

경로를 두 번 믿으면 TOCTOU가 된다

  • 가장 큰 버그 묶음은 경로를 한 번 확인하고, 다시 같은 경로에 행동하는 패턴이었음

    • 첫 syscall에서 경로 상태를 확인한 뒤, 두 번째 syscall에서 같은 문자열 경로를 다시 해석함
    • 그 사이 부모 디렉터리에 쓰기 권한이 있는 공격자가 경로 컴포넌트를 심볼릭 링크로 바꿀 수 있음
    • 커널은 두 번째 호출에서 경로를 새로 해석하므로, 권한 있는 작업이 공격자가 고른 대상에 꽂힘
  • CVE-2026-35355 사례가 딱 이 패턴임

    • install 유틸리티가 먼저 목적지 파일을 지우고, 이후 File::create(to)로 다시 생성함
    • 삭제와 생성 사이에 공격자가 to/etc/shadow를 가리키는 심볼릭 링크로 바꾸면, 권한 있는 프로세스가 /etc/shadow를 덮어쓸 수 있음
    • 수정은 OpenOptions::create_new(true)를 써서 대상 위치에 파일이나 dangling symlink가 있으면 실패하게 만드는 방식이었음
  • 일반 규칙은 &Path를 값처럼 믿지 말라는 것임

    • Rust 코드에서 Path는 안정적인 값처럼 보이지만, 커널 입장에선 매번 다시 해석하는 이름일 뿐임
    • 새 파일 생성은 create_new()가 도움이 되지만, 그 외 작업은 부모 디렉터리를 열고 file descriptor 기준으로 상대 작업을 해야 함
    • 같은 경로에 두 번 행동하면 TOCTOU라고 의심하고 증명해야 함

권한과 루트 체크도 문자열 감각으로 하면 터진다

  • 권한은 만든 뒤 고치는 게 아니라, 만들 때부터 맞게 만들어야 함

    • create_dir()로 기본 권한 디렉터리를 만든 다음 set_permissions(0o700)을 호출하면 짧은 시간이지만 넓은 권한 상태가 존재함
    • 다른 사용자가 그 틈에 open()으로 file descriptor를 잡으면, 나중에 chmod해도 이미 열린 핸들은 사라지지 않음
    • OpenOptions::mode()DirBuilderExt::mode()로 생성 시점 권한을 지정해야 함
  • --preserve-root 같은 보호 로직을 문자열 비교로 구현하면 우회가 쉬움

    • 기존 chmod 체크는 file == Path::new("/") 형태였음
    • /../, /./, /usr/.., /를 가리키는 심볼릭 링크는 문자열은 다르지만 실제 대상은 루트임
    • 수정은 canonicalize로 실제 경로를 해석한 뒤 /인지 비교하는 쪽으로 갔음
  • 파일 정체성은 문자열이 아니라 (dev, inode)로 봐야 할 때가 많음

    • 루트 디렉터리는 부모가 없어 canonicalize 비교가 비교적 안전하지만, 일반 경로 비교는 여전히 공격자 교체 문제가 있음
    • GNU coreutils처럼 양쪽을 열고 device와 inode를 비교해야 “같은 파일인가”에 가까워짐
    • CVE-2026-35363처럼 rm .rm ..은 거부하면서 rm ./, rm .///은 받아 현재 디렉터리를 지우는 웃픈 버그도 나옴

Unix 경계에서는 String이 아니라 byte로 살아야 한다

  • Rust의 String&str은 항상 UTF-8이지만, Unix의 경로와 입력 스트림은 그렇지 않음

    • 경로, 환경변수, 인자, cut, comm, tr 같은 도구의 입력은 raw byte 세계에 있음
    • from_utf8_lossy는 invalid byte를 U+FFFD로 바꿔서 조용히 데이터를 망가뜨림
    • unwrap이나 ?로 엄격 변환하면 유효하지 않은 UTF-8에서 크래시하거나 작업을 거부함
  • CVE-2026-35346의 comm 버그가 대표적임

    • GNU comm은 바이너리 파일도 byte 그대로 처리함
    • uutils 버전은 String::from_utf8_lossy로 출력하면서 유효하지 않은 byte를 치환해버렸음
    • 수정은 BufWriterwrite_all로 raw byte를 stdout에 직접 쓰는 방식이었음
  • 시스템 코드에서는 타입 선택이 곧 보안 선택임

    • 파일시스템 경로는 PathPathBuf, 환경변수는 OsString, 스트림 내용은 Vec<u8>&[u8]가 맞는 경우가 많음
    • 보기 편하다고 String으로 왕복시키는 순간 손상, 거부, panic 중 하나가 끼어들 수 있음
    • UTF-8은 앱 문자열 기본값으로는 좋지만 Unix 도구의 raw byte 기본값으로는 틀릴 수 있음

panic과 버려진 에러는 운영 장애가 된다

  • CLI에서 unwrap, expect, 인덱싱, unchecked arithmetic은 전부 잠재적 서비스 거부임

    • 공격자가 입력을 만들 수 있다면 panic은 프로세스를 죽임
    • cron job, CI 파이프라인, 셸 스크립트 안에서 죽으면 전체 작업 흐름이 멈춤
    • sort --files0-from은 NUL로 구분된 파일명 목록을 읽으면서 UTF-8 변환에 expect()를 써서 non-UTF-8 파일명에서 panic이 났음
  • bad input은 panic이 아니라 에러로 바꿔야 함

    • ?, get, checked_*, try_from 같은 패턴을 써서 호출자가 처리할 수 있는 실패로 올려야 함
    • Clippy에서 unwrap_used, expect_used, panic, indexing_slicing, arithmetic_side_effects를 경고로 두는 베이스라인도 제안됨
    • 테스트 코드에서는 panic이 자연스러우니 범위를 잘 나눠야 함
  • 에러를 버리는 것도 실제 CVE로 이어짐

    • chmod -R, chown -R이 마지막 파일의 exit code만 반환해 중간 실패를 숨기는 문제가 있었음
    • ddset_len() 결과를 .ok()로 버렸다가 디스크가 꽉 찬 상황에서 반쪽짜리 출력 파일을 조용히 만들 수 있었음
    • .ok(), .unwrap_or_default(), let _ =를 쓸 때는 왜 그 실패를 무시해도 안전한지 설명이 필요함

💡

> 시스템 Rust 코드 리뷰를 한다면 from_utf8_lossy, unwrap(), expect(), File::create, 버려진 Result, "/" 문자열 비교부터 grep해도 꽤 많은 냄새를 잡을 수 있음.

재구현에서는 예쁜 의미보다 호환성이 안전이다

  • GNU coreutils와 다르게 동작하는 것 자체가 취약점이 될 수 있음

    • kill -1에서 GNU는 -1을 “signal 1”로 읽고 PID를 요구함
    • uutils는 이를 “기본 signal을 PID -1에 보내기”로 해석했고, Linux에서 PID -1은 볼 수 있는 모든 프로세스를 의미함
    • 오타 하나가 시스템 전체 kill switch가 되는 셈임
  • battle-tested 도구를 다시 만들 땐 bug-for-bug 호환성도 보안 기능임

    • 셸 스크립트는 exit code, 에러 메시지, 옵션 의미, edge case에 의존함
    • “이쪽이 더 깔끔한 의미론인데?”라고 바꾼 부분에서 누군가의 운영 스크립트가 잘못된 결정을 할 수 있음
    • uutils가 GNU coreutils upstream test suite를 CI에서 돌리기 시작한 건 이 문제에 맞는 방어 규모임
  • chroot의 CVE-2026-35368은 단일 버그 중 가장 심각한 사례로 소개됨

    • 코드 흐름은 chroot(new_root) 이후 get_user_by_name(name)을 호출하고, 그 다음 setgid, setuid, exec를 하는 구조였음
    • 문제는 get_user_by_name이 NSS를 통해 새 루트 파일시스템의 동적 라이브러리를 로드할 수 있다는 점임
    • 공격자가 chroot 안에 파일을 심어둘 수 있으면 uid 0 상태에서 코드 실행이 가능해짐
    • GNU chroot처럼 사용자 정보를 chroot 전에 해석하는 게 수정 방향임
  • 결론은 Rust 시스템 코드의 안전 경계가 더 선명해졌다는 쪽에 가까움

    • Rust 덕분에 C coreutils에서 반복되던 메모리 취약점 종류는 상당히 사라짐
    • 대신 남은 버그는 경로, byte, syscall, 권한, 호환성처럼 OS와 만나는 경계에 몰림
    • 좋은 Rust는 borrow checker를 통과하는 코드가 아니라, 실행 환경의 지저분함을 타입과 흐름에 정직하게 반영하는 코드라는 얘기임

기술 맥락

  • uutils의 선택은 GNU coreutils를 Rust로 재구현해 메모리 안전성을 크게 끌어올리는 거였어요. 왜냐면 C로 된 오래된 시스템 도구들은 버퍼 오버플로, use-after-free 같은 취약점 이력이 워낙 많았고, Rust는 이 범주를 구조적으로 줄여주거든요.

  • 하지만 Unix 도구는 메모리 안에서만 사는 프로그램이 아니에요. 파일시스템 이름은 syscall마다 다시 해석되고, 경로는 공격자가 바꿀 수 있고, 입력은 UTF-8이 아닐 수 있어요. Rust 타입이 안전해도 외부 세계의 의미는 사람이 모델링해야 해요.

  • Path 대신 file descriptor에 고정하거나, String 대신 byte와 OsStr을 쓰는 건 덜 예뻐 보여도 더 정직한 설계예요. 왜냐면 커널이 실제로 다루는 단위가 문자열 값이 아니라 열린 핸들, inode, raw byte인 경우가 많기 때문이에요.

  • GNU 호환성을 테스트하는 것도 단순 품질 관리가 아니에요. coreutils는 수많은 자동화 스크립트의 암묵적 API라서 exit code 하나가 달라져도 보안 판단이나 배포 흐름이 틀어질 수 있거든요.

  • 그래서 이 글의 체크리스트는 Rust 팀에도 바로 쓸 수 있어요. TOCTOU, UTF-8 변환, panic, 버려진 Result, chroot 전후 해석 순서처럼 컴파일러가 안 잡는 영역을 리뷰 룰과 CI lint로 따로 관리해야 해요.

Rust를 쓰면 C식 메모리 취약점은 크게 줄지만, ‘운영체제와 만나는 경계’는 여전히 사람이 설계해야 한다. 이 글은 Rust 비판이라기보다, 프로덕션 시스템 Rust에서 진짜 리뷰해야 할 체크리스트에 가깝다.

댓글

댓글

댓글을 불러오는 중...

security

GitHub 내부 Git 인프라 RCE 취약점, git push 한 번으로 서버 실행까지 갔다

Wiz Research가 GitHub 내부 Git 처리 파이프라인에서 CVE-2026-3854 원격 코드 실행 취약점을 발견했다. 세미콜론으로 구분되는 내부 `X-Stat` 헤더에 사용자 입력이 그대로 들어가면서 보안 필드가 덮어써졌고, GHES는 전체 서버 장악, GitHub.com은 공유 스토리지 노드 접근으로 이어질 수 있었다.

security

캐나다 정부, 사기 막겠다며 가상자산 현금입출금기 금지 추진

캐나다 연방정부가 사기 피해를 줄이기 위해 가상자산 현금입출금기 금지를 추진 중이다. 현금만 넣으면 빠르게 비트코인 같은 가상자산으로 바꿔 해외 지갑으로 보낼 수 있는 구조가 사기범에게 너무 좋은 도구가 됐다는 판단이다.

security

Bitwarden CLI npm 패키지에 악성코드 — Checkmarx 공급망 공격 확산 중

Socket 보안팀이 Bitwarden CLI npm 패키지(@bitwarden/cli2026.4.0)가 악성코드에 감염된 채로 배포된 사실을 발견했다. Bitwarden의 CI/CD에 쓰이던 GitHub Action이 뚫려 공식 빌드 라인에 bw1.js 페이로드가 심어졌고, GitHub·AWS·Azure·GCP 자격증명부터 Claude/MCP 설정까지 광범위하게 탈취한다. 최근 번지고 있는 Checkmarx 공급망 캠페인의 일부로 확인됐다.

security

구글에 인수된 위즈, Security Graph 확장…AWS·Azure·Salesforce 에이전트까지 한 화면

구글 클라우드가 인수한 위즈(Wiz)가 클라우드 넥스트 2026에서 Security Graph 확장을 발표했다. 데이터브릭스, AWS Agentcore, Azure Copilot Studio, Salesforce Agentforce 등 멀티 에이전트 스튜디오를 단일 그래프에서 통합 감시한다. AI 에이전트끼리 직접 통신하는 위협 모델 변화에 대응한 개편이다.

security

애플, FBI가 삭제된 시그널 메시지 복원에 쓰던 알림 캐시 버그 패치

애플이 푸시 알림 본문이 기기 내부 DB에 최대 한 달간 캐시되던 버그를 수정했다. FBI가 이 캐시를 통해 시그널에서 자동 삭제된 메시지까지 포렌식으로 복원했다는 404 Media 보도가 발단이 됐고, iOS 18까지 백포트 패치가 배포됐다.