Rust가 잡지 못하는 버그들
(corrode.dev)- 메모리 안전성은 크게 개선되지만, 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::fsAPI는&Path기반 재해석을 기본으로 두어 이런 실수를 만들기 쉬움fs::metadata,File::create,fs::remove_file,fs::set_permissions는 호출 때마다 경로를 다시 해석함- 로컬 공격자를 막아야 하는 privileged 도구에서는 이런 기본 경로가 위험해짐
CVE-2026-35355에서는 파일 삭제 뒤 같은 경로에 새 파일을 만드는 흐름이 악용됨src/uu/install/src/install.rs에서fs::remove_file(to)?뒤File::create(to)?가 이어졌음- 삭제와 생성 사이에
to가/etc/shadow같은 대상을 가리키는 심볼릭 링크로 바뀌면 권한 있는 프로세스가 그 파일을 덮어쓸 수 있음
- 수정은
OpenOptions::create_new(true)을 써서 새 파일만 생성하도록 바뀜- 문서상
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()를 사용해 원하는 권한으로 태어나게 해야 함- 커널은 여기에
umask를 추가로 적용하므로, 그 영향까지 중요하면umask도 명시적으로 다뤄야 함
경로 문자열 비교는 파일시스템 동일성이 아님
chmod의 초기--preserve-root검사는 문자열 비교만 했음recursive && preserve_root && file == Path::new("/")/../,/./,/usr/..,/를 가리키는 심볼릭 링크처럼 실제로는 루트를 가리키지만 문자열이/가 아닌 입력은 이 검사를 우회함
- 수정은
fs::canonicalize로 경로를 실제 절대 경로로 해석한 뒤 비교하는 방식으로 바뀜- 수정 PR
canonicalize는..,., 심볼릭 링크를 해결한 실제 경로를 돌려줌
--preserve-root의 경우/는 부모 디렉터리가 없어 이 방식이 통함- 두 임의 경로가 같은 파일시스템 객체인지 일반적으로 비교하려면 문자열이 아니라
(dev, inode)를 비교해야 함- GNU coreutils도 이런 방식으로 처리함
CVE-2026-35363에서는rm이.과..는 거부하면서도./,.///는 허용해 현재 디렉터리를 지울 수 있었음- 입력 형태 차이를 문자열 수준에서만 다루면 검사가 쉽게 비껴감
Unix 경계에서는 문자열보다 바이트를 우선해야 함
- Rust의
String과&str은 항상 UTF-8이지만, Unix의 경로·환경 변수·인자·스트림 데이터는 원시 바이트 세계에 있음 - 이 경계를 넘을 때 잘못된 선택은 두 부류의 버그로 이어짐
from_utf8_lossy같은 손실 변환은 잘못된 바이트를U+FFFD로 바꿔 조용히 데이터를 훼손함unwrap이나?같은 엄격 변환은 입력을 거부하거나 프로세스를 종료시킬 수 있음
comm의CVE-2026-35346은 손실 변환으로 출력이 망가진 경우였음src/uu/comm/src/comm.rs에서 입력 바이트ra,rb를String::from_utf8_lossy로 바꿔print!했음- GNU
comm은 바이너리 파일에서도 바이트를 그대로 옮기지만, uutils는 유효하지 않은 UTF-8을U+FFFD로 바꿔 출력을 손상시켰음 - 수정은
BufWriter와write_all로 raw bytes를 그대로stdout에 쓰는 방식이었음
print!는Display를 거치며 UTF-8 왕복을 강제하지만,Write::write_all은 그렇지 않음- Unix 계열 시스템 코드에서는 상황에 맞는 타입을 써야 함
- 포매팅 편의를 위해
String을 경유하면 데이터 훼손이 스며들기 쉬움
모든 panic은 서비스 거부로 이어질 수 있음
- CLI에서
unwrap,expect, 슬라이스 인덱싱, 검사 없는 산술,from_utf8는 공격자가 입력을 조절할 수 있을 때 DoS 지점이 될 수 있음panic!은 스택을 unwind하고 프로세스를 중단시킴- cron job, CI pipeline, shell script에서 실행 중이면 전체 작업이 멈출 수 있음
- 반복 실행 환경에서는 crash loop로 시스템 전체를 마비시킬 수도 있음
sort --files0-from의CVE-2026-35348은 NUL 구분 파일명 목록에서 비 UTF-8 파일명을 만나면 중단됐음- 파서는 각 이름 바이트에
std::str::from_utf8(bytes).expect(...)를 호출했음 - GNU
sort는 파일명을 커널과 마찬가지로 raw bytes로 다루지만, uutils는 UTF-8을 강제하면서 첫 비 UTF-8 경로에서 전체 프로세스를 중단시켰음
- 파서는 각 이름 바이트에
- 신뢰할 수 없는 입력을 처리하는 코드에서는
unwrap,expect, 인덱싱,ascast를 잠재 CVE로 봐야 함?,get,checked_*,try_from을 쓰고 실제 오류를 호출자에게 올려야 함
- CI에서 잡기 위한 clippy 기준도 제시됨
unwrap_usedexpect_usedpanicindexing_slicingarithmetic_side_effects
- 테스트 코드에서는 이런 경고가 과도할 수 있어
cfg(test)범위에서 제한하는 방식이 적절함
오류를 버리면 실패가 성공처럼 보일 수 있음
- 일부 CVE는 오류를 무시하거나 오류 정보가 사라지는 흐름에서 나왔음
chmod -R와chown -R는 전체 작업 중 마지막 파일의 종료 코드만 반환했음- 앞선 다수 파일 처리에 실패해도 마지막 파일이 성공하면
0으로 끝날 수 있음 - 스크립트는 전체 작업이 문제없이 끝난 것으로 오판하게 됨
- 앞선 다수 파일 처리에 실패해도 마지막 파일이 성공하면
dd는/dev/null에서 GNU 동작을 흉내 내기 위해set_len()결과에Result::ok()를 호출했음- 의도는 제한된 상황에서 오류를 버리는 것이었지만 같은 코드가 일반 파일에도 적용됐음
- 디스크가 가득 찬 경우에도 반쯤만 써진 목적 파일이 조용히 남을 수 있었음
.ok(),.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로 이어질 수 있음
- GNU는
- 재구현 도구에서는 bug-for-bug 호환성이 출구 코드, 오류 메시지, edge case, 옵션 의미까지 포함한 안전 장치가 됨
- GNU와 다른 동작이 있는 지점마다 셸 스크립트가 잘못된 판단을 내릴 가능성이 커짐
- uutils는 이제 CI에서 upstream GNU coreutils 테스트 스위트를 함께 돌림
- 이런 종류의 차이를 막기 위한 방어 규모로 적절해 보임
신뢰 경계를 넘기 전에 먼저 해석해야 함
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를 계속 내왔음
pwddeep path buffer overflownumfmtout-of-bounds readunexpand --tabsheap buffer overflowod --strings -Nheap buffer 바깥 NUL 쓰기sortheap buffer 앞 1바이트 readsplit --line-bytesheap overwrite인 CVE-2024-0684b2sum --checkmalformed input에서 unallocated memory readtail -fstack buffer overrun
- 같은 기간 비교에서 Rust 재구현은 이런 범주의 버그를 0건으로 유지했음
- 단, 감사가 메모리 안전성 버그 부재를 증명한 것은 아니고 발견하지 못했을 뿐이라는 단서도 붙음
- 남은 문제는 Rust 내부보다 외부 세계와 맞닿는 경계에서 주로 생김
- 경로
- 바이트와 문자열
- syscall
- 시간차와 파일시스템 상태 변화
올바른 Rust는 관용적 Rust이기도 함
- 관용적 Rust는 borrow checker를 통과하고
clippy가 조용한 코드에 그치지 않음 - 정확성도 관용성의 일부여야 함
- 현실에서 살아남는 코드 형태가 커뮤니티 경험을 통해 굳어졌기 때문임
- 견고한 시스템은 현실의 지저분함을 숨기기보다 그대로 반영해야 함
- 경로 대신 파일 디스크립터
String대신OsStrunwrap대신?- 더 깔끔해 보이는 의미보다 원본과의 bug-for-bug 호환성
- 타입 시스템은 많은 것을 표현할 수 있지만 두 syscall 사이 시간 경과처럼 통제 밖 조건까지 담아내지는 못함
- 관용적 Rust는 코드의 타입, 이름, 제어 흐름이 실행 환경의 진실을 드러내야 함
- 보기 좋은 화이트보드 코드보다 덜 예쁘더라도 더 정직한 형태가 필요함
참고 자료
- An update on rust-coreutils: 감사 결과 공개
- Patterns for Defensive Programming in Rust: 함께 읽을 수 있는 방어적 Rust 패턴
- Pitfalls of Safe Rust: safe Rust에서도 생길 수 있는 흔한 실수
- Sharp Edges In The Rust Standard Library:
std의 의외의 동작 - uutils/coreutils on GitHub: Rust로 재구현한 GNU coreutils
Hacker News 의견들
-
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그리고 글에서 Rust 재작성은 비슷한 기간 동안 메모리 안전성 버그가 0개라고 했는데, 그건 사실이 아님 :)
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 메모리를 파일 저장용으로 그냥 제대로 동작하게 만드는 문제를 보면 어떨지 궁금함
libcI/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위험한 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/
이전 소프트웨어가 쌓아온 지식을 충분히 가져오지 못한 채 잘못된 방식으로 진행된 듯함재작성을 하려면 전작을 완전히 이해하고 배워야 함
그렇지 않으면 같은 실수를 반복하게 되고, 솔직히 꽤 민망한 일임분명히 하자면 Rust는 좋아하고 여러 프로젝트에 쓰고 있으며 훌륭함
다만 Rust가 나쁜 엔지니어링까지 구해주진 못함-
흥미롭게도
uutils는 GNU coreutils 테스트 스위트를 사용함덧붙이면 GPL 소스를 읽고 작성한 기여는 받지 않겠다는 입장도 명시해 둔 상태임
-
unity,upstart,snap을 만든 쪽이라면 딱 이런 일도 예상 범위 안임 -
새 시스템 프로그래머들에게는 이런 환영 인사를 해야 할 듯함
Unix는 망가져 있고, 결국 못생기고 교육적이지도 않은 우회책을 직접 써야 하며, 경험적 테스트도 해야 함
신뢰할 수 있는 소프트웨어와 좋은 소프트웨어 엔지니어링은 원래 그런 식으로 굴러감
-
-
왜 differential fuzzing이 이런 버그를 못 잡았는지 궁금함
-
경로에 대해 한 번 syscall로 확인하고, 같은 경로에 다시 syscall을 날려 작업하는 패턴은 늘 같은 문제를 부름
부모 디렉터리에 쓰기 권한이 있는 공격자는 그 사이에 경로 구성 요소를 심볼릭 링크로 바꿔치기할 수 있고, 커널은 두 번째 호출에서 경로를 처음부터 다시 resolve해서 권한 있는 작업이 공격자가 고른 대상으로 가게 됨- 사실은 이보다 더 나쁨
부모 디렉터리에 쓰기 권한이 있는 공격자는 하드링크로도 장난칠 수 있음
정규 파일 쪽만 건드릴 수 있다고 해도 사실상 마땅한 완화책이 거의 없음
예시는 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의 의사결정 체인이 어떻게 망가졌길래 이런 게 프로덕션까지 들어갔는지임
- 가끔 성장이란 건 키만 커지는 일이기도 함