GCC 14における静的解析の改善点
(developers.redhat.com)- GCC 14の
-fanalyzerは、Cコードの複数の実行経路をシンボリック実行で追跡し、実行時まで見つけにくい欠陥をコンパイル時に発見することに重点を置いている - 今回の改善は、無限ループ検出、バッファオーバーフローの可視化、C文字列追跡、汚染解析のデフォルト有効化に分かれており、診断結果をより理解しやすくすることに集中している
-Wanalyzer-out-of-boundsは単純な警告を超えて、テキストダイアグラムでバッファと書き込み位置の関係を示し、UTF-8文字列ではどの文字の途中であふれるかも表示する- 新しい
null_terminated_string_arg(PARAM_IDX)属性は、ヌル終端文字列を期待するAPI条件をアナライザとコード読者に伝え、ヌル終端されていないバッファが渡される経路をより明確にする - 汚染解析は
-fanalyzerを選択するとデフォルトで有効になり、攻撃者が制御する値が上限チェックなしでサイズ・インデックス・オフセットなどに使われる流れをより簡単に確認できる
-fanalyzerが扱う問題の範囲
-fanalyzerはGCCの静的解析パスであり、Cソースコードの複数の実行経路をシンボリック実行してコンパイル時に欠陥を見つけようとする- GCC 14の改善点は2024年4月の公式リリースを前に整理されたもので、記事執筆時点ではGCC 14.1リリースが2024年4月中に期待されていた
- GCC 14.0プレリリースはFedora 40 Betaですでに利用されている
- 例はCompiler Explorerで新しいコンパイラオプションとともに試すことができる
単純な無限ループ検出
- GCC 14には新しい警告
-Wanalyzer-infinite-loopが追加された - 例のコードは、入れ子になった
for文で2つ目のループ条件がj < nなのに、増分式がi++のままになっている形である- 1つ目の
for文をコピーしたあと、増分式のiをjに変え忘れたミスに相当する - アナライザはこの経路で
j < nのtrue分岐を追い続け、ループ反復を警告する
- 1つ目の
- 現在の診断出力は、イベント番号
(1)から(5)までを順番に読まないと流れを理解しにくい - GCC 15では、制御フロー経路を強調するASCIIアートのような方法で可読性を高める案が今後の課題として残っている
- このコードはCompiler Explorerの例で試せる
バッファオーバーフローをテキストで可視化
- GCC 13でアナライザは
-Wanalyzer-out-of-boundsを通じて境界チェックをサポートし始めた - GCC 14では、予測されたバッファオーバーフローの空間的な関係をテキストベースのダイアグラムで出力できる
char buf[10]にstrcpy(buf, "hello")を行ったあとstrcat(buf, " world!")を呼ぶ例では、スタックベースのバッファオーバーフローが検出される- 従来のメッセージでは、
bufの容量が10バイトで、byte 10からbyte 12まで範囲外書き込みが発生すると示される - GCC 14のダイアグラムは、
strcpyが埋めた宛先バッファと、strcatの開始位置になる既存の終端NULバイトをあわせて示す
- 従来のメッセージでは、
- 非ASCII文字列の例でもUTF-8表現を追跡しながらオーバーフロー位置を表示する
char buf[11]に"サツキ"をコピーしたあと、"メイ"を連結するコードが例として使われている- ダイアグラムは、オーバーフローが
メ文字、つまりU+30E1の途中で発生することを示す
- 関連コードはASCII文字列の例と非ASCII文字列の例で確認できる
C文字列演算の追跡強化
- GCC 14アナライザはC文字列演算の追跡を改善し、ヌル終端バイトを探しながらバッファを走査するAPIをシミュレートする
- ポインタがヌル終端されていないバッファを指しているのに、そのAPIに渡される経路があると警告する
- 新しい関数属性
null_terminated_string_arg(PARAM_IDX)は、特定の引数がヌル終端文字列でなければならないという条件をアナライザとコード読者に伝える - 例では
example_fn(const char *p)にnull_terminated_string_arg(1)とnonnull属性が付いているchar str[3] = "abc";にはヌル終端バイトを入れる空きがないexample_fn(str)を呼ぶと、アナライザはstrの末尾の先を1バイト読むスタックベースのバッファ過剰読み取りを警告する- 診断には、
example_fnの第1引数がヌル終端文字列ポインタである必要があるというnoteもあわせて出力される
- この例はCompiler Explorerで試せる
汚染解析のデフォルト有効化
- アナライザの汚染解析は、攻撃者が制御する入力、サニタイズ地点、サニタイズされないまま使われる地点を追跡する
- 以前のGCCリリースでは、バグや大量の誤検出のためデフォルトでは有効化されず、別のコマンドライン引数の背後に隠されていた
- GCC 14では、複数のバグ修正の後、
-fanalyzerを選択すると汚染解析がデフォルトで有効になる - この変更により、次の6つの汚染ベース警告もあわせて有効になる
CVE-2011-2210で見るカーネル解析事例
- LinuxカーネルのCVE-2011-2210抜粋例が汚染解析の事例として使われている
__SYSCALL_DEFINExマクロに__attribute__((tainted_args))を追加し、osf_getsysinfo引数が信頼境界を越えてきた値であり、汚染された値として扱うべきことをアナライザに伝える- GCC 14アナライザは、
copy_to_user(buffer, hwrpb, nbytes)で攻撃者制御の値nbytesが上限チェックなしにサイズとして使われていると警告する- 診断は、
nbytesがif (nbytes < sizeof(*hwrpb))で下限チェックしか受けていないことを示す copy_to_userの第3引数はaccess(write_only, 1, 3)属性によってサイズ引数であることが示される
- 診断は、
- 問題は、サニタイズ条件が
if (nbytes < sizeof(*hwrpb))と書かれていた点にある- 意図された条件は
if (nbytes > sizeof(*hwrpb))の形だった - 条件を修正した版ではアナライザは警告しない
- 意図された条件は
- カーネルでアナライザを実行して脆弱性を見つけ、アナライザの誤検出を修正する作業は現在も続いている
1件のコメント
Hacker News のコメント
私にとって fanalyzer は、Clang に対する GCC のキラー機能の一つ。エラーを説明してくれるので C プログラミングがずっと簡単になり、エラーメッセージも開発者に優しいという点で Rust に近いと感じ始めている
技術の学習をやめてしまう最大の理由は、苛立たしい、あるいは不明瞭なエラーであることが多い。少し話がそれたが、C が好きで、Rust レベルのエラーメッセージがあればさらに良くなるだろう、という意味だった
error: missing semicolonの代わりに、テンプレートのインスタンス化 に関するエラーメッセージが1000行以上も吐き出されることがよくあったGCC analyzer は何度か使ってみたが、出力を読みやすくしてくれる良いフロントエンドを見つけられなかった。Clang にはかなり良い HTML 出力、CodeChecker、Xcode 統合などいくつも選択肢があるが、GCC 側の出力はどう読むのか気になる。さらに、GCC は Clang より 誤検出 がかなり多いように思える
別スレッドにコメントがさらに36件ある: https://news.ycombinator.com/item?id=39918278
“GCC 14 Boasts Nice ASCII Art for Visualizing Buffer Overflows (phoronix.com)”、2時間前の記事
数か月前に小さな Linux ユーティリティ を作った。任意の実行ファイルを置き換えるドロップイン shim で、呼び出されると元のプログラムのふりをしつつ、本物を fork して stdout/stderr に接続するものだった
エラー出力はそのプログラムの文脈を知っているカスタム GPT アシスタントに送り、アシスタントが元のエラーを人間に読みやすい形に変換してから shim の stderr に出していた。GCC/Clang の concept/template 関連のネストしたコンパイラダンプを見るのにうんざりして使っていたが、望めばどんなプログラムにも使えた。うまく動いていたが、ひどく体調を崩してそれ以上作業できなかった。誰かがもう一度きちんと作って汎用化してくれれば、良いプロジェクトになると思う
解析結果にもっと良い出力形式があるといい。今の方式は スクリーンリーダー には地獄だ
出力例はここで見られる: https://godbolt.org/z/aan6Kfxds
記事の最初の例にコマンドラインオプション
-fdiagnostics-format=sarif-stderrを追加したもの。ダイアグラムを SVG で出力する実験もしたが、GCC 14 に入れられるほど十分には磨き込めなかった元のコードは、
nbytes < sizeof(*hwrpb)なら-1を返し、copy_to_user(buffer, hwrpb, nbytes)が失敗したら-2を返す形だった。適用された修正はnbytes > sizeof(*hwrpb)のチェックだったが、正しい修正はcopy_to_user(buffer, hwrpb, sizeof(*hwrpb))だと思うhwrpbポインタからsizeof(*hwrpb)ではないサイズをコピーしていくのは筋が通らないnbytes = 4を渡し、sizeof(hwrpb)が16バイトなら、呼び出し側のバッファから 12バイト余分にコピー して、所有していないメモリを読めてしまう。避けるべきだと思うより良い解決策は、呼び出し側と呼び出される側の両方がサポートする最小バイト数だけをコピーすること。たとえば
nbytes = MIN(nbytes, sizeof(hwrpb));のようにすればよい。hwrpb->sizeのバージョン情報が守られるという前提なら、構造体の一部が初期化されていなくても下位/上位互換性を保証できるnbytesより長い値を渡すこともできない本当に素晴らしい。投入された作業量は途方もないものに見える。難易度は標準ライブラリと C 標準に fat pointer/配列 view を導入するのと同じくらいに見える
-Wstringop-overflowは誤検出が多すぎて、最初に無効にする警告だ。analyzer 版だからといって良くなるのかは疑わしいとてもクール。最近は C 開発をあまりしていないので、
strcpyとstrcatがどれくらい頻繁に使われているのか気になる。最後に確認したときは、gotoと同じくらい禁忌に近いものだったもちろん、カーネル開発では
gotoがよく好まれることは知っている。C 文字列解析 が実際にどれほど役に立つのか気になるgotoの使用は明らかに正しく、優雅でもある。何が何でも避けようとすると、保守しにくい醜く入り組んだコードになり得る。一般的ではなくても、有効な使い道はあるstrcpyのような関数はあまり推奨されないが、より強い不変条件、たとえば言語レベルの不変条件が破られない限り正しいと保証される状況もある。そうしたケースが破られているなら、すでにはるかに大きな問題があるということだ。まれな場合ではあるが、名目上より安全な代替手段が、利益なしにわずかに効率が悪いだけだという主張も可能だgotoの使用は今でも C では慣用的だ。注意は必要だが、いずれにせよ C なのだからそういうこともあるただし
longjmpはまったく好きではないgotoの使用には問題はない。一方で strxcpy 系 は完全にめちゃくちゃで、どんな理由でも使うべきではない。カーネルで使われているという事実はひどいそうした関数と、それらを「直そう」とした失敗作のすべては、軌道上から吹き飛ばされるべきだった
gotoは慎重に使えば問題ない。strcpyとstrcatも、コードが正しいと分かっていて、間違っていれば大問題になるという意味では「問題ない」。残念ながら、この説明は C の大部分に当てはまるgotoがstrcatやstrcpyほど禁忌だとは思わない。gotoは問題ないし、同じスコープで正確なサイズのmallocなしに使われるstrcatとstrcpyは コードスメル に近いとても良い。何が間違っているのかを説明する 詳細なレポート がすべて付いていてうれしい