1 ポイント 投稿者 GN⁺ 1 시간 전 | 1件のコメント | WhatsAppで共有
  • メモリ安全性は大きく改善される一方で、Rustの本番コードでもシステム境界の扱いの問題はそのまま残り、脆弱性につながりうる
  • 同じパスを複数のsyscallで再解釈する流れ、作成後に権限を変更する方式、文字列ベースのパス比較は、TOCTOUや権限露出のような問題を招きやすい
  • Unixではパス、環境変数、ストリームデータが生バイト列としてやり取りされるため、String中心の処理やfrom_utf8_lossyunwrapexpectはデータ破損や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のシステムコードで安全性が終わる地点を凝縮して示している

パスを2回解釈するとTOCTOUが生じる

  • 同じパスを1つのsyscallで確認し、次のsyscallで再び操作するとTOCTOU脆弱性につながりやすい
    • 2つの呼び出しのあいだに、親ディレクトリへの書き込み権限を持つ攻撃者がパス構成要素をシンボリックリンクに差し替えられる
    • 2回目の呼び出しでカーネルがパスを最初から再解釈し、権限付き操作が攻撃者の選んだ対象へ向かってしまう
  • Rustのstd::fs APIは**&Pathベースの再解釈**を前提としており、この種のミスを起こしやすい
  • CVE-2026-35355では、ファイル削除後に同じパスへ新規ファイルを作る流れが悪用された
    • src/uu/install/src/install.rsfs::remove_file(to)?の直後にFile::create(to)?が続いていた
    • 削除と作成のあいだにto/etc/shadowのような対象を指すシンボリックリンクへ変えられると、権限付きプロセスがそのファイルを上書きできてしまう
  • 修正ではOpenOptions::create_new(true)を使い、新規ファイルのみ作成するよう変更された
    • ドキュメント上、create_newは対象位置に既存ファイルだけでなくdangling symlinkも許可しない
  • 同じパスに2回操作する必要があるなら、ファイルディスクリプタに固定するほうが安全
    • 新規ファイル作成以外では、親ディレクトリを一度開き、そのハンドル基準の相対パスで操作するのが適切
    • 同じパスに2回触るなら、反証されるまでは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では/に親ディレクトリがないため、この方式が通用する
  • 2つの任意パスが同じファイルシステムオブジェクトか一般的に比較するには、**文字列ではなく(dev, inode)**を比較すべき
    • GNU coreutilsもこの方式で処理している
  • CVE-2026-35363では、rm...は拒否する一方で././//は許し、カレントディレクトリを削除できてしまった
    • 入力形式の差を文字列レベルでしか扱わないと、検査は簡単にすり抜けられる

Unix境界では文字列よりバイトを優先すべき

  • RustのString&strは常にUTF-8だが、Unixのパス・環境変数・引数・ストリームデータは生バイト列の世界にある
  • この境界を越える際の誤った選択は、2種類のバグにつながる
    • from_utf8_lossyのような損失変換は、不正なバイトをU+FFFDに置き換えて静かにデータを壊す
    • unwrap?のような厳格な変換は、入力を拒否したりプロセスを終了させたりしうる
  • commCVE-2026-35346は、損失変換によって出力が壊れたケースだった
    • src/uu/comm/src/comm.rsで入力バイトrarbString::from_utf8_lossyに変換してprint!していた
    • GNU commはバイナリファイルでもバイトをそのまま流すが、uutilsは不正なUTF-8をU+FFFDへ置き換え、出力を破損させていた
    • 修正はBufWriterwrite_allでraw bytesをそのままstdoutへ書き出す方式だった
  • print!Displayを経由するためUTF-8往復を強制するが、Write::write_allはそうではない
  • Unix系システムコードでは、状況に合った型を使うべき
    • ファイルパスにはPathPathBuf
    • 環境変数にはOsString
    • ストリーム内容にはVec<u8>または&[u8]
  • フォーマットの都合でStringを経由すると、データ破損が忍び込みやすい

すべてのpanicはサービス拒否につながりうる

  • CLIでのunwrapexpect、スライスのインデックス参照、検査なし算術、from_utf8は、攻撃者が入力を制御できる場合DoSポイントになりうる
    • panic!はスタックをunwindし、プロセスを停止させる
    • cron job、CI pipeline、shell scriptで実行中なら、作業全体が止まりうる
    • 繰り返し実行環境ではcrash loopによりシステム全体を麻痺させることもある
  • sort --files0-fromCVE-2026-35348は、NUL区切りのファイル名一覧で非UTF-8のファイル名に出会うと停止していた
    • パーサは各名前バイトにstd::str::from_utf8(bytes).expect(...)を呼んでいた
    • GNU sortはカーネルと同様にファイル名をraw bytesとして扱うが、uutilsはUTF-8を強制し、最初の非UTF-8パスでプロセス全体を停止させていた
  • 信頼できない入力を処理するコードでは、unwrapexpect、インデックス参照、as castを潜在的なCVEと見なすべき
    • ?getchecked_*try_fromを使い、実際のエラーを呼び出し側へ返すべき
  • CIで検出するためのclippy基準も示されている
    • unwrap_used
    • expect_used
    • panic
    • indexing_slicing
    • arithmetic_side_effects
  • テストコードではこうした警告が過剰になりうるため、cfg(test)の範囲で制限する形が適切

エラーを捨てると失敗が成功に見える

  • 一部のCVEは、エラーを無視したりエラー情報が失われる流れから生まれていた
  • chmod -Rchown -Rは、全体の処理中最後のファイルの終了コードしか返していなかった
    • 先行する多数のファイル処理に失敗していても、最後のファイルが成功すれば0で終わることがある
    • スクリプトは全処理が問題なく完了したと誤認してしまう
  • dd/dev/nullでGNUの挙動を模倣するためにset_len()の結果へResult::ok()を呼んでいた
    • 意図としては限定状況でエラーを捨てることだったが、同じコードが通常ファイルにも適用されていた
    • ディスクが満杯でも半分だけ書かれた宛先ファイルが静かに残る可能性があった
  • .ok().unwrap_or_default()let _ =Resultを捨てると、重要な失敗原因が消えてしまう
  • 最初の失敗で即中断しないとしても、最も重大なエラーコードを記録して終了すべき
  • どうしてもResultを捨てる必要があるなら、その失敗をなぜ安全に無視できるのか理由をコードに残すべき

元ツールとの正確な互換性も安全機能である

  • 複数のCVEは、コードが危険な操作をしたからではなくGNUと異なる動作をしたために起きていた
    • 実際のシェルスクリプトは元のGNUの挙動に依存しており、意味の差がセキュリティ問題につながる
  • kill -1CVE-2026-35369が代表例
    • GNUは-1signal 1として読み、PIDを要求する
    • uutilsはこれをPID -1へのデフォルトシグナル送信と解釈していた
    • LinuxでPID -1は可視な全プロセスを意味するため、単純なタイプミスがシステム全体のkillにつながりうる
  • 再実装ツールでは、bug-for-bug互換性が終了コード、エラーメッセージ、edge case、オプションの意味まで含む安全装置になる
  • GNUと異なる動作がある箇所ごとに、シェルスクリプトが誤った判断を下す可能性が高まる
  • uutilsは現在、CIでupstream GNU coreutilsのテストスイートもあわせて実行している
    • この種の差異を防ぐための防御規模として妥当に見える

信頼境界を越える前に先に解決すべき

  • CVE-2026-35368は、chrootlocal root code executionだった
  • 問題のパターンは、chroot(new_root)?の後で攻撃者が制御する新しいルート内でユーザー名を解決していたことにあった
    • get_user_by_name(name)?が新しいルートファイルシステムの共有ライブラリを読み込み、ユーザー名を解決してしまう
    • 攻撃者がchroot内部にファイルを仕込むと、uid 0のコード実行につながりうる
  • GNU chrootはユーザー解決を**chroot前**に行う
    • 修正も同じ順序に変更された
  • ひとたび信頼境界を越えると、その後のライブラリ呼び出し1つひとつが攻撃者コードを実行しうる
  • 静的リンクでもこの問題は防げない
    • 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
    • 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互換性
  • 型システムは多くを表現できても、2つのsyscallのあいだの時間経過のような制御外の条件までは表せない
  • イディオマティックなRustでは、コードの型、名前、制御フローが実行環境の真実を露わにすべき
    • 見栄えのよいホワイトボード上のコードより、多少不格好でもより正直な形が必要

参考資料

1件のコメント

 
GN⁺ 1 시간 전
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 ソースを読んで書かれた貢献は受け入れないという方針も明示している

    • unityupstartsnap を作ったところなら、まさにこういうことも予想の範囲だ

    • 新しいシステムプログラマには、こういう歓迎の言葉を送るべきなのかもしれない
      Unix は壊れていて、結局は見た目も悪く教育的でもない回避策を自分で書かなければならず、経験的テスト も必要だ
      信頼できるソフトウェアと良いソフトウェアエンジニアリングは、もともとそういうものだ

  • なぜ differential fuzzing がこういうバグを見つけられなかったのか気になる

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

  • パスについて 1 回 syscall で確認し、同じパスに再び syscall を投げて処理するパターンは、常に同じ問題を招く
    親ディレクトリに書き込み権限がある攻撃者は、その間にパス構成要素を シンボリックリンク にすり替えられ、カーネルは 2 回目の呼び出しでパスを最初から再解決するため、権限付き操作が攻撃者の選んだ対象に向かってしまう

    • 実際にはこれよりもっと悪い
      親ディレクトリに書き込み権限がある攻撃者は ハードリンク でもいたずらできる
      通常ファイルだけを触れる場合であっても、実際にはまともな緩和策はほとんどない
      例としては 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 の問題ではない
      そんな書き方をした人は、このプロジェクトに参加する資格がほとんどないように思える

    • むしろ 関数型安全言語 を使うと、扱っているデータまで無状態だと錯覚しやすいのかもしれない
      しかし OS では実に多くのものが常に変化する

      スナップショットを提供するファイルシステムが現れるまでは、すべてを 何度も確認し直す 必要がある

      結局必要なのは、入力を受け取ったら成功した結果か失敗だけを返す 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.sonewroot/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 の 意思決定の連鎖 がどう壊れていたら、こんなものが本番まで入ってしまうのかという点だ

    • ときには成長というのは、ただ背が伸びることだったりもする