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


Clang開発版に[libFuzzer][libfuzzer]が新しいサニタイザとして[取り込まれた][commit]。`clang -fsanitize=fuzzer`で使える。

[libfuzzer]: http://llvm.org/docs/LibFuzzer.html
[commit]: https://github.com/llvm-mirror/clang/commit/3efd04e48004628cfaffead00ecb1c206b0b6cb2

## ファジングとは

ファジング (fuzzing) はテスト手法のひとつ。おかしなデータを自動的に大量生成してプログラムへ入力し、クラッシュを誘発することでバグをあぶりだす。
[libFuzzer][libfuzzer]はファジングをおこなうためのライブラリ。

## trunk版のclangをインストール

Clangのtrunk版をインストール:

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

```cpp: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行目に問題があったことがわかる:

```cpp
            if (parens.top() != ch) {
```

開き括弧にであう前に閉じ括弧にであうと、空のスタックを読もうとしてクラッシュするというバグだった。修正:

```cpp
            if (parens.empty() || parens.top() != ch) {
```

修正後にコンパイルしなおしてファジングを実行すると、クラッシュしないまま延々テストが走りつづける。[ドキュメント][libfuzzer]によれば`-timeout`オプションでタイムアウトが設定できるはずだけど、なんかうまくいかなかった。

## 参考

- http://llvm.org/docs/LibFuzzer.html
- https://github.com/llvm-mirror/llvm/tree/master/lib/Fuzzer
- http://lists.llvm.org/pipermail/llvm-dev/2017-May/112609.html - compiler-rtに入れないの? という議論