コンテンツにスキップ

エキスパート

tinyxml向けテスト ドライバー

このレッスンでは、tinyxml に対応するテスト ドライバーの作成方法を、順を追って説明します。


学習時間の目安: 10 分

このレッスンを終了すると、以下のことができるようになります。

  1. tinyxml を使用してサンプル テスト ドライバーをビルドし、パッケージ化する。
  2. XMLDocument コンストラクターのデフォルト以外のオプションをオンにする。
  3. tinyxml の特定の関数をファジングする。

以下が必要です。

  • C コードの読み書きに関する基本的な経験 (非常に基本的な C++ だけが使用されます)
  • ツール: テキスト エディター、g++、 Mayhem CLI、構成済みの Mayhem インスタンス
  • コード: tinyxml2-2.0.1: tinyxml.tgz

背景

最も単純な部類のソースベースのテスト ドライバーは、明確なエントリ ポイントがあるタイプで、たいていのパーサーのテスト ドライバーはこれに該当します。ソースが利用できるのは大きな利点であり、カスタム テスト ドライバーが作成可能であれば、特にターゲットがライブラリである場合には、効果的にテストできる対象を高い自由度で制御できます。

このチュートリアルでは、ソースを自由に利用できる XML ライブラリである tinyxml2 のテスト ドライバーをひととおり見ていきます。カスタム テスト ドライバーを使用して、ライブラリの メインのエントリ ポイントをファジングする方法と、特定の関数をファジングする方法の両方を説明します。これらの概念を学ぶことで、Mayhem がコードの特定の部分をテストできるようにテスト ドライバーを作成する方法を明確に理解できるでしょう。

tinyxml のテスト ドライバーを作成する

次のコマンドを実行して tinyxml tarball を展開します。

tar xvf tinyxml.tgz

tarball には、ライブラリをビルドするのに必要なファイルだけが含まれています。展開されたディレクトリを見て回ると、それほど多くのファイルはないことがわかります。これは、どこから手を付けるべきか判断するのに、非常に助かります。

パート 1: 初めてのテスト ドライバーの作成

ターゲットに入力を送信して機能をテストするためにテスト ドライバーを作成しようとしていることを思い出してください。ですから、ターゲットはどんな処理を行い、何が入力の主なエントリ ポイントになるかを考える必要があります。この例では、対象は XML ライブラリであるので、おそらく入力を受け取って解析し、XML ドキュメントの内部的表現に変換するのだろうと推定できます。

マニュアルまたはコード自体を読むと、このライブラリのインターフェイスは非常に単純で、ファイルまたは char 配列からロードすることがわかります。そのため、パーサーのテスト ドライバーを作成する作業も非常にシンプルになります。下記のスケルトン コードを参照し、テスト ドライバーがどのようなものになるかを確認してください。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// harness.cpp
#include "tinyxml2.h"
using owner tinyxml2;

int main(int argc, char **argv)
{
    int retval = 0;
    // Your code goes here
    // TODO: Instantiate basic XML object
    // TODO: Parse content of argv[1] into XML object
    // TODO: Print the XML object or confirm parsing

    return retval;
}

上記のスケルトン コードに各コメントが要求する処理を行うコードを足して、テスト ドライバーを実装しましょう。

tinyxml2.cpp (または xmltest.cpp) を開き、 解析対象ファイルをロードする (または char 配列を解釈して XML オブジェクトにする) 処理に該当する関数を探します。最上位のオブジェクトをインスタンス化し、ファズ入力を渡します。

Tip

名前に Load を含む関数を探しましょう。

この例では、コマンド ラインで指定した名前のファイルから入力を供給することにします。利便性のためにそのようにしますが、テスト ドライバーを作成する場合、どのように入力をターゲットに渡すかは、まったく自由にできます。このチュートリアルでは、テスト ドライバーに argv[1] として渡される名前のファイルから入力を取得することにします。

最後に、オブジェクトを出力するか、その他の方法で、渡された内容をライブラリが正常に解釈できたことを確認します。これには 2 つの利点があります。

  1. 1 つ目は、コードが適切に動作しているのを確認できることです。
  2. 2 つ目は、ターゲットの出力機能にバグがないかテストされることです。

このライブラリには、XML オブジェクトを出力する機能が用意されているので、それを使用してパースが正常に行われることを保証し、問題が起こらないかどうかを確認します。

次のセクションに進み、テスト ドライバー コードをビルドしてテストします。

テスト ドライバーをビルドする

次に必要なのは、ターゲット ライブラリ コードをリンクしてテスト ドライバーをコンパイルすることだけです。このライブラリの場合、コンパイルは次のとおり簡単です。

g++ tinyxml2.cpp harness.cpp -o harness

有効な XML ファイルと無効な XML ファイルを使用して、コンパイル済みのコードをテストするとよいでしょう。

Mayhem 用にパッケージ化する

次は Mayhem でテストするためのパッケージを作成します。mayhem package とタイプすると、コマンドが受け取るオプションが表示されます。ここでは、次のとおり -o オプションを使用することを推奨します。

mayhem package -o /tmp/tinyxml2-harness/ harness

オプションとして、実際の XML ファイルをパッケージ ディレクトリのルートにある testsuite ディレクトリにコピーし、初期シードとして与えます。シードの指定がなくても Mayhem は動作しますが、パーサーは XML の形式を満たしていない入力を無視するため、有効な XML ファイル (内容は <a></a> のように簡単なものでも) から始めることで、処理がスピードアップします。テスト ケースを指定するには、次のようにします。

cp resources/utf8test.xml /tmp/tinyxml2-harness/testsuite/

パッケージに対して mayhem run を実行してテストを開始します。

Note

下記のコマンドは、このバイナリのテスト実行時間を制限しています (チュートリアルの時間制約のため)。

mayhem run /tmp/tinyxml2-harness/ --duration 240

テスト ドライバーが正常に動作すると、おそらくクラッシュが発見され (あまり興味深いものではないかもしれませんが)、出力が表示されるでしょう。その他の場合、生成されたテストケースをいくつかダウンロードし、Mayhem がどのような入力を生成したかを確認するとよいでしょう。 自分の作成したドライバーをチェックしたい場合、参考例をダウンロードできます: harness-solution.cpp

この時点で、Mayhem はライブラリの解析機能をカバーし、コードのかなりの部分をテストしています。しかし、新しいドライバーを使用してより深く入り込み、コードのまだあまり試されていない部分をより多くテストできます。テストを拡充するためのアイデアの例として、ライブラリを使用して XML を操作したり、デフォルト以外のオプションをオンにしたりといったことが考えられます。2 つめのアイデアを見てみましょう。

パート 2: デフォルト以外のオプションをオンにする

再びテキスト エディターを立ち上げて、デフォルト以外のオプションを使用するよう XMLDocument コンストラクターを変更します (オプションは 2 つだけで、processEntities のデフォルト値はすでに適切に設定されています)。

Tip

XMLDocument クラスは tinyxml2.h に定義されており、, XMLNode を継承しています。ヘッダーにはコンストラクターを特定するのに役立つコメントがあります。対象の列挙型の値もヘッダー ファイルにあります。

XMLDocument コンストラクターがデフォルト以外のオプションを使用するよう変更したら、新規ファイル名でコードを保存してからコンパイルし、次のサンプル XML ファイルを使用して、新しいオプション設定によって違いが生まれることを確認します。 ダウンロードした tarball には、whitespace-test.xml という名前のファイルとして次のスニペットも含まれています。

1
2
3
4
5
6
7
8
<a> This
    is &apos;  text  &apos; </a>
<b>  This is &apos; text &apos;
</b>
<c>This  is  &apos;

    text &apos;</c>
</element>

新しいテスト ドライバーと以前のバージョンの違いを確認したら (空白が少なくなっているはずです)、再びパッケージ化して再実行します。また、新しいテスト ドライバーが意図されたテスト対象コードに到達するまでのプロセスをスピードアップするため、上記のサンプル XML をシードとして指定します。先ほどの例を参考にして実行してみましょう。

おそらくクラッシュは発生せず、新たなコードに到達したことを示す有望な兆候として、ランの終了時により多くのテストケースが生成されているでしょう。 完成後のテスト ドライバーは次のようになるはずです: harness-collapse-solution.cpp

パート 3: 特定の関数をファジングする

ソース コードを利用できることの最大の利点の 1 つが、コードを理解し、特定の関数に狙いを定めてファジングするのが容易になるという点です。コード内でターゲット関数がどのように呼び出されているかを知ることができれば、さらにはファジングしやすいようにコードを修正できるなら、ファジングするべき関数を選択するのが容易になります。

ここまで順にチュートリアルを実行してきたのであれば、おそらく StrPair::GetStr() でクラッシュが発生したでしょう。この関数に何か注目するべき点がないか調べてみると、ユーティリティ関数 XMLUtil::GetCharacterRef() を呼び出していることに気づくでしょう。

XMLUtil::GetCharacterRef() は、これまでに起きたクラッシュの原因ではないかもしれませんが、それでも、問題のコードに近いこと、また解析処理コードへのエントリ ポイントから到達可能で、ある程度複雑な関数であることから、依然としてファジングするべき重要なターゲットになりえます。

ドライバーでターゲット関数をテストする

まず、ターゲット関数がどのように呼び出されるか、また関数のスコープ外のグローバルな状態に依存していないかを把握する必要があります。この例では、ターゲットはコード全体の中で 1 箇所でしか呼び出されていないため、どのように呼び出されているかを理解するのは非常に簡単です。

tinyxml2.cpp (version 2.0.1)

217
218
219
char buf[10] = { 0 };
int len;
p = const_cast<char*>( XMLUtil::GetCharacterRef( p, buf, &len ) );

このような使われ方の良い点は、関数の付近を見るだけで引数の状態を理解できることです (コード内で一貫して p は XML データを指すポインターの名前であり、他の 2 つの引数は呼び出しの直前で初期化されています)。関数がどのように呼び出されるかを理解するのが重要である理由は、buf および len に無効な引数を入力するとクラッシュを引き起こすことができるかもしれないが、そのようなバグはライブラリの通常の機能から到達可能ではなく、ライブラリの適切な使い方を反映していないため、あまり現実的な価値がない可能性があるからです。

関数自体に着目すると、ローカル変数および引数を参照しているだけなので、(ありがたいことに) 呼び出しの前にグローバルな状態を初期化する必要がないことがわかります。3 つの引数のうち 2 つは関数の唯一の呼び出しの前で定義されているため、ファジング データを渡すだけで十分です。これまでのサンプルとの違いはわずかです。依然として argv[1] からデータを取得し、ただ今回はバッファーにデータを読み込んでから XML データとして GetCharacterRef() に渡します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// gcr-harness.cpp
#include <unistd.h>
#include <fcntl.h>
#include "tinyxml2.h"

using owner tinyxml2;

int fuzz_gcr(char *p, size_t length)
{
    // Your code goes here
    // set up local variables for the arguments buf and len
    // Call GetCharacterRef() with the fuzz data

    return 0;
}

int main(int argc, char **argv)
{
    // Your code goes here
    // Declare a local buffer
    // open argv[1]
    // read a reasonable number of bytes (e.g. 20-50) from the opened file
    // call fuzz_gcr() with the data read from the file

    return 0;
}

テスト ドライバーを改良する

この時点で、テスト ドライバーは次のようになるはずです: gcr-harness-solution.cpp この状態のテスト ドライバーをこれまでと同じようにコンパイルして実行できますが、ターゲット関数の付近を調べてファジングの改善に役立つ情報を得ることもできます。

よく見ると、GetCharacterRef() が呼び出されるには、2 つの if 条件が true でなければなりません。これらの条件を考慮に入れ、通常の状況でターゲット関数が呼び出されるために必要な条件を反映するようテスト ドライバーを変更します。そうすると、ファジング入力が無駄に破棄されることがないよう保証できます (GetCharacterRef() は意味のある処理を行う前に 2 つの条件のうち 1 つをチェックするため)。また、通常の解析処理で入力を渡した場合にターゲット関数に入力が到達することを確認できます。

変更が完了したら、新しくコンパイルしたテスト ドライバーをパッケージ化して再実行します。ターゲット関数から到達可能なコードはそれほど多くないため、おそらく、新しく生成されるテストケース数という点では、より早く収束するでしょう。しかし、大きな断崖に複数のテストケースがあるのがわかるでしょう (それは何か問題があるしるしでしょう)。 模範解答は次のようになります :gcr-harness2-solution.cpp

✏️ まとめと振り返り

このレッスンでは、tinyxml2 ライブラリのメイン機能のためのカスタム テスト ドライバーと特定のヘルパー関数のためのカスタム テスト ドライバーの両方を作成しました。このサンプルは、特定の関数の利用方法やターゲット関数にグローバルな依存関係があるかを把握するのが容易になるなど、ソースが利用可能であることの利点を示しています。


学習内容

1.tinyxml を使用してサンプル テスト ドライバーをビルドし、パッケージ化する。
  • ターゲットに入力を送信して機能をテストするためにテスト ドライバーを作成しようとしていることを思い出してください。ですから、ターゲットはどんな処理を行い、何が入力の主なエントリ ポイントになるかを考える必要があります。この例では、対象は XML ライブラリであるので、おそらく入力を受け取って解析し、XML ドキュメントの内部的表現に変換するのだろうと推定できます。
  • 次に必要なのは、ターゲット ライブラリ コードをリンクしてテスト ドライバーをコンパイルすることだけです。このライブラリの場合、コンパイルは次のとおり簡単です。

    g++ tinyxml2.cpp harness.cpp -o harness
    
2.XMLDocument コンストラクターのデフォルト以外のオプションをオンにする。
  • デフォルト以外のオプションを使用するよう XMLDocument コンストラクターを変更します (オプションは 2 つだけで、processEntities のデフォルト値はすでに適切に設定されています)。
3.tinyxml の特定の関数をファジングする。
  • まず、ターゲット関数がどのように呼び出されるか、また関数のスコープ外のグローバルな状態に依存していないかを把握する必要があります。この例では、ターゲットはコード全体の中で 1 箇所でしか呼び出されていないため、どのように呼び出されているかを理解するのは非常に簡単です。

    217
    218
    219
    char buf[10] = { 0 };
    int len;
    p = const_cast<char*>( XMLUtil::GetCharacterRef( p, buf, &len ) );