世界を動かす技術を、日本語で。

Rustが捕まえられないバグ

2026年4月29日原文(corrode.dev)

概要

  • 2026年4月、Canonicalがuutils(Rust製GNU coreutils再実装)の44件のCVEを公開
  • 多くは26.04 LTSリリース前の外部監査で発見
  • Rustの安全機構(borrow checker、clippy、cargo audit)をすり抜けたバグが多数
  • 監査結果はRustの安全性の限界を示す貴重な教材
  • 本内容はuutilsチームへの批判ではなく、学びの共有

Rustで発生した44件のCVEから学ぶシステムプログラミングの教訓

  • uutilsRust で書かれた GNU coreutils の再実装であり、Ubuntu 25.10以降デフォルトで採用
  • 2026年4月、 Canonical が44件のCVEを公開
  • 多くは 外部監査 による発見、26.04 LTSリリースの事前準備として実施
  • Rustの 型安全性静的解析ツール では検出できなかった実バグ
  • uutilsチーム は詳細な監査結果を公開し、コミュニティ全体の学びに貢献

システムコール間のパスの信頼性(TOCTOU問題)

  • TOCTOU(Time Of Check To Time Of Use)バグ が最大のクラスター

  • 例:fs::remove_fileでファイル削除後、File::createで再作成する間にシンボリックリンクへすり替え可能

  • 攻撃者が親ディレクトリへの書き込み権限を持つと、特権操作が任意ファイルに作用

  • Rust標準ライブラリの fs::metadata, File::create, fs::remove_file, fs::set_permissions などのAPIはパスベースで再解決

  • OpenOptions::create_new(true) を使い、ファイル新規作成時のみ安全性を担保

  • それ以外の場合は 親ディレクトリのファイルディスクリプタ を基準に相対パスで操作

    • ルール :同じパスに2回作用するならTOCTOUを疑い、ファイルディスクリプタ基準で処理

パーミッションは作成時に設定

  • ディレクトリやファイルの作成後にset_permissionsでパーミッションを修正すると、短時間デフォルト権限で存在

  • 他ユーザーがその間にopen()可能、chmodしても既存のfdは権限維持

  • DirBuilderExt::mode()OpenOptions::mode() で作成時にパーミッション指定

  • umask の明示的設定も重要

    • ルール :パーミッションは必ず作成時に指定、後から修正しない

パスの文字列比較はファイルシステム上の同一性を保証しない

  • 例:--preserve-rootの判定が"/"と文字列比較のみだと"/../"やシンボリックリンクで回避可能

  • fs::canonicalize で正規化し絶対パス比較

  • より厳密には (dev, inode) ペアで比較

    • ルール :パス比較は文字列ではなく、正規化またはファイルシステムIDで行う

Unix境界ではバイト列で扱う

  • Rustの String&str は常にUTF-8、しかしUnixのパスや環境変数、標準入出力はバイト列

  • from_utf8_lossyは不正バイトをU+FFFDに変換しデータ破壊、unwrapや?はクラッシュ

  • OsStr/OsString&[u8]Vec<u8> でバイト列として扱うべき

  • print!はUTF-8経由だが、write_allはバイト列をそのまま出力

    • ルール :Unix系の生データはバイト列型で処理し、String経由の変換は避ける

panic!はDoS攻撃の温床

  • unwrap、expect、インデックスアクセス、unchecked演算、from_utf8などはpanic!でプロセス全体が異常終了

  • CLIやバッチ処理、CI環境ではDoS(サービス不能)につながる

  • 例:sort --files0-fromで非UTF-8ファイル名にexpectを使い即panic

  • Clippy のunwrap_used、expect_used、panic、indexing_slicing、arithmetic_side_effectsをwarn指定

  • テストコードではpanic許容、CIでは本番コードのみ警告

    • ルール :不正入力はpanic!ではなくエラーとして返却、unwrap/expect禁止

エラーは捨てずに伝播

  • chmod -Rやchown -Rが最後のファイルのエラーコードのみ返却、途中失敗が無視される

  • ddがset_lenのResultを.ok()で黙殺し、ディスクフルでもエラー無視

  • let _ =や.ok()でResultを捨てる際は、なぜ安全かコメント必須

    • ルール :意味あるエラーは必ず伝播し、最悪ケースを記録

オリジナルツールとの完全互換性

  • 多くのCVEは「GNU coreutilsと挙動が違う」ことが原因

  • 例:kill -1の解釈違いで全プロセスkill

  • オプション、エラーコード、エラーメッセージ、端的な挙動までバグ互換が安全性

    • ルール :バトルテスト済みツールの再実装では、バグ含めて挙動を完全再現

まとめ

  • Rustは強力な安全機構を持つが、 TOCTOU問題パーミッションのタイミングバイト列処理panic!によるDoS など、設計・実装レベルでの注意が不可欠
  • 監査レポート はRustによるシステムプログラミングの落とし穴とベストプラクティスの宝庫
  • uutilsチーム の透明性とコミュニティ貢献に感謝

Hackerたちの意見

注目すべきは、これらのバグがすべて、Rustのコードベースに存在していることだね。書いた人たちはちゃんとした知識があったけど、UnixのAPIやセマンティクス、落とし穴にはあまり経験がなかったみたい。長年GNU coreutils(またはBSDやSolarisベース)の開発者から見ると、ほとんどがアマチュア的なミスで、数十年前に特定されて解決された問題なんだよね。それでも、古いコードベースにはまだ修正が続いてるけど、最近はほとんどが微々たるものだね。

それ以上に、Rustの標準ライブラリは、開発者を不適切な抽象レベルでのきれいなAPIを使うように促してる気がする。例えば、ハンドルベースではなくパスベースのファイル操作とか。間違ってるといいな。

誰かが「逆アセンブラの怒り」という関連用語を作ったことがあるんだ。これは、すべてのミスが近くで見るとアマチュアに見えるという考え方。逆アセンブラに座って、例えば関数呼び出しの中で条件文を使った高レベルプログラマーに対して怒る人たちから来てる。彼らは、間違った部分だけを見て、周りの正しい行の数千を無視してるんだよね。

新しい言語でcoreutilsを書き換えて、Unixの経験がほとんどないのに、バグや脆弱性がほとんどないってすごいと思う。少なくとももっと多くの問題が出ると思ってたよ。Rustがどれだけ優れているかを示してるね。経験のないUnix開発者でも、こんなものを書いてほとんどミスをしないんだから。

メモリ安全性はバッファオーバーフローをキャッチする。CIは論理バグをキャッチする。でも、Unix APIの落とし穴は誰もドキュメント化してないから、どちらも捕まえられないんだよね。

こんにちは、私はGNU Coreutilsのメンテナです。この記事ありがとう、面白いトピックがいくつか取り上げられてるね。私が使った少しのRustでは、std::fsを使うとTOCTOUレースが書きやすいと感じたよ。最終的にはopenatに似たAPIが標準ライブラリに追加されることを願ってる。あと、「ルール:パスを比較する前に解決する」というセクションには同意できないな。一般的にはfstatを呼び出してst_devとst_inoを比較する方がいいと思う。でも、この記事でも触れられてたね。あまり考慮されない副作用としては、パフォーマンスへの影響があるよ。実際の例を挙げると、$ mkdir -p $(yes a/ | head -n $((32 * 1024)) | tr -d '\n') $ while cd $(yes a/ | head -n 1024 | tr -d '\n'); do :; done 2>/dev/null $ echo a > file $ time cp file copy 実行時間 0m0.010s ユーザー 0m0.002s システム 0m0.003s $ time uu_cp file copy 実行時間 0m12.857s ユーザー 0m0.064s システム 0m12.702s こんなことを現実でやる人はほとんどいないと思うけど、GNUソフトウェアは恣意的な制限を避けるためにすごく頑張ってるんだよね[1]。それに、全体的なポイントはまだ有効だけど、記事には「Rustの書き換えでは、同等の活動期間中にこれらの[メモリ安全性のバグ]はゼロだった」と書いてある。でも、それは真実じゃないよ[2]。 :)

ごめん、完全な初心者なんだけど。どうして$(yes a/ | head -n $((32 * 1024)) | tr -d '\n')にcdしなかったの?whileループを使ってcdする必要があるの? 編集:わかった。-bash: cd: a/a/a/....../a/a/: ファイル名が長すぎます

まずは、反対側の視点からの簡潔な意見をありがとう。これからどうやって学べるかな?(特にインターネットの文章において、対比をはっきりさせるためにかなり攻撃的に聞いてるよ。)(君は僕に時間や精神的な余裕を貸す必要は全くないからね。)じゃあ、いくつか質問するね。質問1: どうして「スピード」、「パフォーマンス」、レースコンディション、st_inoが何度も出てくるの?スピード(レイテンシ)、物理的にストレージに書き込むこと(順次、原子的に(ACID)、HDD、NVME、SSD、ODD、FDD、テープ、「Haskellモナド」、イベントホライズン、光と情報の有限速度、何でも)やレースコンディションは、結局同じことに行き着くように思える。会計のような信頼性の高いシステムでは、ACIDかハイウェイが道筋のようだね。「信頼性のない」システムは、コンピュータがあまり違いを生まないほど早く忘れる。質問2: 日常のアプリケーションでは、スループットはレイテンシよりも重要なの?質問3(今回は説明から始めるね): inode番号に焦点を当てるのは、CやUnix系OS、GNU coreutilsの歴史を考えると理解できる。でも、この基本的な例はどう?USBメモリをファイル保存用に「動かす」だけでいいんだけど(nandフラッシュの劣化やUSBは無視して)。libcのIOバッファリング、fflush、カーネルバッファリング(LinuxやFreeBSDよりHurdが好きならそれで)、マルチコアやタイムスライスシステム上で複数のアプリケーションが動いている状況(ブロッキングIOの単一ユーザーレベルのバイナリしか動かしていない単一コアCPUを排除するために)に引っかからないように。

公平に言うと、RustのVec::set_lenバグは2021年のことだったんだ。それでも、unsafeとして注釈を付けなきゃいけなかった。で、その後非推奨になって、リンターのチェックが追加されたんだよね: https://github.com/rust-lang/rust-clippy/issues/7681

多分バカな質問だけど、GNU Core utilsは自分たちのRust書き直しに興味があるのかな?

要するに、ファイルシステム操作に必要な原子性が欠けてるのと、面倒なパスや文字列のエンコーディング、歴史的な振る舞いの慣性が問題なんだね。

リストありがとう。こういうリスト好きだから、.mdファイルに入れて、コードベースで「ファイルごとに1エージェント」を実行して、挙げられたCVEに似たものが見つかるか確認したいんだ。Rustでは捕まえられないけど、今度はエージェントが捕まえてくれるだろうね。 編集: https://gist.github.com/fschutt/cc585703d52a9e1da8a06f9ef93c... これをコピーしたい人のために。

Hacker Newsで議論の続きを見る