序章:なぜ「静的解析の自動化」だけではレガシーコードの腐敗を防げないのか
テックリードとして大規模なC言語コードベースを管轄していると、避けて通れない問題がある。それは、どれほど厳格なコーディング規約をドキュメント化し、プルリクエストのレビュープロセスを自動化(CI/CDでのcppcheckやSonarQubeの導入)しても、「開発者のローカル手元でのコンパイル時」に検知されない限り、危険な関数は必ずコードベースに混入するという残酷な現実だ。
特に、`strcpy`や`sprintf`、あるいはレガシーな独自I/Oラッパーといった「知っていれば使わないが、コピペで蔓延する関数」は、コードレビューの網をすり抜け、ステージング環境でのメモリ破壊(Segmentation Fault)や、最悪の場合はセキュリティ脆弱性として顕現する。
CIパイプラインでエラーを検知してビルドを落とすアプローチは、フィードバックループが遅すぎる。真に生産性の高い開発環境とは、「開発者がエディタ上でコードを書き、保存し、コンパイルボタンを押した(あるいはLSPがバックグラウンドで走った)そのコンマ数秒の瞬間に、コンパイラ自身が冷徹にレッドカードを突きつける」仕組みである。
今回は、LLVM/Clangの強大なるAST(抽象構文木)マッチャーとプラグイン機構を直接ハックし、「特定の関数呼び出しをビルド時に完全ブロックするカスタム診断(Diagnostics)プラグイン」を自作する実践手法を解説する。ネットの海を漂うチュートリアルとは一線を画し、実務のプロダクション環境に即座に組み込めるレベルの堅牢な実装と、チーム全体の開発体験(DX)を跳ね上げるエコシステムの構築法を授けよう。
—
1. アーキテクチャの全貌:Clangプラグインはいかにしてコードを検知するか
Clangは、単なるソースコードの翻訳機(コンパイラ・フロントエンド)ではない。それは「プログラムの構造を完全に把握するための超高精度なクエリエンジン」でもある。
Clangプラグインのライフサイクルは、おおむね以下のパイプラインで構成されている。
1. AST(抽象構文木)の生成: ソースコードは字句解析・構文解析を経て、C言語の構文木(AST)へと変換される。
2. AST Matchersによるパターンマッチング: 開発者が定義した「危険なノードの構造」に合致する箇所を、AST全体から高速に検索する。
3. DiagnosticEngineによる警告・エラーの送出: マッチしたノードに対して、コンパイルエラー(あるいは警告)を発行し、ビルドを中断させる。
今回は、AST MatchersのAPIを活用し、`target_function` という名前の特定の関数呼び出しノードを発見した瞬間に、コンパイルプロセスを強制終了させるプラグイン `NoForbiddenFuncPlugin` を実装する。
—
2. 開発環境の構築とビルドシステムのベストプラクティス
Clangプラグインの開発には、LLVM/Clangのソースツリー、あるいはインストール済みの開発用ヘッダ(`libclang-dev`など)が必要だ。しかし、毎回LLVM全体を数時間かけてビルドするのは時間の無駄である。
ここでは、すでにシステムにインストールされているClangのライブラリを利用し、CMakeを使ってミニマムかつ高速にビルドできるプロジェクト構造を構築する。
2.1 ディレクトリ構成
clang-forbidden-plugin/
├── CMakeLists.txt # プラグインビルド用のCMake設定
└── ForbiddenFuncPlugin.cpp # プラグインのコアロジック
2.2 `CMakeLists.txt` の完全実装
以下の設定ファイルは、LLVM/ClangのCMakeモジュールを適切にインポートし、共有ライブラリ(`.so` または `.dylib`)としてプラグインをコンパイルするためのベストプラクティスである。
cmake_minimum_required(VERSION 3.16.0)
project(ForbiddenFuncPlugin)
C++17規格を強制(LLVM/Clangの近代的なAPIを使用するため必須)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
LLVMとClangのCMakeパッケージをシステムから探索
find_package(LLVM REQUIRED CONFIG)
find_package(Clang REQUIRED CONFIG)
LLVMの定義済みマクロやコンパイルフラグを取り込む
include_directories(${LLVM_INCLUDE_DIRS})
include_directories(${CLANG_INCLUDE_DIRS})
add_definitions(${LLVM_DEFINITIONS})
プラグイン本体をモジュールライブラリ(共有ライブラリ)として定義
add_library(ForbiddenFuncPlugin MODULE
ForbiddenFuncPlugin.cpp
)
LLVM/Clangのコアライブラリをリンク
target_link_libraries(ForbiddenFuncPlugin PRIVATE
clangAST
clangASTMatchers
clangBasic
clangFrontend
clangSerialization
clangTooling
)
—
3. 実装:AST Matcherによる関数呼び出しのインターセプト
ここからが本題だ。`ForbiddenFuncPlugin.cpp` の実装コードを記述する。
このコードでは、`gets` や `strcpy` といったC言語の致命的な脆弱性を持つ関数をターゲットにし、検出時にコンパイルエラー(`diag::err_builtin_definition_redefinition` などを流用、またはカスタム診断IDを発行)を出力する。
3.1 `ForbiddenFuncPlugin.cpp` の全コードと詳細解説
include “clang/AST/AST.h”
include “clang/AST/ASTConsumer.h”
include “clang/ASTMatchers/ASTMatchFinder.h”
include “clang/ASTMatchers/ASTMatchers.h”
include “clang/Frontend/CompilerInstance.h”
include “clang/Frontend/FrontendPluginRegistry.h”
include “clang/Basic/Diagnostic.h”
using namespace clang;
using namespace clang::ast_matchers;
namespace {
// 禁止したい関数名のリスト(必要に応じて拡張可能)
const std::vector
// 1. AST Matcherのヒット時に呼び出されるコールバッククラス
class ForbiddenFuncCallback : public MatchFinder::MatchCallback {
private:
CompilerInstance &CI;
public:
explicit ForbiddenFuncCallback(CompilerInstance &CI) : CI(CI) {}
void run(const MatchFinder::MatchResult &Result) override {
// マッチしたASTノードからCallExpr(関数呼び出し式)を取得
if (const auto Call = Result.Nodes.getNodeAs
if (const auto Decl = Call->getDirectCallee()) {
std::string FuncName = Decl->getNameAsString();
// 登録された禁止関数リストに含まれているかチェック
for (const auto &Forbidden : kForbiddenFunctions) {
if (FuncName == Forbidden) {
// 診断エンジン(DiagnosticEngine)を取得し、カスタムエラーを発行
DiagnosticsEngine &Diags = CI.getDiagnostics();
// 一意なエラーIDをその場で定義(実務ではあらかじめDiagnosticsファイルで定義推奨)
unsigned DiagID = Diags.getCustomDiagID(
DiagnosticsEngine::Error,
“Security Violation: Use of forbidden function ‘%0’ is strictly prohibited in this codebase.”
);
// ソースコード上の該当位置と、エラーメッセージ内のプレースホルダー(%0)に関数名を渡す
Diags.Report(Call->getBeginLoc(), DiagID) << FuncName;
}
}
}
}
}
};
// 2. AST Consumer:コンパイルの各フェーズでMatcherを実行するオーケストレータ
class ForbiddenFuncASTConsumer : public ASTConsumer {
private:
MatchFinder Finder;
ForbiddenFuncCallback Callback;
public:
explicit ForbiddenFuncASTConsumer(CompilerInstance &CI) : Callback(CI) {
// 関数呼び出し(CallExpr)かつ、呼び出し先が関数宣言であるノードをキャプチャするマッチャーを構築
Finder.addMatcher(
callExpr(callee(functionDecl())).bind("call"),
&Callback
);
}
void HandleTranslationUnit(ASTContext &Context) override {
// 翻訳単位(ソースファイル全体)のASTに対してマッチングを実行
Finder.matchAST(Context);
}
};
// 3. プラグインのエントリーポイント(Clangのフロントエンドアクション)
class ForbiddenFuncAction : public PluginASTAction {
protected:
std::unique_ptr
return std::make_unique
}
bool ParseArgs(const CompilerInstance &CI, const std::vector
// プラグイン引数をパースする場合はここに記述(今回はデフォルトで全禁止)
return true;
}
};
} // namespace
// 4. Clangのプラグインレジストリに登録
static FrontendPluginRegistry::Add
X(“forbidden-func-checker”, “Reject compilation if forbidden functions are used”);
—
4. 実行検証:コンパイラが「拒絶」する瞬間を見る
プラグインをビルドし、実際にCのソースコードをコンパイルしてその効果を検証しよう。
4.1 プラグインのビルド
ビルドディレクトリの作成とコンパイル
mkdir build && cd build
cmake -DCMAKE_BUILD_TYPE=Release ..
make
これにより、`libForbiddenFuncPlugin.so`(macOSの場合は `.dylib`)が生成される。
4.2 テスト用ソースコードの用意 (`test.c`)
わざと禁忌とされる `strcpy` を使ったコードを用意する。
include
void vulnerable_function(const char input) {
char buffer[16];
// ここで危険な関数を呼び出す
strcpy(buffer, input);
}
int main() {
vulnerable_function(“hello”);
return 0;
}
4.3 プラグインを読み込ませたコンパイルの実行
Clangに対して `-fplugin` オプションと `-Xclang -load` を渡し、自作プラグインをロードする。
clang -fplugin=./libForbiddenFuncPlugin.so -Xclang -load -Xclang ./libForbiddenFuncPlugin.so -c test.c
【実行結果(コンソール出力)】
test.c:6:5: error: Security Violation: Use of forbidden function ‘strcpy’ is strictly prohibited in this codebase.
strcpy(buffer, input);
^~~~~
1 error generated.
見事に、コード生成フェーズに移行する前の段階でClangのフロントエンドがソースコードのASTを解析し、ビルドを強制停止させた。開発者はエディタの保存と同時にこのエラーを視認できるため、レビューを待つ必要すらない。
—
5. チーム開発の生産性を爆発させる「設定の共通化・隠しテクニック」
個人のローカル環境でプラグインが動くだけでは、テックリードの仕事としては半分だ。チーム全員がこのルールを強制され、かつ「意識せずに」恩恵を受けられる仕組みを構築して初めて真のDXが実現する。
5.1 LSP (clangd) との統合でエディタ上にリアルタイム警告を出す
VS CodeやNeovimでC/C++開発を行う際、LSPサーバとして `clangd` を使用しているケースがほとんどだろう。コンパイル時だけでなく、タイピングしているその瞬間にエディタ上で赤波線(Diagnostics)を出すためには、コンパイルオプション(`-fplugin` 等)を `clangd` に認識させる必要がある。
プロジェクトのルートディレクトリに `compile_flags.txt` を配置し、チーム全体で共有(Git管理)する。
compile_flags.txt
プロジェクト全体で常に適用するコンパイルフラグ
-std=c11
-Wall
-Wextra
自作プラグインのパスを相対パスで指定し、clangdのバックグラウンド解析に組み込む
-fplugin=./build/libForbiddenFuncPlugin.so
-Xclang
-load
-Xclang
./build/libForbiddenFuncPlugin.so
これにより、VS Codeの `clangd` 拡張機能は、ソースコードを保存する前の入力段階でバックグラウンドコンパイルを走りませ、開発者のエディタ上に即座にエラーを表示するようになる。
5.2 CI/CDパイプライン(GitHub Actions)での完全ガード
ローカルでの回避を防ぐため、GitHub ActionsなどのCI環境でも同一のプラグインをビルド・実行し、PRのマージを物理的にブロックする。
.github/workflows/security_check.yml
name: Clang AST Security Check
on:
pull_request:
branches: [ main, develop ]
jobs:
static-analysis:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Install LLVM and Clang Development Packages
run: |
sudo apt-get update
sudo apt-get install -y clang libclang-dev cmake build-essential
- name: Build Custom Clang Plugin
run: |
mkdir build && cd build
cmake -DCMAKE_BUILD_TYPE=Release ..
make
- name: Compile Source Code with Plugin Enforcement
run: |
# プロジェクト内の全ソースコードに対してプラグインを適用してビルドテスト
clang -fplugin=./build/libForbiddenFuncPlugin.so \
-Xclang -load -Xclang ./build/libForbiddenFuncPlugin.so \
-c src/vulnerable_check_target.c
このCIパイプラインを導入すれば、「うっかりレビューをスルーしてマージしてしまった」というヒューマンエラーは二度と発生しなくなる。
—
結び:コンパイラを味方につける者だけが、真の高速開発を手に入れる
多くの開発現場では、コンパイラは「コードの重箱の隅をつついてエラーを吐く厳しいお目付役」として捉えられている。しかし、Clangのプラグイン機構とAST Matchersを飼いならしたエンジニアにとって、コンパイラは「チーム独自のコーディング規約やセキュリティポリシーを寸分狂わず強制してくれる、最強の自動化エンジン」に変わる。
人間によるコードレビューは、アーキテクチャの設計やビジネスロジックの妥当性評価といった、人間にしかできない高次元の創造的作業に集中させるべきだ。「危険な関数のチェック」といった機械的なルールは、今回解説したカスタムコンパイラプラグインにすべて肩代わりさせよう。それこそが、複雑化するソフトウェア開発の荒波を乗りこなし、圧倒的な開発スピードと品質を両立させる唯一にして最大の近道である。