LoginSignup
36
26

More than 5 years have passed since last update.

Clangでファジング (-fsanitize=fuzzer)

Posted at

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()を書いて、テストしてみる。この実装にはセグフォをおこすようなバグが入ってる。

test.cc
#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オプションでタイムアウトが設定できるはずだけど、なんかうまくいかなかった。

参考

36
26
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
36
26