
コンフィグベースCVEマッチング for Linuxカーネル
English version here.
この記事はOSS Summit Japan 2022にて発表したConfig Based CVE Matching for Linux Kernelのブログバージョンです。
組込みLinux業界では、ソフトウェアの脆弱性を修正するためCVE検索と呼ばれる手法が広く用いられています。 パッケージ名とバージョン番号でNVDを検索し、そのパッケージのCVEリスト(脆弱性リスト)を作成します。 実際の製品でのユースケースやビルド設定をもとにCVEの影響を調査し、影響を受けると判断されたら、修正パッチをcherry-pickして対応します。
ここで問題となるのは、Linuxカーネルは他のソフトウェアと比べて報告されるCVEが多すぎるということです。 他のソフトウェアはおよそ0~10件/年ですが、Linuxカーネルは約100件/年のペースでCVE-IDが発行されています。
cherry-pickは良くない、最新LTSを使おう、と言われますが、組込みデバイスでこの方針は現実的に不可能です。 LTSカーネルはおよそ毎週2回リリースされており、追いつけません。 典型的な開発環境では、ほとんどのパッチはビルド対象外となるため、すべてのリリースに追従する必要はありません。 使用しているカーネルがCVEの影響を受けないと判定できれば、アップデート頻度を下げることができます。
本稿では、LinuxカーネルがCVEの影響を受けるかどうか調べる2種類の方法を紹介します。
トリアージ手法1: Ubuntu CVE Trackerのデータを使って脆弱なバージョン範囲を特定する
LinuxカーネルのCVEは、大抵そのバグが修正されたメジャーバージョンしかNVDに登録されていません。
カーネル5.15.y(5.15は2021/10リリース)について調べるケースを考えます。 2021/11以降に発見された脆弱性はLinux 5.16以降で修正されることになるので、NVDには「linux < 5.16が影響を受ける」と登録されます。 CVE検索ツールはLinux 5.16以降に実装された新機能に対する脆弱性であっても、NVDの情報をもとに5.15.yも影響を受けると判定します。 CVEの影響を受ける正確なバージョン範囲の情報が必要です。

Ubuntu CVE Trackerが各CVEのバグを導入したコミットと修正したコミットの情報を提供しています。 これらをbreak-commit, fix-commitと呼ぶことにします。 例えばCVE-2021-45480のbreak-commit, fix-commitは https://ubuntu.com/security/CVE-2021-45480 で確認できます。 このページはマシンリーダブルなテキストファイルから生成されており、元ファイルはここにあります: https://git.launchpad.net/ubuntu-cve-tracker/tree/active/CVE-2021-45480
Patches_linux:
break-fix: aced3ce57cd37b5ca332bcacd370d01f5a8c5371 5f9562ebe710c307adc5f666bf1a2162ee7977c0
CVE-2021-45480について、バグはaced3ce57cd37b5ca332bcacd370d01f5a8c5371で導入され5f9562ebe710c307adc5f666bf1a2162ee7977c0で修正されたことがわかります。 これらはメインラインのコミットIDです。以下のコマンドでこれらのコミットIDのリリースバージョンを特定できます。
$ git remote -v
origin https://kernel.googlesource.com/pub/scm/linux/kernel/git/stable/linux.git (fetch)
origin https://kernel.googlesource.com/pub/scm/linux/kernel/git/stable/linux.git (push)
$ git fetch
$ git commit-graph write --reachable
$ git tag --contains aced3ce57cd37b5ca332bcacd370d01f5a8c5371 | sort -V | head -1
v5.13
$ git tag --contains 5f9562ebe710c307adc5f666bf1a2162ee7977c0 | sort -V | head -1
v5.16
git commit-graph
はスキップできますが、これを実行しておくことでgit tag --contains
が劇的に高速化します。
この場合、CVE-2021-45480のバグは5.13で混入し5.16で修正されたことがわかります。 5.15.yカーネルを使用している場合このCVEの影響を受ける可能性があります。 5.15はまだメンテナンスされているので修正コミット5f9562ebe710c307adc5f666bf1a2162ee7977c0はlinux-5.15.yブランチにバックポートされているはずです。 5.15.yブランチでCVEの影響を受けるyの範囲を特定するには、修正コミットがバックポートされたマイナーバージョンを特定する必要があります。 stableカーネルのメンテナンスルールによると、パッチをバックポートする際はコミットログに対応するメインラインのコミットIDを記入しなければならないことになっています。
The upstream commit ID must be specified with a separate line above the commit text, like this:
commit <sha1> upstream.
https://www.kernel.org/doc/html/v5.15/process/stable-kernel-rules.html
修正コミット5f9562ebe710c307adc5f666bf1a2162ee7977c0がlinux-5.15.yブランチにバックポートされているなら、コミットログをアップストリームのコミットID5f9562ebe710c307adc5f666bf1a2162ee7977c0で検索してバックポートしたコミットを特定できます。
$ git log --grep=5f9562ebe710c307adc5f666bf1a2162ee7977c0 v5.15..linux-5.15.y
commit 68014890e4382ff9192e1357be39b7d0455665fa
Author: Hangyu Hua <hbh25y@gmail.com>
Date: Tue Dec 14 18:46:59 2021 +0800
rds: memory leak in __rds_conn_create()
[ Upstream commit 5f9562ebe710c307adc5f666bf1a2162ee7977c0 ]
__rds_conn_create() did not release conn->c_path when loop_trans != 0 and
trans->t_prefer_loopback != 0 and is_outgoing == 0.
Fixes: aced3ce57cd3 ("RDS tcp loopback connection can hang")
Signed-off-by: Hangyu Hua <hbh25y@gmail.com>
Reviewed-by: Sharath Srinivasan <sharath.srinivasan@oracle.com>
Signed-off-by: David S. Miller <davem@davemloft.net>
Signed-off-by: Sasha Levin <sashal@kernel.org>
$ git tag --contains 68014890e4382ff9192e1357be39b7d0455665fa | sort -V | head -1
v5.15.11
CVE-2021-45480はlinux-5.15.yブランチでは5.15.11で修正されたことがわかりました。 CVE-2021-45480の影響を受けるカーネルバージョンは以下の通りです。
- 5.13 <= linux < 5.16 (メインライン)
- 5.15 <= linux < 5.15.11 (linux-5.15.yブランチ)
本稿ではコミットIDでブランチの履歴をgrepしましたが、自動化に使えるもっと実用的な正規表現がCIPプロジェクトのリポジトリにあります。
# https://gitlab.com/cip-project/cip-kernel/cip-kernel-sec/-/blob/master/scripts/import_stable.py
RE_USE = {'hash': r'[0-9a-f]{40}'}
BACKPORT_COMMIT_TOP_RE = re.compile(
r'^(?:' r'commit ({hash})(?: upstream\.?)?'
r'|' r'\[ [Uu]pstream commit ({hash}) \]'
r'|' r'\[ commit ({hash}) upstream \]'
r')$'
.format(**RE_USE))
BACKPORT_COMMIT_ANYWHERE_RE = re.compile(
r'^(?:' r'\(cherry[- ]picked from commit ({hash})\)'
r'|' r'\(backported from(?: commit)? ({hash})\b.*' # Ubuntu
r')$'
.format(**RE_USE))
トリアージ手法2: 脆弱なコードがコンパイルされるかどうかkoverageコマンドで確認する
Linuxカーネルの脆弱性はほとんどarch
かdrivers
のものです。 組込みLinuxプラットフォームではターゲットデバイスに合わせてビルド設定を最適化するので、ほとんどの脆弱性はビルド対象外として無視することになります。

koverageコマンドでパッチがコンパイル対象になるかどうか調べることができます。 CVEのバグを導入したコミットIDがわかっているので、そのパッチがコンパイルされないなら、そのカーネルはCVEの影響を受けないと判定できます。
CVE-2021-45480の場合、以下のコマンドでbreak-commitがビルド対象かどうか判定できます。
$ pip install kmax
$ cd linux-5.15.4
$ git revert aced3ce57cd37b5ca332bcacd370d01f5a8c5371
$ git revert HEAD
$ git format-patch HEAD^
$ export CROSS_COMPILE=aarch64-linux-gnu-
$ koverage --config ../config.txt \
--arch arm64 \
--check-patch 0001-Revert-Revert-RDS-tcp-loopback-connection-can-hang.patch \
--cross-compiler make \
-o result.json
上記のコマンドでgit-revert
を2回実行していますが、これはhunk offsetを修正するためです。 koverageコマンドは行番号によって判定を行うため、hunk offsetがない状態のdiffが必要です。
koverageコマンドに--check-patch
でパッチファイルを渡すと、まずunified diffフォーマットをパースしてパッチが編集しようとしている「ファイル名:行番号」のリストを作ります。 次にカーネルのソースコードに行番号のマーカーを挿入し、Cプリプロセッサを実行します。 プリプロセッサによって行番号のマーカーが削除されたら、その行はコンパイルされないと判定します。
上記のコマンドではkoverageコマンドはレポートをresult.json
に生成します。これは以下のような内容です。
{
"headerfile_loc": {
"net/rds/tcp.h": [
[61, "FILE_EXCLUDED"],
[62, "FILE_EXCLUDED"],
[63, "FILE_EXCLUDED"]
]
},
...
すべての行がFILE_EXCLUDED
またはLINE_EXCLUDED_FILE_INCLUDED
なら、break-commitによって導入されたコードはコンパイルされないので、カーネルはCVEの影響を受けないと判定できます。
トリアージフロー
まとめると、CVEトリアージの手順は以下のようになります。手動でも実行できるし、自動化もできます。
- まずUbuntu CVE Trackerのリポジトリでbreak-fix commitを調べます。例: https://git.launchpad.net/ubuntu-cve-tracker/tree/active/CVE-2021-45480
git tags --contains
コマンドでこのCVEの影響を受けるメインラインのバージョン範囲を調べます。このバージョン範囲外なら、そのカーネルはCVEの影響を受けないと判定します。- 使用しているstableブランチでの修正コミットを
git log --grep
コマンドで調べます。 - 使用しているstableブランチでこのCVEの影響を受けるバージョン範囲を
git tags --contains
コマンドで調べます。このバージョン範囲外なら、そのカーネルはCVEの影響を受けないと判定します。 - break-commitがコンパイルされるかどうか
koverage
コマンドで調べます。もしbreak-commitで編集されるすべての行がコンパイル対象外なら、そのカーネルはCVEの影響を受けないと判定します。
ベンチマーク1
本稿で紹介した2つの手法を評価するため、ベンチマークをとりました。

3つのカーネルバージョンに対してCVE検索ツール(go-cve-dictionary)でCVEリストを生成し、上記2手法でCVEを分類しました。
- トータル高さはCVE検索ツールが報告したCVEの数を表します
- 濃い緑と黄色の領域はトリアージ手法1によって偽陽性と判定されたCVEの数を表します
- 黄色と明るい緑の領域はトリアージ手法2によって偽陽性と判定されたCVEの数を表します
- 黄色の領域はトリアージ手法1, 2両方によって偽陽性と判定されたCVEの数を表します
- 3つのカーネルバージョン5.4.221, 160, 80はリリース日が約1年間隔となるように選びました
- トリアージ手法2で使用する
.config
はarm64のdefconfig
を使用しました

リアージ手法1によって偽陽性と判定されたCVEの数はリリース日に対しておよそ線形になります。 NVDは脆弱性を修正したメジャーバージョンしか記録していないので、CVE検索ツールは異なるマイナーバージョンに対してほぼ同じCVEリストを生成します。 しかし、Linuxカーネルには毎年およそ100件のCVEが報告され修正されるので、偽陽性が毎年100件増え続けることになります。

リアージ手法2によって偽陽性と判定されたCVEの数はマイナーバージョンに対しておよそ一定です。 リアージ手法2によって偽陽性と判定されるかどうかは.config
とCVE検索ツールが生成するCVEリストの内容によりますが、前述のとおり、これらはマイナーバージョンで変化しません。
ベンチマーク2
メンテナンスが継続しているLTSカーネルの最新バージョンに対して同じベンチマークをとりました。

- 最新バージョンなので当然深刻なバグはすべて修正済みであり、80%以上が偽陽性と判定されています
- 古いメジャーバージョンほどCVEリストが大きくなっているのは、NVDにメジャーバージョンしか記録されておらずCVE検索ツールもこれに従うからです
まとめ
- 最新のLTSカーネルはほとんどのCVEが修正済みであるにも関わらず、CVE検索ツールは数百のCVEを誤って報告します。
- 古いカーネルでは、CVEの一部(今回のケースでは20%)は
.config
を考慮してパッチがビルド対象かどうか調べることで偽陽性と判定できます
参考文献
- https://sched.co/1D14m (プレゼンバージョン)
- https://nvd.nist.gov/
- https://git.launchpad.net/ubuntu-cve-tracker
- https://github.com/vulsio/go-cve-dictionary
- https://github.com/paulgazz/kmax
- https://www.kernel.org/doc/html/v5.15/process/stable-kernel-rules.html
- https://gitlab.com/cip-project/cip-kernel/cip-kernel-sec/