Clang開発版にlibFuzzerが新しいサニタイザとして取り込まれた。clang -fsanitize=fuzzer
で使える。
ファジングとは
ファジング (fuzzing) はテスト手法のひとつ。おかしなデータを自動的に大量生成してプログラムへ入力し、クラッシュを誘発することでバグをあぶりだす。
libFuzzerはファジングをおこなうためのライブラリ。
trunk版のclangをインストール
Clangのtrunk版をインストール:
svn co http://llvm.org/svn/llvm-project/llvm/trunk llvm
svn co http://llvm.org/svn/llvm-project/cfe/trunk llvm/tools/clang
svn co http://llvm.org/svn/llvm-project/compiler-rt/trunk llvm/projects/compiler-rt
mkdir build
cd build
cmake ../llvm -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=~/opt/llvm-trunk
make -j
make install
cp lib/libLLVMFuzzer.a ~/opt/llvm-trunk/lib
必要なライブラリ (libLLVMFuzzer.a) がビルドだけされてインストールされなかったので、手動でコピーした。
Fuzzerを使ってみる
テスト対象
文字列中の括弧(){}[]
の開閉が正しく対応しているかどうか調べる関数check_parens()を書いて、テストしてみる。この実装にはセグフォをおこすようなバグが入ってる。
#include <stack>
#include <cstddef>
#include <cstdint>
bool check_parens(char const* text, std::size_t length)
{
std::stack<char> parens;
for (std::size_t i = 0; i < length; ++i) {
switch (char const ch = text[i]) {
case '(':
case '{':
case '[':
parens.push(ch);
break;
case ')':
case '}':
case ']':
if (parens.top() != ch) {
return false;
}
parens.pop();
break;
default:
break;
}
}
return true;
}
extern "C"
int LLVMFuzzerTestOneInput(std::uint8_t const* data, std::size_t size)
{
check_parens(reinterpret_cast<char const*>(data), size);
return 0;
}
末尾の関数LLVMFuzzerTestOneInput()はlibFuzzerとテスト対象とのインターフェイス。自動生成されたデータがどんどんこの関数に渡される。戻り値は将来のために予約されていて、現在はゼロを返すきまり。
コンパイル
で、サニタイザとしてfuzzerを指定してコンパイル。fuzzerはクラッシュを引き起こすだけなので、どこでクラッシュしたのかを特定するためにAddress Sanitizerと-gオプションもつける。
~/opt/llvm-trunk/bin/clang++ -std=c++14 -fsanitize=address,fuzzer -g -o fuzzer test.cc
実行
生成されたfuzzerを実行するとクラッシュする。
% ./fuzzer
...
...
==2573==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc 0x000000553034 bp 0x7ffc33bdf3f0 sp 0x7ffc33bdf160 T0)
==2573==The signal is caused by a READ memory access.
==2573==Hint: address points to the zero page.
#0 0x553033 in check_parens(char const*, unsigned long) /tmp/test.cc:18:24
#1 0x55341e in LLVMFuzzerTestOneInput /tmp/test.cc:33:5
#2 0x42abbb in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) (/tmp/
...
...
0xee,0x29,0x28,
\xee)(
artifact_prefix='./'; Test unit written to ./crash-5bd58fe0ed66554106714e1679fb62a06670b0ee
Base64: 7iko
末尾にクラッシュを引き起こした入力データ\xee)(
が出力される。その上はAddress Sanitizerの出力。バックトレースから、test.ccの18行目に問題があったことがわかる:
if (parens.top() != ch) {
開き括弧にであう前に閉じ括弧にであうと、空のスタックを読もうとしてクラッシュするというバグだった。修正:
if (parens.empty() || parens.top() != ch) {
修正後にコンパイルしなおしてファジングを実行すると、クラッシュしないまま延々テストが走りつづける。ドキュメントによれば-timeout
オプションでタイムアウトが設定できるはずだけど、なんかうまくいかなかった。