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のシステムコードで安全性が終わる地点を凝縮して示している
パスを2回解釈するとTOCTOUが生じる
- 同じパスを1つのsyscallで確認し、次のsyscallで再び操作するとTOCTOU脆弱性につながりやすい
- 2つの呼び出しのあいだに、親ディレクトリへの書き込み権限を持つ攻撃者がパス構成要素をシンボリックリンクに差し替えられる
- 2回目の呼び出しでカーネルがパスを最初から再解釈し、権限付き操作が攻撃者の選んだ対象へ向かってしまう
- 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も許可しない
- ドキュメント上、
- 同じパスに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や?のような厳格な変換は、入力を拒否したりプロセスを終了させたりしうる
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前**に行う- 修正も同じ順序に変更された
- ひとたび信頼境界を越えると、その後のライブラリ呼び出し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を出し続けている
pwddeep path buffer overflownumfmtout-of-bounds readunexpand --tabsheap buffer overflowod --strings -Nheap buffer外へのNUL書き込みsortheap buffer手前1バイト readsplit --line-bytesのheap overwriteであるCVE-2024-0684b2sum --checkのmalformed inputでのunallocated memory readtail -fstack buffer overrun
- 同期間の比較では、Rustによる再実装はこのカテゴリのバグを0件に保っていた
- ただし、監査がメモリ安全性バグの不在を証明したわけではなく、見つからなかっただけという留保もある
- 残る問題はRust内部よりも外部世界と接する境界で主に発生する
- パス
- バイトと文字列
- syscall
- 時間差とファイルシステム状態の変化
正しいRustはイディオマティックなRustでもある
- イディオマティックなRustは、borrow checkerを通り
clippyが静かなコードであることにとどまらない - 正確さもイディオムの一部であるべき
- 現実で生き残るコードの形が、コミュニティの経験を通じて定着してきたため
- 堅牢なシステムは、現実の泥臭さを隠すのではなくそのまま反映すべき
- パスの代わりにファイルディスクリプタ
Stringの代わりにOsStrunwrapの代わりに?- きれいに見える意味づけより元実装とのbug-for-bug互換性
- 型システムは多くを表現できても、2つの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
1件のコメント
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 メモリをファイル保存用として単に正しく動かす問題を考えるとどうだろう
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 エラーのようなものならともかく、
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 がこういうバグを見つけられなかったのか気になる
https://github.com/uutils/coreutils/tree/main/fuzz/uufuzz
パスについて 1 回 syscall で確認し、同じパスに再び syscall を投げて処理するパターンは、常に同じ問題を招く
親ディレクトリに書き込み権限がある攻撃者は、その間にパス構成要素を シンボリックリンク にすり替えられ、カーネルは 2 回目の呼び出しでパスを最初から再解決するため、権限付き操作が攻撃者の選んだ対象に向かってしまう
親ディレクトリに書き込み権限がある攻撃者は ハードリンク でもいたずらできる
通常ファイルだけを触れる場合であっても、実際にはまともな緩和策はほとんどない
例としては https://michael.orlitzky.com/articles/posix_hardlink_heartache.xhtml を参照
いくつかのバグの 根本原因 は 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.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 の 意思決定の連鎖 がどう壊れていたら、こんなものが本番まで入ってしまうのかという点だ