[Rust] cargo-fuzzでファジングテストをする

2023.09.07

Introduction

近年RustはLinuxカーネルからWebアプリなど幅広く利用されていますが、
耐障害性・セキュリティの確保は重要です。
本稿ではそれらの向上のため、fuzzy testについての概要と
Rustでfuzzy testを行う手法について紹介します。 

Fuzzy Test?

Fuzzyテストは一般的なテスト手法の一つで、
ランダム(fuzzy)なデータを入力としてプログラムを実行し、
予測不能な結果が発生するかどうかを検証するテスト方法です。
このテストによりプログラムの耐障害性やセキュリティを向上させるために役立ちます。

fuzzyテストの特徴は下記です。

  • ランダムなデータ生成
    Fuzzテストはランダム/半自動で生成されたデータでテストします。
    このデータは通常のテストケースよりも幅広い入力データを網羅します。

  • Error Fuzzing
    Fuzzyテストでランダムな入力データを使用してプログラムがクラッシュしたり、
    予期せぬ結果を返すかを検証します。 これにより、プログラムのバグや脆弱性が特定されます。

  • 持続的なテスト
    Fuzzyテストは通常、プログラムが実行される間ずっと持続的にテストを実行します。
    これにより、長時間プログラムをテストできます。

  • 自動化
    Fuzzyテストは自動化され、テストケースの生成と実行が(ほぼ)自動で行われます。
    手動で特定のテストケースを記述する必要はありません。

Fuzzyテストは通常、通常のテスト手法と組み合わせて使用され、
プログラムの信頼性・耐障害性・セキュリティを向上させるための手法です。

cargo-fuzz?

cargo-fuzzは、Rustのfuzzy testに使用するツールです。
※cargo-fuzz自体はfuzzingフレームワーク(現在はlibFuzzerのみ)を呼び出す仕組み

cargo-fuzzは、Rustプログラムに対してfuzzy testを実行します。
Cargoのサブコマンドとして使用可能で、Cargoプロジェクトで簡単にテストできます。
自動でfuzzy testを生成可能で、他にもカバレッジ情報を取得したり
失敗したテストを再現したりすることが可能です。

Environment

  • Rust : 1.74.0-nightly

Try cargo-fuzz

cargo-fuzzの基本的なコマンドは下記。

  • cargo fuzz init fuzzy testを初期化します。

  • cargo fuzz add <target> 新しいfuzzy testのtargetを作成します。

  • cargo fuzz run <target> fuzzy testを実行します。

  • cargo fuzz tmin <target> <input> 失敗する入力データを見つけた場合、
    デバッグ用に失敗したテストケースを最小化します。
    これにより、バグの再現が容易になります。

  • cargo fuzz cmin <target> テストで発見された最小化できないバグを最小化するためのコマンド。
    tminでは最小化できない一部のバグに対処するために使用されるらしい。

  • cargo fuzz coverage <target> fuzzy testのカバレッジ情報を生成します。

Fuzzyテストしてみる

ではcargo-fuzzをインストールし、
プロジェクト作成してfuzzy testをつくってみましょう。  

# cargo-fuzzのインストール  
% cargo install cargo-fuzz

# プロジェクト作成
% cargo new fuzz && cd fuzz
% cargo fuzz init

fuzzy testを初期化し、fuzzディレクトリが作成されます。
listコマンドでfuzz testのリスト表示。

% cargo fuzz list
fuzz_target_1

生成されたfuzzy testは↓です。
ここで対象の関数をよびだして検証します。

#![no_main]

use libfuzzer_sys::fuzz_target;

fuzz_target!(|data: &[u8]| {
    // fuzzed code goes here
});

テスト対象関数を実装

foo/src/lib.rsを作成し、
割り算をする関数(バグあり)を定義します。

pub fn devide(x:u8,y:u8) -> u8{
    x / y
}

次に、arbitraryをfoo/fuzz/Cargo.tomlに追加します。
Arbitraryは、任意の非構造化入力から構造化データを生成するcrateです。
libfuzzerが生成するバイトバッファを、構造化された値(structやenum)に変換します。

[dependencies]
arbitrary = { version = "1.3.0", features = ["derive"] }

自動生成されたfoo/fuzz/fuzz_targets/fuzz_target_1.rsに
さきほどの関数を呼び出すfuzzy testを記述します。

#![no_main]

use libfuzzer_sys::fuzz_target;
use arbitrary::Arbitrary;

extern crate foo;

#[derive(Arbitrary, Debug)]
struct Input {
    x: u8,
    y: u8,
}

fuzz_target!(|input: Input| {
    use foo::devide;
    let _ = devide(input.x,input.y);
});

そしてfuzzy testを実行。
。。。のはずですが動きません。

% cargo fuzz run fuzz_target_1
error: failed to run `rustc` to learn about target-specific information

Caused by:
  process didn't exit successfully: 
  --- stderr
  error: the option `Z` is only accepted on the nightly compiler

現状、cargo-fuzzはnightlyじゃないとダメです。

% rustup install nightly
% rustup override set nightly

とするか、下記のように直接nightlyを指定してfuzz runを実行しましょう。

% cargo +nightly fuzz run fuzz_target_1

fuzzy testが実行されますが、何度目かのテストでクラッシュします。

% cargo +nightly fuzz run fuzz_target_1
INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 2623483526
INFO: Loaded 1 modules   (6699 inline 8-bit counters): 6699 [0x102fcc110, 0x102fcdb3b),
INFO: Loaded 1 PC tables (6699 PCs): 6699 [0x102fcdb40,0x102fe7df0),
・・・
────────────────────────────────────────────────────────────────────────────────

Failing input:

    fuzz/artifacts/fuzz_target_1/crash-0e356ba505631fbf715758bed27d503f8b260e3a

Output of `std::fmt::Debug`:

    Input {
        x: 1,
        y: 0,
    }

Reproduce with:

    cargo fuzz run fuzz_target_1 fuzz/artifacts/fuzz_target_1/crash-0e356ba505631fbf715758bed27d503f8b260e3a

Minimize test case with:

    cargo fuzz tmin fuzz_target_1 fuzz/artifacts/fuzz_target_1/crash-0e356ba505631fbf715758bed27d503f8b260e3a

────────────────────────────────────────────────────────────────────────────────

Error: Fuzz target exited with exit status: 77

0除算でプログラムが失敗してます。
では関数を修正して再度実行してみましょう。
その前に[Minimize test case with:]で書いてあるtminコマンドを実行することで
失敗したテストケースをminifyして保存しておくことができます。

% cargo +nightly fuzz tmin ・・・

devide関数を修正。

pub fn devide(x:u8,y:u8) -> u8{

    if y != 0 {
        x / y
    } else {
        x / 1
    }
}

今度は成功。

% cargo fuzz run fuzz_target_1 fuzz/artifacts/fuzz_target_1/crash-0e356ba505631fbf715758bed27d503f8b260e3a

 なお、普通に制限なしで実行して特にエラーもない場合、
延々とランダムに生成した値でテストを続けます。

% cargo +nightly fuzz run fuzz_target_1

ある程度のところでfuzzy testを止めたい場合、
-max_total_timeオプションなどを指定して適当なところで止めましょう。

Summary

今回はcargo-fuzzを使ってRustでfuzzy testを試してみました。
ちゃんとfuzzyテストを実装し、CIに組み込んだりすると
プログラムの品質が上がること請け合いです。

References