Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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

参考

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away