# Rust가 잡지 못하는 버그들

> Clean Markdown view of GeekNews topic #29027. Use the original source for factual precision when an external source URL is present.

## Metadata

- GeekNews HTML: [https://news.hada.io/topic?id=29027](https://news.hada.io/topic?id=29027)
- GeekNews Markdown: [https://news.hada.io/topic/29027.md](https://news.hada.io/topic/29027.md)
- Type: GN+
- Author: [neo](https://news.hada.io/@neo)
- Published: 2026-04-30T10:06:57+09:00
- Updated: 2026-04-30T10:06:57+09:00
- Original source: [corrode.dev](https://corrode.dev/blog/bugs-rust-wont-catch/)
- Points: 1
- Comments: 1

## Topic Body

- **메모리 안전성**은 크게 개선되지만, Rust 프로덕션 코드에서도 **시스템 경계 처리** 문제는 그대로 남아 취약점으로 이어질 수 있음
- 같은 경로를 여러 syscall에서 다시 해석하는 흐름, 생성 뒤 권한을 바꾸는 방식, **문자열 기반 경로 비교**는 TOCTOU와 권한 노출 같은 문제를 만들기 쉬움
- Unix에서는 경로, 환경 변수, 스트림 데이터가 **원시 바이트**로 오가므로 `String` 중심 처리나 `from_utf8_lossy`, `unwrap`, `expect`는 데이터 훼손이나 **DoS**로 이어질 수 있음
- 오류를 버리면 실패가 성공처럼 보일 수 있고, GNU coreutils와의 **동작 차이**도 셸 스크립트와 privileged 도구에서 곧바로 보안 문제로 이어질 수 있음
- 이번 감사에서는 **buffer overflow**, use-after-free, double-free 같은 메모리 안전성 계열 버그는 나오지 않았고, 남은 핵심 위험은 Rust 내부보다 **외부 세계와 맞닿는 경계**에 집중돼 있었음

---

### 감사에서 드러난 Rust의 한계
- Canonical이 공개한 uutils의 **44개 CVE**는 Rust 프로덕션 코드에서도 **borrow checker**, clippy, cargo audit가 잡지 못하는 취약점이 남을 수 있음을 보여줌
- 문제의 중심은 메모리 안전성보다 **시스템 경계 처리**에 있었음
  - 경로와 syscall 사이 시간차가 있었음
  - Unix 바이트 데이터와 UTF-8 문자열이 어긋났음
  - 원본 도구와의 동작 차이가 있었음
  - 오류 처리 누락과 `panic!` 종료가 있었음
- 이 CVE 목록은 Rust 시스템 코드에서 **안전성이 끝나는 지점**을 압축해 보여줌

### 경로를 두 번 해석하면 TOCTOU가 생김
- 같은 경로를 한 syscall에서 확인하고 다음 syscall에서 다시 작업하면 **TOCTOU** 취약점으로 이어지기 쉬움
  - 두 호출 사이에 상위 디렉터리에 쓰기 권한이 있는 공격자가 경로 구성 요소를 **심볼릭 링크**로 바꿀 수 있음
  - 두 번째 호출에서 커널이 경로를 처음부터 다시 해석하면서 권한 있는 작업이 공격자가 고른 대상으로 향하게 됨
- Rust의 `std::fs` API는 **`&Path` 기반 재해석**을 기본으로 두어 이런 실수를 만들기 쉬움
  - [`fs::metadata`](https://doc.rust-lang.org/std/fs/fn.metadata.html), [`File::create`](https://doc.rust-lang.org/std/fs/struct.File.html#method.create), [`fs::remove_file`](https://doc.rust-lang.org/std/fs/fn.remove_file.html), [`fs::set_permissions`](https://doc.rust-lang.org/std/fs/fn.set_permissions.html)는 호출 때마다 경로를 다시 해석함
  - 로컬 공격자를 막아야 하는 privileged 도구에서는 이런 기본 경로가 위험해짐
- `CVE-2026-35355`에서는 파일 삭제 뒤 같은 경로에 새 파일을 만드는 흐름이 악용됨
  - [`src/uu/install/src/install.rs`](https://github.com/uutils/coreutils/pull/10067/files)에서 `fs::remove_file(to)?` 뒤 `File::create(to)?`가 이어졌음
  - 삭제와 생성 사이에 `to`가 `/etc/shadow` 같은 대상을 가리키는 심볼릭 링크로 바뀌면 권한 있는 프로세스가 그 파일을 덮어쓸 수 있음
- 수정은 [`OpenOptions::create_new(true)`](https://doc.rust-lang.org/std/fs/struct.OpenOptions.html#method.create_new)을 써서 **새 파일만 생성**하도록 바뀜
  - 문서상 `create_new`는 대상 위치에 기존 파일뿐 아니라 **dangling symlink**도 허용하지 않음
- 같은 경로에 두 번 작업해야 하면 **파일 디스크립터에 고정**하는 쪽이 안전함
  - 새 파일 생성 외의 경우에는 부모 디렉터리를 한 번 열고 그 핸들 기준 상대 경로로 작업하는 편이 맞음
  - 같은 경로에 두 번 작업하면 반증되기 전까지 TOCTOU로 봐야 함

### 권한은 생성 후 수정하지 말고 생성 시점에 정해야 함
- 디렉터리나 파일을 기본 권한으로 만든 뒤 나중에 `chmod`하는 흐름도 **짧은 노출 구간**을 만듦
  - `fs::create_dir(&path)?` 후 `fs::set_permissions(&path, Permissions::from_mode(0o700))?`처럼 작성하면 그 사이 `path`가 기본 권한으로 존재함
  - 다른 사용자는 그 창구간 동안 `open()`할 수 있고, 이후 `chmod`를 해도 이미 얻은 파일 디스크립터는 회수되지 않음
- 권한은 **생성 시점에 함께 지정**해야 함
  - `OpenOptions::mode()`와 [`DirBuilderExt::mode()`](https://doc.rust-lang.org/std/os/unix/fs/trait.DirBuilderExt.html#tymethod.mode)를 사용해 원하는 권한으로 태어나게 해야 함
  - 커널은 여기에 `umask`를 추가로 적용하므로, 그 영향까지 중요하면 `umask`도 명시적으로 다뤄야 함

### 경로 문자열 비교는 파일시스템 동일성이 아님
- `chmod`의 초기 `--preserve-root` 검사는 **문자열 비교**만 했음
  - `recursive && preserve_root && file == Path::new("/")`
  - `/../`, `/./`, `/usr/..`, `/`를 가리키는 심볼릭 링크처럼 실제로는 루트를 가리키지만 문자열이 `/`가 아닌 입력은 이 검사를 우회함
- 수정은 [`fs::canonicalize`](https://doc.rust-lang.org/std/fs/fn.canonicalize.html)로 경로를 실제 절대 경로로 해석한 뒤 비교하는 방식으로 바뀜
  - [수정 PR](https://github.com/uutils/coreutils/pull/10033)
  - `canonicalize`는 `..`, `.`, 심볼릭 링크를 해결한 실제 경로를 돌려줌
- `--preserve-root`의 경우 `/`는 부모 디렉터리가 없어 이 방식이 통함
- 두 임의 경로가 같은 파일시스템 객체인지 일반적으로 비교하려면 **문자열이 아니라 `(dev, inode)`** 를 비교해야 함
  - GNU coreutils도 이런 방식으로 처리함
- `CVE-2026-35363`에서는 `rm`이 `.`과 `..`는 거부하면서도 `./`, `.///`는 허용해 현재 디렉터리를 지울 수 있었음
  - 입력 형태 차이를 문자열 수준에서만 다루면 검사가 쉽게 비껴감

### Unix 경계에서는 문자열보다 바이트를 우선해야 함
- Rust의 `String`과 `&str`은 항상 **UTF-8**이지만, Unix의 경로·환경 변수·인자·스트림 데이터는 **원시 바이트** 세계에 있음
- 이 경계를 넘을 때 잘못된 선택은 두 부류의 버그로 이어짐
  - [`from_utf8_lossy`](https://doc.rust-lang.org/std/string/struct.String.html#method.from_utf8_lossy) 같은 **손실 변환**은 잘못된 바이트를 `U+FFFD`로 바꿔 조용히 데이터를 훼손함
  - `unwrap`이나 `?` 같은 **엄격 변환**은 입력을 거부하거나 프로세스를 종료시킬 수 있음
- `comm`의 `CVE-2026-35346`은 손실 변환으로 출력이 망가진 경우였음
  - [`src/uu/comm/src/comm.rs`](https://github.com/uutils/coreutils/pull/10206/files)에서 입력 바이트 `ra`, `rb`를 `String::from_utf8_lossy`로 바꿔 `print!`했음
  - GNU `comm`은 바이너리 파일에서도 바이트를 그대로 옮기지만, uutils는 유효하지 않은 UTF-8을 `U+FFFD`로 바꿔 **출력을 손상**시켰음
  - 수정은 `BufWriter`와 `write_all`로 raw bytes를 그대로 `stdout`에 쓰는 방식이었음
- [`print!`](https://doc.rust-lang.org/std/macro.print.html)는 [`Display`](https://doc.rust-lang.org/std/fmt/trait.Display.html)를 거치며 **UTF-8 왕복**을 강제하지만, [`Write::write_all`](https://doc.rust-lang.org/std/io/trait.Write.html#method.write_all)은 그렇지 않음
- Unix 계열 시스템 코드에서는 상황에 맞는 타입을 써야 함
  - 파일 경로에는 [`Path`](https://doc.rust-lang.org/std/path/struct.Path.html), [`PathBuf`](https://doc.rust-lang.org/std/path/struct.PathBuf.html)
  - 환경 변수에는 [`OsString`](https://doc.rust-lang.org/std/ffi/struct.OsString.html)
  - 스트림 내용에는 `Vec&lt;u8&gt;` 또는 `&[u8]`
- 포매팅 편의를 위해 `String`을 경유하면 **데이터 훼손**이 스며들기 쉬움

### 모든 panic은 서비스 거부로 이어질 수 있음
- CLI에서 `unwrap`, `expect`, 슬라이스 인덱싱, 검사 없는 산술, [`from_utf8`](https://doc.rust-lang.org/std/str/fn.from_utf8.html)는 공격자가 입력을 조절할 수 있을 때 **DoS 지점**이 될 수 있음
  - `panic!`은 스택을 unwind하고 프로세스를 중단시킴
  - cron job, CI pipeline, shell script에서 실행 중이면 전체 작업이 멈출 수 있음
  - 반복 실행 환경에서는 crash loop로 시스템 전체를 마비시킬 수도 있음
- `sort --files0-from`의 [`CVE-2026-35348`](https://ubuntu.com/security/CVE-2026-35348)은 NUL 구분 파일명 목록에서 **비 UTF-8 파일명**을 만나면 중단됐음
  - 파서는 각 이름 바이트에 `std::str::from_utf8(bytes).expect(...)`를 호출했음
  - GNU `sort`는 파일명을 커널과 마찬가지로 raw bytes로 다루지만, uutils는 UTF-8을 강제하면서 첫 비 UTF-8 경로에서 전체 프로세스를 중단시켰음
- 신뢰할 수 없는 입력을 처리하는 코드에서는 `unwrap`, `expect`, 인덱싱, `as` cast를 **잠재 CVE**로 봐야 함
  - `?`, `get`, `checked_*`, `try_from`을 쓰고 실제 오류를 호출자에게 올려야 함
- CI에서 잡기 위한 clippy 기준도 제시됨
  - `unwrap_used`
  - `expect_used`
  - `panic`
  - `indexing_slicing`
  - `arithmetic_side_effects`
- 테스트 코드에서는 이런 경고가 과도할 수 있어 `cfg(test)` 범위에서 제한하는 방식이 적절함

### 오류를 버리면 실패가 성공처럼 보일 수 있음
- 일부 CVE는 오류를 무시하거나 **오류 정보가 사라지는 흐름**에서 나왔음
- `chmod -R`와 `chown -R`는 전체 작업 중 **마지막 파일의 종료 코드**만 반환했음
  - 앞선 다수 파일 처리에 실패해도 마지막 파일이 성공하면 `0`으로 끝날 수 있음
  - 스크립트는 전체 작업이 문제없이 끝난 것으로 오판하게 됨
- `dd`는 `/dev/null`에서 GNU 동작을 흉내 내기 위해 [`set_len()`](https://doc.rust-lang.org/std/fs/struct.File.html#method.set_len) 결과에 [`Result::ok()`](https://doc.rust-lang.org/std/result/enum.Result.html#method.ok)를 호출했음
  - 의도는 제한된 상황에서 오류를 버리는 것이었지만 같은 코드가 일반 파일에도 적용됐음
  - 디스크가 가득 찬 경우에도 **반쯤만 써진 목적 파일**이 조용히 남을 수 있었음
- `.ok()`, [`.unwrap_or_default()`](https://doc.rust-lang.org/std/result/enum.Result.html#method.unwrap_or_default), `let _ =`로 `Result`를 버리면 중요한 실패 원인이 사라짐
- 첫 실패에서 바로 중단하지 않더라도 **가장 심한 오류 코드**를 기억해 종료해야 함
- 꼭 `Result`를 버려야 하면 그 실패를 왜 안전하게 무시할 수 있는지 **이유를 코드에 남겨야 함**

### 원본 도구와의 정확한 호환성도 안전 기능임
- 여러 CVE는 코드가 위험한 연산을 해서가 아니라 **GNU와 다르게 동작**해 생겼음
  - 실제 셸 스크립트는 원본 GNU 동작에 의존하고 있어, 의미 차이가 보안 문제로 이어짐
- `kill -1`의 `CVE-2026-35369`가 대표적임
  - GNU는 `-1`을 **signal 1**로 읽고 PID를 요구함
  - uutils는 이를 **PID -1에 기본 시그널 전송**으로 해석했음
  - Linux에서 PID -1은 볼 수 있는 모든 프로세스를 뜻하므로 단순한 오타가 시스템 전체 kill로 이어질 수 있음
- 재구현 도구에서는 **bug-for-bug 호환성**이 출구 코드, 오류 메시지, edge case, 옵션 의미까지 포함한 안전 장치가 됨
- GNU와 다른 동작이 있는 지점마다 셸 스크립트가 **잘못된 판단**을 내릴 가능성이 커짐
- uutils는 이제 CI에서 **upstream GNU coreutils 테스트 스위트**를 함께 돌림
  - 이런 종류의 차이를 막기 위한 방어 규모로 적절해 보임

### 신뢰 경계를 넘기 전에 먼저 해석해야 함
- [`CVE-2026-35368`](https://ubuntu.com/security/CVE-2026-35368)은 `chroot`의 **local root code execution**이었음
- 문제 패턴은 `chroot(new_root)?` 후 공격자가 제어하는 새 루트 안에서 사용자 이름을 해석한 데 있었음
  - `get_user_by_name(name)?`가 새 루트 파일시스템의 공유 라이브러리를 읽어 사용자 이름을 해석하게 됨
  - 공격자가 chroot 내부에 파일을 심어두면 **uid 0 코드 실행**으로 이어질 수 있음
- GNU `chroot`는 사용자 해석을 **`chroot` 이전**에 수행함
  - 수정도 같은 순서로 바뀜
- 신뢰 경계를 한 번 넘은 뒤에는 라이브러리 호출 하나하나가 공격자 코드를 실행시킬 수 있음
- 정적 링크도 이 문제를 막지 못함
  - `get_user_by_name`은 NSS를 거치며 런타임에 `libnss_*` 모듈을 `dlopen`하기 때문임

### Rust가 실제로 막아낸 버그들
- 이번 감사에서 발견되지 않은 버그 종류도 분명함
  - **buffer overflow**는 없었음
  - **use-after-free**도 없었음
  - **double-free**도 없었음
  - 공유 가변 상태의 **data race**도 없었음
  - **null-pointer dereference**도 없었음
  - **uninitialized memory read**도 없었음
- 도구에 버그가 있더라도 임의 메모리 읽기로 악용될 수 있는 종류는 감사 결과에 나오지 않았음
- GNU coreutils는 최근 몇 년간 이런 **메모리 안전성 계열 CVE**를 계속 내왔음
  - `pwd` deep path buffer overflow
  - `numfmt` out-of-bounds read
  - `unexpand --tabs` heap buffer overflow
  - `od --strings -N` heap buffer 바깥 NUL 쓰기
  - `sort` heap buffer 앞 1바이트 read
  - `split --line-bytes` heap overwrite인 [CVE-2024-0684](https://nvd.nist.gov/vuln/detail/CVE-2024-0684)
  - `b2sum --check` malformed input에서 unallocated memory read
  - `tail -f` stack buffer overrun
- 같은 기간 비교에서 Rust 재구현은 이런 범주의 버그를 **0건**으로 유지했음
  - 단, 감사가 메모리 안전성 버그 부재를 증명한 것은 아니고 발견하지 못했을 뿐이라는 단서도 붙음
- 남은 문제는 Rust 내부보다 **외부 세계와 맞닿는 경계**에서 주로 생김
  - 경로
  - 바이트와 문자열
  - syscall
  - 시간차와 파일시스템 상태 변화

### 올바른 Rust는 관용적 Rust이기도 함
- 관용적 Rust는 borrow checker를 통과하고 `clippy`가 조용한 코드에 그치지 않음
- **정확성**도 관용성의 일부여야 함
  - 현실에서 살아남는 코드 형태가 커뮤니티 경험을 통해 굳어졌기 때문임
- 견고한 시스템은 현실의 지저분함을 숨기기보다 **그대로 반영**해야 함
  - 경로 대신 파일 디스크립터
  - `String` 대신 `OsStr`
  - `unwrap` 대신 `?`
  - 더 깔끔해 보이는 의미보다 원본과의 bug-for-bug 호환성
- 타입 시스템은 많은 것을 표현할 수 있지만 **두 syscall 사이 시간 경과**처럼 통제 밖 조건까지 담아내지는 못함
- 관용적 Rust는 코드의 타입, 이름, 제어 흐름이 **실행 환경의 진실**을 드러내야 함
  - 보기 좋은 화이트보드 코드보다 덜 예쁘더라도 더 정직한 형태가 필요함

### 참고 자료
- [An update on rust-coreutils](https://discourse.ubuntu.com/t/an-update-on-rust-coreutils/80773): 감사 결과 공개
- [Patterns for Defensive Programming in Rust](https://corrode.dev/blog/defensive-programming/): 함께 읽을 수 있는 방어적 Rust 패턴
- [Pitfalls of Safe Rust](https://corrode.dev/blog/pitfalls-of-safe-rust/): safe Rust에서도 생길 수 있는 흔한 실수
- [Sharp Edges In The Rust Standard Library](https://corrode.dev/blog/sharp-edges-in-rust-std/): `std`의 의외의 동작
- [uutils/coreutils on GitHub](https://github.com/uutils/coreutils): Rust로 재구현한 GNU coreutils

## Comments



### Comment 56586

- Author: neo
- Created: 2026-04-30T10:07:04+09:00
- Points: 1

###### [Hacker News 의견들](https://news.ycombinator.com/item?id=47943499) 
- **GNU Coreutils** 유지보수자로서 글은 흥미롭게 읽었지만, 내가 조금 써본 Rust에서는 `std::fs`로 **TOCTOU race**를 만들기가 너무 쉬웠음  
  `openat` 비슷한 API가 표준 라이브러리에 결국 들어가길 바람
  
  그리고 **경로를 비교하기 전에 resolve하라**는 규칙에는 동의하지 않음  
  일반적으로는 `fstat`를 호출해서 `st_dev`와 `st_ino`를 비교하는 편이 더 낫고, 글에도 그 점은 일부 들어가 있었음
  
  덜 자주 고려되는 부작용은 **성능 비용**임  
  실제 예시로 아주 깊은 디렉터리 경로에서 `cp`는 0.010초인데 `uu_cp`는 12.857초가 걸렸음
  
  현실에서 이런 경로를 일부러 만드는 일은 드물겠지만, GNU 소프트웨어는 임의의 한계를 피하려고 매우 강하게 노력함  
  [https://www.gnu.org/prep/standards/standards.html#Semantics](https://www.gnu.org/prep/standards/standards.html#Semantics)
  
  그리고 글에서 Rust 재작성은 비슷한 기간 동안 **메모리 안전성 버그**가 0개라고 했는데, 그건 사실이 아님 :)  
  [https://github.com/advisories/GHSA-w9vv-q986-vj7x](https://github.com/advisories/GHSA-w9vv-q986-vj7x)
  - 맞음, `std::fs`는 **lowest common denominator** 문제를 안고 있음  
    Rust 1.0에 뭔가는 넣어야 했고, 안타깝게도 그 상태가 오래 굳어졌음
    
    `uutils`는 더 실수하기 어려운 **std::fs 대체 API**를 설계해 보기 좋은 장소라고 봄
  - 반대편 입장에서 이 관점을 이렇게 간결하게 설명해줘서 고마움
    
    여기서 뭘 배워야 하는지 묻고 싶음  
    인터넷 글치고는 일부러 아주 공격적으로 묻는 편인데, 대비가 있어야 차이와 실수를 더 또렷하게 볼 수 있어서 그럼  
    물론 시간이나 정신적 에너지를 써줄 의무는 전혀 없음
    
    왜 자꾸 **속도**, **성능**, race condition, `st_ino`가 같이 따라오는지 궁금함  
    지연시간, 실제 저장장치에 쓰는 일, 원자성, ACID, 유한한 정보 전달 속도 같은 것들이 결국 비슷한 본질로 수렴하는 듯 보임  
    회계 같은 신뢰성 높은 시스템은 결국 ACID로 가야 하는 것 같고, 신뢰성이 낮은 시스템은 너무 빨리 잊혀져서 컴퓨터의 차이가 크지 않은 것처럼 느껴지기도 함
    
    또 일상적인 애플리케이션에서는 **throughput**이 정말 **latency**보다 더 중요한지도 궁금함
    
    그리고 C, Unix 계열 OS, GNU coreutils의 역사 때문에 inode 번호에 초점을 맞추는 건 이해가 가지만,  
    아주 기본적인 예로 USB 메모리를 파일 저장용으로 그냥 제대로 동작하게 만드는 문제를 보면 어떨지 궁금함  
    `libc` I/O 버퍼링, `fflush`, 커널 버퍼링, 멀티코어, 시분할, 여러 애플리케이션 동시 실행 같은 복잡성을 피하지 않고도 말임
  - 완전 초보라서 그런데, 왜 그냥 `$(yes a/ | head -n $((32 * 1024)) | tr -d '\n')`로 바로 `cd`하지 않고 `while` 루프가 필요했는지 궁금했음
    
    수정: 이해했음. `-bash: cd: a/a/a/....../a/a/: File name too long` 때문이었음
  - 혹시 봤는지 모르겠지만, `wget` 같은 **GNU 유틸리티**를 메모리 안전한 **C++ subset**으로 자동 변환하는 데모가 있음  
    [https://duneroadrunner.github.io/scpp_articles/PoC_autotranslation_of_wget](https://duneroadrunner.github.io/scpp_articles/PoC_autotranslation_of_wget)
    
    위험한 C 요소를 동작이 대응되는 안전한 C++ 요소로 거의 1:1 치환하는 방식이라, 재작성처럼 새 버그와 새 동작 차이를 들여올 가능성이 더 낮아 보임
    
    원본 코드를 조금만 정리하면 변환은 완전 자동화될 수 있어서, 빌드 단계에서 원래 C 소스로부터 약간 느리지만 메모리 안전한 실행 파일을 만들 수 있음
  - 좀 멍청한 질문일 수 있지만, **GNU Coreutils** 쪽에서 자체적인 **Rust 재작성**을 검토하거나 계획 중인지 궁금함

- Rust는 쓸 줄 알았겠지만, **Unix API**와 그 의미론, 함정에 충분히 익숙했던 건 아님  
  저 실수들 대부분은 오래된 GNU coreutils나 BSD, Solaris 기반 개발자 관점에선 꽤 초보적인 축에 들어감  
  그런 이슈는 수십 년 전에 이미 상당수 드러나고 정리됐고, 기존 코드베이스에는 지금도 긴 꼬리의 수정이 남아 있지만 이제는 대체로 소량만 계속 들어오는 수준임
  - 그 **Canonical 스레드**를 읽고 정말 기가 막혔음  
    요지는 대충 “Rust가 더 안전하고, 보안이 최우선이니, coreutils 전체 재작성 배포는 긴급하다. 뭔가 깨져도 괜찮고 나중에 고치면 된다”는 식이었음
    
    나는 그런 식으로 생각하는 사람들이 만든 코드를 내 머신에서 돌리고 싶지 않음  
    나도 Rust에는 찬성하지만, Rust가 더 안전하다는 건 **다른 조건이 같을 때만** 성립함  
    여기서는 다른 조건이 전혀 같지 않음
    
    재작성은 필연적으로 수십 년간 관리된 코드보다 **훨씬 더 많은 버그와 취약점**을 가질 수밖에 없어서, 보안 논리는 장기 전환 전략에서는 의미가 있어도 성급한 롤아웃의 근거는 못 됨
    
    배포 후에 사용자 영향은 별것 아니라고 축소하거나, “이렇게 해야 버그가 드러난다”, “기존 coreutils에도 제대로 된 테스트가 없었다”고 말하는 태도는 너무 무책임함  
    사용자는 실험용 쥐가 아님  
    유지보수자는 사용자 시스템의 신뢰성을 해치지 않을 **도덕적 책임**이 있다고 봄
  - 그보다 더 근본적으로, Rust 표준 라이브러리가 개발자를 **잘못된 추상화 수준**의 깔끔한 API로 유도하는 것처럼 보임  
    예를 들면 핸들 기반 파일 작업 대신 경로 기반 작업 쪽으로 말임  
    내가 틀렸길 바람
  - Rust의 요점은 가장 크고, 가장 쉽게 빠지는 함정은 굳이 신경 쓰지 않아도 되게 만드는 데 있다고 봄
    
    이 글의 핵심도 결국 **파일 시스템 API**가 그런 역할을 해야 한다는 데 있는 듯함
  - 누군가 이와 비슷한 표현으로 **disassembler rage**라는 말을 만든 적이 있음  
    충분히 가까이서 들여다보면 모든 실수는 아마추어처럼 보인다는 뜻임
    
    디스어셈블러만 들여다보며, 콜스택 100프레임 아래 함수 안에서 왜 `switch` 대신 `if`를 썼냐고 고수준 프로그래머를 욕하는 태도에서 나온 말이기도 함
    
    지금 우리는 그들이 틀린 몇 가지만 보고 있고, 그 주변의 수천 줄 **맞게 작성된 코드**는 거의 보지 않음
  - 이런 유틸리티에서 `panic`이 나는 건 Rust 기준으로 봐도 꽤 **아마추어한 실수**임  
    처리 불가능한 alloc 에러 같은 거라면 모르겠지만, `expect`와 `unwrap`은 그 코드 경로가 절대 실행되지 않게 하는 불변식을 정말 엄격히 보장하는 경우가 아니면 변명하기 어려움

- 코드를 재작성할 때 어려운 점 중 하나는, 원래 코드는 실제 운영 환경에서만 드러난 문제들에 대응하면서 **점진적으로 변형**돼 왔다는 데 있음
  
  그 과정에서 얻은 교훈이 코드 안에 조용히 스며들고, 문서화돼 있지 않으면 동등한 수준에 도달하기 전에 해야 할 **숨은 작업**이 엄청 많아짐
  
  원문은 바로 그런 종류의 리스트를 잘 보여줌
  
  그렇다고 곧바로 아마추어라고 부르기 전에, 이게 소프트웨어에서 가장 소프트웨어다운 현상 중 하나라는 점도 봐야 함  
  coreutils에 정말 좋은 기술 문서와 해당 케이스를 담은 테스트가 있었는데도 무시한 게 아니라면, 이런 일은 거의 필연이었음
  - 글에 나온 좋은 예가 **chroot + NSS CVE**임  
    NSS가 동적이고, `chroot` 내부에서 라이브러리를 `dlopen`한다는 규칙은 어디에도 눈에 띄게 적혀 있지 않음
    
    그건 25년 넘게 시스템 관리자들이 몸으로 겪으며 알아낸 사실에 가깝고, 클린룸 재작성은 그걸 대개 **새 CVE**로 다시 배움  
    같은 코드를 LLM으로 포팅해도 사정은 비슷함  
    함수 시그니처는 읽을 수 있지만, 정말 필요한 건 그 코드에 남은 **상처와 흉터**들임
  - **GPL**을 피하려고 원본 소스도 읽지 않은 채 이 작업을 한다면 더 어려워짐
    
    내 생각엔 `uutils`가 GPL이었고, **coreutils 원본 소스**에서 직접 영감을 받아도 됐다면 훨씬 나았을 것임
  - 이런 교훈이나 최소한 피하려 했던 버그와 취약점을 **문서화하지 않는 것**도 나쁜 관행이라는 점은 분명히 해야 함
    
    물론 애초에 코드를 잘 짜서 암묵적으로 피한 모든 버그를 전부 문서로 남기긴 어렵지만,  
    미래의 독자에게는 “여기서 `bar` 대신 `foo`를 쓰는 이유는 ABC 조건에서 `bar`를 쓰면 XYZ 때문에 위험한 `baz`가 생기기 때문” 같은 설명을 남기는 편이 중요함  
    시간과 문서 공간이 좀 낭비돼 보이더라도 그게 낫다고 봄

- 이 글에서 지적한 것들 중 상당수는, 특히 **GNU coreutils 소스**와 비교해 보면, 웬만한 unit test나 수동 리뷰에서 걸러졌어야 한다고 느낌  
  coreutils 재작성은 끔찍한 아이디어처럼 보이고  
  [https://www.joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i/](https://www.joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i/)  
  이전 소프트웨어가 쌓아온 지식을 충분히 가져오지 못한 채 잘못된 방식으로 진행된 듯함
  
  재작성을 하려면 전작을 완전히 이해하고 배워야 함  
  그렇지 않으면 같은 실수를 반복하게 되고, 솔직히 꽤 민망한 일임
  
  분명히 하자면 Rust는 좋아하고 여러 프로젝트에 쓰고 있으며 훌륭함  
  다만 Rust가 **나쁜 엔지니어링**까지 구해주진 못함
  - 흥미롭게도 `uutils`는 **GNU coreutils 테스트 스위트**를 사용함
    
    덧붙이면 GPL 소스를 읽고 작성한 기여는 받지 않겠다는 입장도 명시해 둔 상태임
  - `unity`, `upstart`, `snap`을 만든 쪽이라면 딱 이런 일도 예상 범위 안임
  - 새 시스템 프로그래머들에게는 이런 환영 인사를 해야 할 듯함  
    **Unix는 망가져 있고**, 결국 못생기고 교육적이지도 않은 우회책을 직접 써야 하며, **경험적 테스트**도 해야 함  
    신뢰할 수 있는 소프트웨어와 좋은 소프트웨어 엔지니어링은 원래 그런 식으로 굴러감

- 왜 **differential fuzzing**이 이런 버그를 못 잡았는지 궁금함
  
  [https://github.com/uutils/coreutils/tree/main/fuzz/uufuzz](https://github.com/uutils/coreutils/tree/main/fuzz/uufuzz)

- 경로에 대해 한 번 syscall로 확인하고, 같은 경로에 다시 syscall을 날려 작업하는 패턴은 늘 같은 문제를 부름  
  부모 디렉터리에 쓰기 권한이 있는 공격자는 그 사이에 경로 구성 요소를 **심볼릭 링크**로 바꿔치기할 수 있고, 커널은 두 번째 호출에서 경로를 처음부터 다시 resolve해서 권한 있는 작업이 공격자가 고른 대상으로 가게 됨
  - 사실은 이보다 더 나쁨  
    부모 디렉터리에 쓰기 권한이 있는 공격자는 **하드링크**로도 장난칠 수 있음  
    정규 파일 쪽만 건드릴 수 있다고 해도 사실상 마땅한 완화책이 거의 없음  
    예시는 [https://michael.orlitzky.com/articles/posix_hardlink_heartache.xhtml](https://michael.orlitzky.com/articles/posix_hardlink_heartache.xhtml) 참고
  - 음… 디렉터리에 **write lock**을 거는 방법이 있을지도 모르겠지만, timeout 같은 문제까지 얽히면 금방 더 복잡해질 듯함

- 몇몇 버그의 **근본 원인**은 Unix API가 너무 불투명하다는 데 있는 듯함
  
  예를 들어 `get_user_by_name`이 새 루트 파일시스템 안에서 공유 라이브러리를 로드해 사용자 이름을 해석하고, 그래서 `chroot` 안에 파일을 심을 수 있는 공격자가 uid 0으로 코드를 실행하게 된다는 건 거의 **부비트랩**처럼 느껴짐
  
  사용자 데이터를 얻는 함수가 갑자기 공유 라이브러리까지 로드하는 건 관심사가 섞인 설계로 보임  
  사용자 데이터 조회와 라이브러리 로딩을 함수 차원에서 분리하거나, 적어도 이름만 봐도 그런 동작이 드러나게 해야 한다고 봄
  - 일부는 그럴 수 있어도, coreutils를 바닥부터 다시 쓰기로 했다면 **POSIX API를 이해하는 것** 자체가 문자 그대로 핵심 업무임
    
    게다가 경로가 파일시스템 루트를 가리키는지 검사하는 코드가 `file == Path::new("/")`였다면, 그건 API 문제가 아님  
    그렇게 쓴 사람은 이 프로젝트에 참여할 자격이 거의 없어 보임
  - 오히려 **함수형 안전 언어**를 쓰면 다루는 데이터도 무상태라고 착각하게 만들 수 있다고 봄  
    하지만 운영체제에서는 정말 많은 것이 늘 바뀜
    
    스냅샷을 제공하는 파일시스템이 나오기 전까지는 모든 걸 **계속 다시 확인**해야 함
    
    결국 필요한 건 입력이 들어오면 성공한 결과 아니면 실패만 주는 API임  
    성공, 실패, 에러 셋 중 하나를 주는 API가 아님
  - 맞음, `musl libc`는 바로 그런 부분 하나를 제거함
  - 근본 원인은 Unix API의 불투명함보다, **root가 자신이 통제하지 못하는 디렉터리로 chroot**하는 상황을 제대로 생각하지 않은 데 있다고 봄
    
    무엇이든 `chroot`한 대상은 그 chroot를 만든 쪽의 통제 아래 있고, 그걸 이해하지 못하면 `chroot()`를 쓸 자격이 없음
    
    `get_user_by_name`이 함정처럼 느껴질 수는 있지만, 사실 `newroot/etc/passwd`를 쓰는 것과 `newroot/usr/lib/x86_64-linux-gnu/libnss_compat.so`, `newroot/bin/sh` 같은 걸 쓰는 것 사이에는 실질적 차이가 거의 없음
    
    그래서 `/usr/sbin/chroot`가 애초에 사용자 ID를 조회할 이유가 없다고 봄  
    `toybox chroot`는 그러지도 않음  
    결국 버그는 뭔가를 잘못한 방식이 아니라, **애초에 그 작업을 한 것 자체**였음
  - **Unix와 POSIX**는 프랙탈처럼 어디를 잘라도 함정투성이임

- Rust 쪽 사람들이 Linux 경험 없이 coreutils를 다시 썼다 치더라도, Ubuntu가 그걸 어떻게 **mainline**에 받아들였는지가 더 이해되지 않음
  - Ubuntu는 거의 매 릴리스마다 시스템의 기반 요소 하나쯤을 **엉성하고 미완성인 실험**으로 바꾸는 정책이라도 있는 듯함
    
    여기서 핵심은 “세상에, Rust 코드에 버그가 있었네”가 아니라 바로 그 점이라고 봄
  - 원본은 **GPL 라이선스**고, 재작성판은 **MIT 라이선스**임

- “이 버그들이 실제 배포된 Rust 코드에서 나왔고, 작성자들도 자기들이 뭘 하는지 아는 사람들이었다”는 말이 사실이라면,  
  원래 유틸리티에도 **테스트 하니스**가 없었고, 재작성도 그걸 먼저 만드는 데서 출발하지 않았다는 뜻인지 궁금함
  
  엣지 케이스가 많다 해도, OS와 FS를 어느 정도 추상화해서 `rm .//`가 정말 기대한 대로 현재 디렉터리를 지우지 않는지 같은 건 검증할 수 있지 않나 싶음
  
  이건 지저분한 코딩 문제나 언어 비판이라기보다, 또다시 **시스템 프로그래밍에서는 테스트 안 한다**는 오래된 태도처럼 보이기도 함
  
  반대로 원래 유틸리티에 테스트가 있었는데도 이렇게 빈틈이 많았다면, 원본 테스트 스위트 자체가 크게 부족한 것일 수도 있음
  - 그렇다고 봄
    
    하지만 OS와 FS를 충분히 추상화해서 검증할 수 있다는 데는 그만큼 확신하지 못하겠음  
    사람들이 내가 태어나기 전부터 그런 시도를 해왔지만 아직 성공하지 못한 듯하기 때문임
    
    예를 들어 `/`를 몇 개까지 붙여서 시험할지 어떻게 정할 건지부터 애매함
    
    더 나아가 `rm`이 파일의 처음 9바이트가 `important`면 삭제를 거부한다고 가정하면,  
    그 문자열을 미리 모르는 상태에서 그런 동작을 잡아내는 테스트를 어떻게 떠올릴지 난감함  
    심지어 그 마법 단어가 사전에 없는 문자열이라면 더 어려움
    
    나는 “시스템 프로그래밍은 테스트 안 한다”는 말을 진지하게 하는 사람은 거의 못 봤음  
    대신 테스트가 사람들이 기대하는 역할을 **항상 해주지는 않는다**는 말은 자주 들었음
  - 내가 이해한 바로는 `uutils` 개발 과정에는 원본 유틸리티와의 **광범위한 동작 비교 테스트**가 있었고, 심지어 버그까지 보존하려 했음
  - 이런 이유 중 하나 때문에 Windows는 기본적으로 **symlink를 비활성화**함  
    추상화로 푸는 게 아니라 기능 자체를 사실상 제거하는 방식임
    
    Unix 계열은 수십 년간 symlink에 의존한 소프트웨어가 너무 많아서 그렇게 할 수 없음
    
    MacOS도 비슷한 식의 대응이 있음  
    예를 들어 `chroot()` 버그는 기본 설정에선 실질적 문제가 잘 안 되는데, MacOS가 `chroot()`를 기본적으로 막기 때문임  
    쓰려면 system integrity protection을 꺼야 함
    
    근본 문제는 **POSIX API의 날카로운 모서리**에 있고, 해결책은 그걸 추상화하는 게 아니라 아예 **없애는 것**에 가까움

- 사람들이 실험하고 서툴게 시도하는 건 괜찮다고 봄  
  원래 그렇게 배우고 성장하니까
  
  진짜 궁금한 건 Ubuntu의 **의사결정 체인**이 어떻게 망가졌길래 이런 게 프로덕션까지 들어갔는지임
  - 가끔 성장이란 건 키만 커지는 일이기도 함
