
LLMにコードの欠陥やセキュリティ上の問題を探させる人が増えています。
手元のファイルをそのまま貼り付けて、「この中に危ない箇所はないか」と尋ねる。これは、とても自然な使い方に見えます。
ただし、同じコードでも、渡し方によって結果は大きく変わります。しかも、情報を多く渡せば渡すほど精度が上がるとは限りません。むしろ、余計な情報が増えることで、重要な手がかりが埋もれてしまうことがあります。
限られたトークンの中で、コードをどんな形に整えて渡せば、LLMはより的確に判断できるのか。本記事では、実際の検証データをもとに、実務に取り入れやすい形で整理します。
表現形式は重要:LLMの脆弱性推論のためのプログラム表現に関する実証研究
Representation Matters: An Empirical Study of Program Representations for LLM Vulnerability Reasoning
本研究は、大規模言語モデル(LLM)を用いた脆弱性検出において、プログラムの構造や意味をどのように表現するのが最適かを探求する。ソースコード、抽象構文木(AST)、制御フローグラフ(CFG)、プログラム依存グラフ(PDG)など、様々なプログラム表現を体系的に評価するベンチマーク「REPBENCH」を提案し、その有効性を検証する。
| 著者 | Andrew Stoltman, Johnathan Tang, Haipeng Cai |
|---|---|
| URL | https://arxiv.org/abs/2606.25356 |
「ソースをそのまま渡す」が当たり前になった
大量のソースコードで学習したモデルは、構文やAPIの使い方を直接読み取れます。そのため、コードをそのまま見せれば判断できるはずだ、という発想は自然です。
しかし、脆弱性はコードの見た目だけで判断できるとは限りません。整数オーバーフローを見抜くには、計算の前に適切な境界チェックがあるかを確認する必要があります。二重解放やuse-after-freeであれば、メモリの確保、解放、別名参照、再利用がどのようにつながっているかをたどる必要があります。
こうした手がかりは、コードの離れた場所に散らばっています。長く整理されていないソースの中では見落とされやすく、モデルにとっても関係を一から組み立て直す負担が大きくなります。
コードを構造に変換してから渡す
静的解析の基盤、たとえばJoernのようなツールを使うと、ソースコードを構造的な見取り図に変換できます。脆弱性の判断に関わる代表的な表現は、主に3つあります。
| 表現 | 何を捉えるか | どんな判断に向くか |
|---|---|---|
| AST(抽象構文木) | 各行でどんな操作が起きているかという文法構造 | 演算、呼び出し、メモリ確保など、局所的な目印の把握 |
| CFG(制御フローグラフ) | 処理がどの順番で実行されうるか | チェックが危険な操作の前に必ず通るかの確認 |
| PDG(プログラム依存グラフ) | どの値がどこへ流れ、どの条件がどの操作を支配するか | 入力から危険な操作までの流れの追跡 |
ただし、これらの表現をツールの出力そのままで渡せばよいわけではありません。DOTやJSONのような形式には、可視化や機械処理のための記述が多く含まれます。そのままでは冗長で、LLMの推論にはかえって扱いにくくなります。
そこで、行番号、ノードの種類、コード断片、依存関係の要約だけを残し、行ごとのコンパクトなテキストに整えます。さらに、ファイル全体ではなく対象の関数に絞って書き出すことで、無関係な文脈が混ざりにくくなり、判断に必要な証拠が前面に出ます。
渡し方を変えるだけで精度が3割上がる
研究者らは、実在するオープンソースのC/C++コードから集めた脆弱性事例を使い、同じ判断タスクで入力の形だけを変えて比べました。
結果は、入力の作り方が精度を大きく左右することを示していました。
生ソースだけを渡した場合の精度は53.5%でした。これをグラフだけの入力に切り替えると、全体では74.4%まで上がりました。最も高い精度を示したのはAST+PDGの組み合わせで、83.2%です。生ソースだけの場合と比べると、29.7ポイントの改善になります。評価されたグラフ系の入力は、いずれも生ソースのみを上回りました。
| 入力の形 | 精度 |
|---|---|
| AST+PDG | 83.2% |
| AST+CFG | 80.4% |
| CFG+PDG | 73.8% |
| 全部入り(ソース+AST+CFG+PDG) | 73.5% |
| AST単体 | 72.1% |
| AST+ソース | 69.3% |
| PDG単体 | 69.0% |
| CFG単体 | 68.0% |
| PDG+ソース | 65.7% |
| 生ソースのみ | 53.5% |
表の数値は、有効な判定が返ったケースに限った正解率です。
ソースを足すほど精度が下がる
ここで特に意外なのは、グラフにソースを足しても精度が上がらない点です。情報量は増えているのに、結果はむしろ悪くなっています。
PDG単体の精度は69.0%でしたが、PDGにソースを足すと65.7%へ下がりました。AST+PDGは83.2%だった一方で、ASTにソースを足した入力は69.3%にとどまっています。ソース、AST、CFG、PDGをすべて入れた「全部入り」でも73.5%で、2種類のグラフを組み合わせた入力には届きませんでした。
しかも、ソースを足すほどコストも重くなります。
| 入力の形 | 平均文字数 | 平均入力トークン | 切り詰め率 |
|---|---|---|---|
| グラフだけ | 16,858 | 5,973 | 10.7% |
| ソースだけ | 34,666 | 10,767 | 43.9% |
| ソース+グラフ | 40,804 | 12,938 | 59.9% |

ソース+グラフの入力は、グラフだけの場合と比べて、文字数で2.4倍、入力トークンで2.2倍を必要としました。プロンプトの上限に当たって切り詰められる割合も、グラフだけでは10.7%だったのに対し、ソース+グラフでは59.9%に達しています。精度が伸びないだけでなく、最もコストの重い組み合わせになっています。
多く渡すほど、肝心の証拠が見えにくくなる。この現象は、コンテキスト希釈と呼べます。長い文脈の真ん中に置かれた情報ほど見落とされやすいという、これまでに知られている傾向ともつながっています。
情報を増やすと埋もれる2つの理由
コンテキスト希釈が起きる理由は、大きく2つに分けられます。
1つ目は、プロンプトの長さによる圧力です。入力が上限に近づくと、判断の手本として添えていた例が削られたり、コードやグラフの一部が切り詰められたりします。その結果、判断に使える手がかりの質が落ちます。
2つ目は、意味的な気の散りです。上限に収まっていたとしても、余計なソースがモデルの注意を別の場所へ引きつけることがあります。
実際に、次のような失敗例があります。ある関数では、メモリ確保の直前に「残り容量が必要数に足りるか」を確認するガードがあり、本来は安全と判断できるコードでした。ところがソースを足した入力では、同じファイル内の別ルーチンにあった「16 + 個数 × 20」のようなバッファサイズ計算が目に入り、モデルはそこを危険な箇所だと誤認しました。
このプロンプトは2万字前後で、上限の5万字にはまだ余裕がありました。つまり、切り詰めが原因ではありません。単純に、注意が本来見るべき場所からそれてしまったのです。
この例からわかるのは、入力を対象の関数に絞ることの重要性です。ファイル全体を渡すと、無関係なコードが、正しい判断に必要な証拠と注意を奪い合います。グラフだけの入力では対象メソッドに絞られていたため、本来見るべきガードと確保処理が前面に残っていました。
構造グラフが判断を助ける理由
構造グラフが精度を押し上げる理由は、主に3つあります。
1つ目は、コード上では暗黙になっている関係を明示できることです。ある値がどこで定義され、その後どこで危険な使われ方をしているのか。PDGはそのつながりを直接示します。生ソースだけでは、モデルが構文を読みながらその関係を組み立て直さなければなりません。
2つ目は、構文上のノイズを減らせることです。書式の違い、コメント、ヘルパー関数の宣言、判断に関係しない文などが抑えられ、構造として重要な証拠が目立ちやすくなります。
3つ目は、どこに注目すべきかの目印を与えられることです。行番号、ノードの種類、エッジのラベル、依存関係の要約があることで、モデルは危険な操作や関連する条件を追いやすくなります。
AST+PDGが最も強かったのは、この2つの表現が互いを補うからだと考えられます。ASTは、それぞれの場所で「何が起きているか」を示します。PDGは、その値が「どのように危険な操作へ届くか」を示します。両方がそろうと、脆弱性判断の核心である、入力から危険な操作までの流れを追いやすくなります。
AST+CFGが2番手だったのは、ガード構造や実行経路を捉えやすい一方で、値の依存関係までは十分に追いきれないためと見られます。CFG+PDGがやや劣ったのは、経路と依存の情報はあっても、ASTが持つ局所的な式や操作の手がかりを欠き、推論を特定のコード片に結びつけにくかったためだと考えられます。
脆弱性の種類で最適な渡し方は変わる
ここまでの傾向は、脆弱性の種類によっても少し変わります。
整数オーバーフローのような算術系の脆弱性では、グラフだけの入力が強く出ました。グラフだけでは77.8%だったのに対し、ソース+グラフでは65.8%に下がっています。入力から危険な演算までの流れを追うには、焦点を絞った構造証拠のほうが向いていると考えられます。
一方で、二重解放やuse-after-freeのようにメモリの扱いが関わる脆弱性では、ソース+グラフが競争力を見せました。二重解放のカテゴリでは、ソース+グラフが76.2%で最も高い結果になっています。クリーンアップの順序やポインタの生存期間をたどる場面では、ソース上の文脈が手がかりになる場合があると見られます。
| 脆弱性のカテゴリ | 生ソース | AST+PDG | グラフだけ | ソース+グラフ |
|---|---|---|---|---|
| 整数オーバーフロー(56件) | 53.8% | 84.6% | 77.8% | 65.8% |
| 二重解放(23件) | 50.0% | 72.7% | 64.6% | 76.2% |
| use-after-free(22件) | 52.4% | 85.7% | 73.3% | 75.4% |
件数の少ないカテゴリは参考程度に見る必要があります。ここでは、ある程度のデータがある3カテゴリだけを挙げています。万能の入力形式があるわけではありません。対象とする脆弱性の種類に合わせて、入力の形を選ぶのが現実的です。
モデルは「危険」を見落としやすい
判定結果には、ラベルによる偏りも見られました。安全なコードを安全と当てる精度は76.1%だった一方で、脆弱なコードを脆弱と当てる精度は66.0%でした。つまり、モデルは「安全」を当てるよりも、「危険」を見つけるほうを苦手としていました。
理由としては、明示的な検証パターンを認識するほうが簡単だからだと考えられます。境界チェックやnullチェックのように、そこに書かれているものは見つけやすい。一方で、欠けているガード、目立たないデータフロー、ポインタの生存期間のズレのような「ないもの」や「微妙なズレ」は見抜きにくくなります。曖昧な場合に、安全側へ倒す保守的な振る舞いも影響している可能性があります。
実務では、モデルが安全と答えた場合でも、そのまま鵜呑みにしないほうがよいでしょう。見逃しが起きやすい前提で、危険側の判定を人のレビューや静的解析で補う運用が現実的です。
この結果をどこまで一般化できるか
この知見を取り入れる前に、検証の前提も確認しておく必要があります。判定は1つのモデルに対して、1ケースあたり1回のクエリで行われています。別のモデルでは順位が変わる可能性があり、1〜3ポイント程度の小さな差は慎重に見るべきです。
対象はC/C++の5カテゴリ、合計107件でした。そのうち半数以上が整数オーバーフローです。つまり、今回の結果は主に、C/C++の算術系とメモリ系の脆弱性に関する証拠です。すべての言語、すべての脆弱性にそのまま当てはまる結論ではありません。
また、グラフの抽出に失敗した事例は評価から外されています。そのため、評価対象の選ばれ方に偏りが残っている可能性もあります。
さらに、プロンプトに添える例の有無も結果に影響しています。大きいプロンプトほど例が削られやすく、それ自体が精度を下げた可能性があります。したがって、グラフだけが有利に見える理由の一部には、この条件の違いも含まれているかもしれません。
スキルなしで試すプロンプトテンプレート
ここまでの考え方は、専用のスキルや静的解析ツールを用意しなくても、ある程度は手元で試せます。
もちろん、本物のASTやPDGを抽出するほうが安定します。ただ、まずは「ファイル全体を丸ごと渡さない」「対象の関数に絞る」「先に構造を取り出してから判断させる」という要点を押さえるだけでも、ソースの丸投げよりは狙いを絞ったレビューに近づけられます。
以下は、コピーして使えるプロンプトテンプレートです。対象の関数とコードを差し込んで、そのまま使えます。脆弱性の種類がわかっている場合は指定し、わからない場合は広めに確認する形で動かします。
あなたはコードのセキュリティレビューを担当します。次の制約と手順を厳密に守ってください。
【対象】
- 解析対象の関数: {関数名}
- 想定する脆弱性: {CWE種別。不明なら「指定なし。算術系とメモリ系の欠陥を広く確認」}
【制約】
- 解析するのは対象の関数の中だけ。同じファイルにある他の関数やルーチンは背景情報として扱い、そこにある計算や処理を欠陥として報告しない。
- まず構造を取り出し、それから判断する。コードを眺めた直感で答えない。
【手順】
1. シンクの特定。対象の脆弱性に対応する危険な操作を、行番号付きで挙げる。{シンクのヒント}
2. ソースの追跡。そのシンクに渡る値や状態が、どこから来ているか(引数・戻り値・外部入力など)をたどる。
3. 依存のつながり。ソースからシンクまで、値がどの行を経由して流れるかを線でつなぐ。
4. ガードの確認。シンクの手前に、境界チェック・サイズチェック・nullチェック・ポインタ状態の更新があるか。そのチェックが実行経路として必ずシンクの前を通るかを確かめる。
- 危険な操作の「後」に行う比較は、すでに起きた問題を防げない。前を通るガードだけを有効とみなす。
- 割り算や比較が、容量や上限を確かめるガードになっていることがある。単なる演算と取り違えない。
5. 判定。ソースからシンクへの経路を支配する有効なガードがあれば SAFE、なければ VULNERABLE。支配すると言い切れない場合は VULNERABLE 側に倒す。
【出力】
次のJSONだけを返してください。
{
"sink": "行番号と危険な操作",
"source": "危険な値・ポインタの出どころ",
"path": "ソースからシンクまでの流れ",
"guard": "見つかったチェックと、それがシンクを支配するか",
"verdict": "VULNERABLE / SAFE",
"confidence": 0.0,
"reason": "根拠を一文で"
}
【対象コード】
{ここに対象の関数を貼る。型や宣言が必要なときだけ最小限を添える}
{シンクのヒント} には、確認したい脆弱性に応じて次のような文を差し込みます。
| 脆弱性 | 差し込むヒント |
|---|---|
| 整数オーバーフロー・アンダーフロー | 加算・減算・乗算・シフトなどの算術演算、サイズや個数の計算に注目 |
| ヒープバッファオーバーフロー | 確保サイズの計算と、その領域への書き込みに注目 |
| 二重解放 | free / delete などの解放呼び出しに注目 |
| use-after-free | 解放後のポインタ参照や読み書きに注目 |
このテンプレートの狙いは、モデルに「構造を先に見る」動きを強制することです。対象の関数だけに絞ることで、別ルーチンの計算や処理に注意を奪われにくくなります。ソースからシンクまでの流れを明示させることで、構造グラフに近い証拠の形を作ります。さらに、危険な操作の後ろにあるチェックを無効とみなす注意書きを入れることで、よくある取り違えも避けやすくなります。
もう少し厳密に試すなら2段構えにする
検証で特に強かったのは、ソースそのものではなく、構造だけを見せる入力でした。これを手元で近づけるなら、1回目で構造サマリーを作り、2回目では元のソースを渡さずに、そのサマリーだけで判定させます。
手間は増えますが、余計なコードに注意がそれるリスクをさらに抑えられます。
1段目では、対象の関数を構造サマリーに変換させます。
次の関数を、行ベースの構造サマリーに変換してください。
各行を「行番号 | 操作の種類 | コードの要点」で一行ずつ書いてください。最後に、ソースからシンクへの依存関係を数本挙げてください。
元のコードは再掲せず、サマリーだけを出力してください。
【対象コード】
{対象の関数を貼る}
2段目では、元のソースを渡さず、構造サマリーだけで判定させます。
以下は、ある関数の構造サマリーです。元のソースは渡しません。
{CWE種別。不明なら「算術系とメモリ系を広く確認」} の観点で、VULNERABLE か SAFE かを判定してください。
手順
1. シンク(危険な操作)を特定する
2. ソースからシンクまでの流れをたどる
3. シンクの手前に、それを支配する有効なガードがあるか確認する。後ろのチェックは無効とみなす
4. 支配するガードがあれば SAFE、なければ VULNERABLE
出力は次のJSONだけにしてください。
{
"sink": "行番号と危険な操作",
"source": "危険な値・ポインタの出どころ",
"path": "ソースからシンクまでの流れ",
"guard": "見つかったチェックと、それがシンクを支配するか",
"verdict": "VULNERABLE / SAFE",
"confidence": 0.0,
"reason": "根拠を一文で"
}
【構造サマリー】
{1段目の出力を貼る}
この方法は、静的解析ツールが作る本物の依存グラフの代わりに、モデル自身に構造を組み立てさせるやり方です。手軽に試せる一方で、依存関係の取りこぼしは起こりえます。
重要なコードや本番のレビューでは、実際の解析ツールで構造を抽出するほうが安定します。まずはこのテンプレートで感触をつかみ、精度や再現性が必要な場面では、専用のスキルや解析ツールに切り替える。そうした使い分けが現実的です。
まとめ
LLMにコードの安全性を判断させるとき、結果を左右するのは情報の量ではなく、渡し方。生ソースを丸ごと渡すよりも、静的解析で構造を抽出し、対象の関数に絞ったコンパクトなグラフとして渡すほうが、精度とコストの両面で有利になりやすいことが示されています。
ソースを足せば常に良くなるわけではありません。むしろ、余計な文脈によって重要な証拠が埋もれ、モデルの注意が別の場所へ向かうことがあります。出発点としてはAST+PDGが有力ですが、脆弱性の種類や使うモデルによって最適な形は変わります。
静的解析は、欠陥を見つけるためだけの道具ではありません。LLMに渡す入力を整えるための前処理として使うことで、モデルの判断を助けるプロンプト設計の一部にもなります。
お知らせ
今回の内容をもとに、コードをLLMに渡す前の下ごしらえを自動化するスキルを用意しました。
対象の関数だけを取り出し、AST+PDGの構造表現に変換し、行ごとのコンパクトなテキストに整えてからモデルへ渡す。この流れを、再利用しやすい形にまとめています。
ソースコードをそのまま丸投げするのではなく、判断に必要な証拠だけが前に出る入力をつくる。記事で紹介した考え方を、手元の作業にそのまま持ち込めるようにするための仕組みです。
本記事の関連研究