コンテンツにスキップ

govulncheck はなぜ賢いのか

コールグラフで変わる脆弱性検出の世界

Go Conference 2026 — yoshioka0101

自己紹介

yoshioka0101
  • バックエンドエンジニア
  • Go で REST API を書いている
  • Go のソースコードを読むのが好き
※ Go コアチームではありません。
一次資料(ソース・issue・blog)を読んで理解したことを話します。

今日話すこと

1. naive な脆弱性チェックの問題
go.sum を見るだけでは何が足りないか
2. govulncheck のアプローチ
reachability とは何か
3. 内部実装を読む
SSA・コールグラフ・interface 問題

あなたの go.sum に CVE がある

$ go list -m -json all | govulncheck -json -

# もしくは単純に
$ grep "golang.org/x/crypto" go.sum
golang.org/x/crypto v0.12.0 h1:...
CVE-2023-XXXX: golang.org/x/crypto < v0.17.0
脆弱な関数が含まれています
→ 本当に危険ですか?

naive な脆弱性チェックの問題

❌ パッケージベースのチェック

「このパッケージを使っている」
→「脆弱性がある」

問題:
脆弱な関数を実際に呼んでいるかは関係ない
→ 大量の false positive

✅ govulncheck のアプローチ

「脆弱な関数が
あなたのコードから呼ばれているか」
を判定する

= reachability analysis


reachability とは

あなたのコード
  └─ http.HandleFunc
       └─ handler()
            └─ bcrypt.GenerateFromPassword()   ← crypto を使っている
                 └─ (内部実装)

脆弱な関数: ssh.NewClientConn()               ← 呼ばれていない
govulncheck の判定
ssh.NewClientConn() はあなたのエントリポイントから到達不可能
この CVE はあなたには影響しない

では、どうやって判定するのか

🔍
コールグラフ (Call Graph)
「どの関数がどの関数を呼ぶか」を
プログラム全体で静的に解析した有向グラフ

govulncheck の内部実装:全体像

golang.org/x/vuln
├── cmd/govulncheck/      エントリポイント
├── internal/vulncheck/   コア分析ロジック
│   ├── source.go         ソース解析モード
│   └── binary.go         バイナリ解析モード
└── internal/callgraph/   コールグラフ構築
コアは internal/vulncheck/source.go
ここで「SSA に変換 → コールグラフ構築 → 脆弱関数への到達判定」が行われる

SSA(Static Single Assignment)

通常の Go コード
x := 1
x = x + 1
x = x * 2
SSA 形式
x₁ = 1
x₂ = x₁ + 1
x₃ = x₂ * 2
変数への代入が一度だけ。
どの値がどこから来たかが自明になる → 静的解析がしやすい

SSA → コールグラフへ

// SSA に変換した後、各 Call 命令を収集する
for _, block := range fn.Blocks {
    for _, instr := range block.Instrs {
        if call, ok := instr.(ssa.CallInstruction); ok {
            // 呼び出し先を記録 → グラフのエッジを追加
        }
    }
}
internal/callgraph/ で CHA (Class Hierarchy Analysis) または
VTA (Variable Type Analysis) を使ってグラフを構築する

interface が難しい

type Store interface {
    Get(key string) ([]byte, error)
}

func process(s Store) {
    s.Get("key")  // どの実装を呼ぶ?
}
問題
interface 越しの呼び出しは、静的解析だけでは呼び先が特定できない
→ 全実装を候補にすると false positive が増える
→ 実装を絞りすぎると false negative が増える

VTA(Variable Type Analysis)で解く

process() が呼ばれる文脈を追う

  main() → process(&RedisStore{})
         → process(&MemStore{})

  ↓ VTA はこれを収集する

  s.Get() の候補 = { RedisStore.Get, MemStore.Get }
                   ↑ MockStore.Get は候補から外れる
プログラム全体のデータフローを見て、実際に渡される型だけに絞る
→ false positive を減らせる

source モード vs binary モード

source モード
  • ソースコードがある
  • SSA + VTA で精密なコールグラフ
  • 行レベルで脆弱な呼び出しを特定
  • 精度: 高い
binary モード
  • コンパイル済みバイナリのみ
  • シンボルテーブルから関数を抽出
  • コールグラフは作れない
  • 精度: 低い(false positive あり)

自分のコードで試す

$ govulncheck ./...

Vulnerability #1: GO-2023-1840
    Timing sidechannel attack in golang.org/x/crypto/ssh
  More info: https://pkg.go.dev/vuln/GO-2023-1840
  Module: golang.org/x/crypto
    Found in: golang.org/x/crypto/ssh@v0.12.0
    Fixed in: golang.org/x/crypto@v0.17.0
    Call stacks in your code:
        main.go:34:18: myapp.NewServer calls ssh.NewClientConn
                                         これがあると危険
Call stacks が出る = 実際に到達可能
Call stacks が出ない = パッケージはあるが影響なし

まとめ:govulncheck はなぜ賢いのか

1️⃣
ソースを SSA に変換する
データフローが追いやすい中間表現へ
2️⃣
VTA でコールグラフを構築する
interface 越しの呼び出しを型情報で絞り込む
3️⃣
脆弱な関数への到達可能性を判定する
呼ばれていない脆弱性は報告しない

Go Far, Go Together

ツールを使うだけでなく
「なぜこう動くのか」を知ること
それが、チームで安全に遠くまで行く力になる
golang.org/x/vuln — ソースを読んでみてください

参考資料

公式
  • go.dev/blog/vuln — govulncheck 設計ブログ
  • go.dev/blog/govulncheck — 詳細解説
  • github.com/golang/vuln — ソースコード
  • pkg.go.dev/golang.org/x/vuln — ドキュメント
内部実装を読む入口
  • golang.org/x/tools/go/ssa — SSA 変換
  • golang.org/x/tools/go/callgraph/vta — VTA アルゴリズム
  • golang.org/x/tools/go/callgraph/cha — CHA アルゴリズム