1 ポイント 投稿者 GN⁺ 2024-04-05 | 1件のコメント | WhatsAppで共有
  • 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文をコピーしたあと、増分式のijに変え忘れたミスに相当する
    • アナライザはこの経路でj < nのtrue分岐を追い続け、ループ反復を警告する
  • 現在の診断出力は、イベント番号(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で試せる

汚染解析のデフォルト有効化

CVE-2011-2210で見るカーネル解析事例

  • LinuxカーネルのCVE-2011-2210抜粋例が汚染解析の事例として使われている
  • __SYSCALL_DEFINExマクロに__attribute__((tainted_args))を追加し、osf_getsysinfo引数が信頼境界を越えてきた値であり、汚染された値として扱うべきことをアナライザに伝える
  • GCC 14アナライザは、copy_to_user(buffer, hwrpb, nbytes)で攻撃者制御の値nbytesが上限チェックなしにサイズとして使われていると警告する
    • 診断は、nbytesif (nbytes < sizeof(*hwrpb))で下限チェックしか受けていないことを示す
    • copy_to_userの第3引数はaccess(write_only, 1, 3)属性によってサイズ引数であることが示される
  • 問題は、サニタイズ条件がif (nbytes < sizeof(*hwrpb))と書かれていた点にある
  • カーネルでアナライザを実行して脆弱性を見つけ、アナライザの誤検出を修正する作業は現在も続いている

1件のコメント

 
GN⁺ 2024-04-05
Hacker News のコメント
  • 私にとって fanalyzer は、Clang に対する GCC のキラー機能の一つ。エラーを説明してくれるので C プログラミングがずっと簡単になり、エラーメッセージも開発者に優しいという点で Rust に近いと感じ始めている

    • Rust が特に HN で、メモリ安全性と優れた抽象化によって大きな注目を集めているのは分かるが、Rust の人気のうちどれくらいが エラーメッセージ によるものなのか気になる
      技術の学習をやめてしまう最大の理由は、苛立たしい、あるいは不明瞭なエラーであることが多い。少し話がそれたが、C が好きで、Rust レベルのエラーメッセージがあればさらに良くなるだろう、という意味だった
    • Clang にも似たツールである Clang Static Analyzer がある: https://clang-analyzer.llvm.org/
    • まったく逆の経験をした。Clang は GCC よりはるかに良いエラーメッセージを頻繁に出し、一部の警告やエラーの実装はより多くのケースを検出し、clang-tidy はずっと優れた静的解析をしてくれる
    • C++ をあれほど嫌っていた理由の一つを思い出した。error: missing semicolon の代わりに、テンプレートのインスタンス化 に関するエラーメッセージが1000行以上も吐き出されることがよくあった
    • この話はかなり驚きに聞こえる。GCC analyzer が、Clang Static Analyzer がまだ報告しない何をさらに検出してくれるのか気になる
      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 関連のネストしたコンパイラダンプを見るのにうんざりして使っていたが、望めばどんなプログラムにも使えた。うまく動いていたが、ひどく体調を崩してそれ以上作業できなかった。誰かがもう一度きちんと作って汎用化してくれれば、良いプロジェクトになると思う

  • 解析結果にもっと良い出力形式があるといい。今の方式は スクリーンリーダー には地獄だ

    • 参考までに、GCC 13 に SARIF 出力 を実装していて、たとえば VS Code ではプラグインで見ることができる。ただし ASCII アートは含まれない
      出力例はここで見られる: 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 開発をあまりしていないので、strcpystrcat がどれくらい頻繁に使われているのか気になる。最後に確認したときは、goto と同じくらい禁忌に近いものだった
    もちろん、カーネル開発では goto がよく好まれることは知っている。C 文字列解析 が実際にどれほど役に立つのか気になる

    • ある文脈では、goto の使用は明らかに正しく、優雅でもある。何が何でも避けようとすると、保守しにくい醜く入り組んだコードになり得る。一般的ではなくても、有効な使い道はある
      strcpy のような関数はあまり推奨されないが、より強い不変条件、たとえば言語レベルの不変条件が破られない限り正しいと保証される状況もある。そうしたケースが破られているなら、すでにはるかに大きな問題があるということだ。まれな場合ではあるが、名目上より安全な代替手段が、利益なしにわずかに効率が悪いだけだという主張も可能だ
    • C にない構造化プログラミング構文と論理的に同等に使う限り、一部の goto の使用は今でも C では慣用的だ。注意は必要だが、いずれにせよ C なのだからそういうこともある
      ただし longjmp はまったく好きではない
    • 単純な goto の使用には問題はない。一方で strxcpy 系 は完全にめちゃくちゃで、どんな理由でも使うべきではない。カーネルで使われているという事実はひどい
      そうした関数と、それらを「直そう」とした失敗作のすべては、軌道上から吹き飛ばされるべきだった
    • goto は慎重に使えば問題ない。strcpystrcat も、コードが正しいと分かっていて、間違っていれば大問題になるという意味では「問題ない」。残念ながら、この説明は C の大部分に当てはまる
    • gotostrcatstrcpy ほど禁忌だとは思わない。goto は問題ないし、同じスコープで正確なサイズの malloc なしに使われる strcatstrcpyコードスメル に近い
  • とても良い。何が間違っているのかを説明する 詳細なレポート がすべて付いていてうれしい