Hacker News 의견들
  • GNU Coreutils 유지보수자로서 글은 흥미롭게 읽었지만, 내가 조금 써본 Rust에서는 std::fsTOCTOU race를 만들기가 너무 쉬웠음
    openat 비슷한 API가 표준 라이브러리에 결국 들어가길 바람

    그리고 경로를 비교하기 전에 resolve하라는 규칙에는 동의하지 않음
    일반적으로는 fstat를 호출해서 st_devst_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::fslowest 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

      위험한 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 에러 같은 거라면 모르겠지만, expectunwrap은 그 코드 경로가 절대 실행되지 않게 하는 불변식을 정말 엄격히 보장하는 경우가 아니면 변명하기 어려움

  • 코드를 재작성할 때 어려운 점 중 하나는, 원래 코드는 실제 운영 환경에서만 드러난 문제들에 대응하면서 점진적으로 변형돼 왔다는 데 있음

    그 과정에서 얻은 교훈이 코드 안에 조용히 스며들고, 문서화돼 있지 않으면 동등한 수준에 도달하기 전에 해야 할 숨은 작업이 엄청 많아짐

    원문은 바로 그런 종류의 리스트를 잘 보여줌

    그렇다고 곧바로 아마추어라고 부르기 전에, 이게 소프트웨어에서 가장 소프트웨어다운 현상 중 하나라는 점도 봐야 함
    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가 나쁜 엔지니어링까지 구해주진 못함

    • 흥미롭게도 uutilsGNU coreutils 테스트 스위트를 사용함

      덧붙이면 GPL 소스를 읽고 작성한 기여는 받지 않겠다는 입장도 명시해 둔 상태임

    • unity, upstart, snap을 만든 쪽이라면 딱 이런 일도 예상 범위 안임

    • 새 시스템 프로그래머들에게는 이런 환영 인사를 해야 할 듯함
      Unix는 망가져 있고, 결국 못생기고 교육적이지도 않은 우회책을 직접 써야 하며, 경험적 테스트도 해야 함
      신뢰할 수 있는 소프트웨어와 좋은 소프트웨어 엔지니어링은 원래 그런 식으로 굴러감

  • differential fuzzing이 이런 버그를 못 잡았는지 궁금함

    https://github.com/uutils/coreutils/tree/main/fuzz/uufuzz

  • 경로에 대해 한 번 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의 의사결정 체인이 어떻게 망가졌길래 이런 게 프로덕션까지 들어갔는지임

    • 가끔 성장이란 건 키만 커지는 일이기도 함