
2026/07/01 16:41
私ではなく、コンパイラです
RSS: https://news.ycombinator.com/rss
要約▶
日本語訳:
最近の Rust コンパイラにおける重大な誤コンパイルは、積極的な最適化が安全な高レベルコードを正しくないローレベル命令に変異させる方法を示した。土曜日の夕方に、JavaScript エンジンのパーサーの再構成中に
consume_test 関数において分岐を消去するために self.0 += x as u32; という処理を行いながら、著者はパースが失敗することを見つけ、アセンブリの検査により最適化後に inc esi 命令が欠落しているか上書きされていることが判明した。この問題は Rust 1.94.0-nightly を rustc --edition=2024 -Copt-level=3 -Clto でコンパイルして再現可能であり、-Cno-prepopulate-passes を追加すると未初期化のメモリからの読み込みが露見した。一方、-Zmir-opt-level=0 を使用すれば MIR 段階でバグは修正された。キャストを明示的なブロックに変える (self.0 += if x { 1 } else { 0 };) と問題が解決し、正しいパース (8 ノードで 14ns) が復元された。この事象は p-critical および i-miscompile というラベルが付与され、報告された課題 61,000 件以上の中でこれほど重大な誤コンパイルが出現したのはわずか 7 件の一つに過ぎない。Hanna Kruppe によって 18 時間以内にマージされた修正はコードの変更を必要とせずに安全性を回復させ、コンパイラパスが仮定を満たさない限り、安全な Rust の抽象化であっても不安定なアセンブリを生み出す可能性があることを強調している。この事象は、そのようなバグが安定版リリースで完全に解決されるまで、パフォーマンスにクリティカルな本番システムにおいて nightly バイルドや極限の最適化レベルを使用する際には注意を払うことの重要性を示している。本文
開発者が狂っていないのはコンパイラだった:Rust コンパイルの奇妙な誤動作と解決記
開発者はかつて、「これは俺のせいじゃない。コンパイラのせいだ!」と呟いたことがあります。しかし、今回は実際にその通りだったという稀な体験談です。
1. バグを発見する瞬間
JavaScript エンジンのパーサーをリファクタリング中、従来のコードパターンを改善しようとしていました。
元のコード(一般的なパターン)
impl LexerConsumer { #[inline] pub fn consume(&mut self) { self.0 += 1; } #[inline] pub fn consume_test(&mut self, store: &LexStore, expected: TokenKind) -> bool { if self.peek(store) == expected { self.consume(); true } else { false } } }
改善しようとしたコード
生成されるアセンブリコードが美しく、かつバイト数も減少しましたが、動作に問題が発生しました。
#[inline] pub fn consume_test(&mut self, store: &LexStore, expected: TokenKind) -> bool { let x = self.peek(store) == expected; self.0 += x as u32; // ここで問題が発生 x }
発生したエラー
Bash ヒストリ上に
for ループの解析で以下のエラーが出ました。
> joe parse - 'for (var lol; false; false) {}' Error was found Diagnostic { kind: E079, flag: Flag(11529215046068469773), byte: 5, current_token: Var } Parse error
解決への試み(偽の解決)
x as u32 の挙動を確認するため、明示的な分岐を書き換えてみました。
#[inline] pub fn consume_test(&mut self, store: &LexStore, expected: TokenKind) -> bool { let x = self.peek(store) == expected; self.0 += if x { 1 } else { 0 }; // これなら正常に動作 x }
これで問題は解消されました。自信を持っていましたが、何か見落としていることに気づきました。
2. コンパイラが原因と判明
開発者は通常「bool を u32 として扱う仕組み」を理解しているはずですが、今回は**コンパイルの誤翻訳(Miscompilation)**が起きていることが分かりました。
自前のビルドシステムでアセンブリを確認
cargo asm のラッパーではなく、自作のビルドシステム(TypeScript + Deno)を使用して、特定の関数のアセンブリコードを可視化しました。
コマンド例:
> x -b --fn ForOrInOfStatement 1 --dry MKDIR ./out RUN rustc src/main.rs ... # (略) RUN mold -melf_x86_64 -o ./out/joe ... RUN bash -c nm out/joe | rustfilt | grep ForOrInOfStatement ...
アセンブリコードの分析
Rust コードでは以下の構造になっています。
fn ForOrInOfStatement(...) -> Termination { // ... if lex.peek_test(store, TokenKind::LParen) { ... } if !lex.consume_test(store, TokenKind::LParen) { return raise_diagnostic(...); // エラーhere } // match 文で分岐が行われるはずだが... }
問題のアセンブリコードの箇所
ForOrInOfStatement 関数のアセンブリを確認すると、重要な命令が欠落していました。
> x -b --fn ForOrInOfStatement 1 000000000021e290 <joe::fe::parser_handlers::ForOrInOfStatement>: 21e290: ff c6 inc esi # lex.consume() // for 21e292: 89 f0 mov eax,esi ...
重要な発見:
(インクリメント) が呼ばれていました。inc esi- しかし、その後の処理で
との比較が行われた際、インクリメントされなかった値が使用されていた迹象が見えた。TokenKind::LParen - 実際には
内のconsume_test
が正常に実行されたように見えたが、コンパイラがそれを誤って解釈していた。self.0 += x as u32
疑問:
* `# consume_test で self.0 += x as u32 と書かれなかったのか?` * `# WAIT.... WHO ATE MY INC ESI?! (誰が私の inc esi を喰らったのだ!)`
3. 調査と原因の特定
誤翻訳の可能性を裏付けるため、詳細な調査を行いました。
リバースエンジニアリングによる確認
- LLVM IR の最適化前を確認するために
を使用。-Cno-prepopulate-passes- 初期化されていないメモリからのロードを検出可能。
- Rust/MIR の問題か LLVM の問題かを区別するため
を使用。-Zmir-opt-level=0- これにより問題が解決し、原因は Rust コンパイラ側にあることが確認された。
ミニマルな再現例(MRE)の作成
単に「馬鹿なコードを書いたわけではない」ことを証明するために、最小化された再現例を作成しました。
Issue 登録
- タイトル:
(疑わしい誤翻訳)Suspected miscompilation- 「コンパイルが正しいと断言している場合、自分自身を責めるための防御壁」として「suspected」を付加。
- 環境: Rust nightly 1.94.0-nightly
- 状況: Nightly で修正されているかもしれないと考えたが、依然として問題が発生していた。
4. 対応と解決
翌日、目覚めた時点でコミュニティによる迅速な対応が行われていました。
コミュニティの対応
- Hanna Kruppe による修正 PR がオープンされていた。
- Issue ラベル:
(重大)p-critical
(誤翻訳)i-miscompile
- ステータス:
- Rust の全問題のうち、これらのラベルを併せ持つのはわずか 7 つ(その 1 つ)。
ラベル自体も合計 247 件しか存在しない。p-critical
結論:コンパイラーの危険なバグ
この種の誤翻訳は「プログラマーと言語の間の契約」を破るため、最も危険なバグの一つとされています。安全に書かれたコードが必ずしも安全とは限らないことを示す典型例です。
- 解決までの期間: 問題が発生してから18 時間以内で修正がマージ(Landed)。
- 拍手を送りたい人:
およびtmiasko
の迅速な対応に対して感謝する。hanna-kruppe
まとめ
この体験を通じて、以下のことを学びました。
- 「これは俺のせいじゃない。コンパイラのせいだ!」 という言葉は、実際には正しい場合がある。
- Rust コンパイラと戦い勝つことができた経験を得た。
- コミュニティの迅速な対応により、重大なセキュリティや安定性のリスクを早期に解消することが可能になった。
これからの Rust 開発において、コンパイルの振る舞いを疑うことは決して馬鹿げたことではないことを確認しました。