チュートリアル: ファジング入門

このチュートリアルでは、Goにおけるファジングの基本について説明します。ファジングでは、ランダムなデータをテストに対して実行し、脆弱性やクラッシュを引き起こす入力を特定しようとします。ファジングによって発見できる脆弱性の例としては、SQLインジェクション、バッファオーバーフロー、サービス拒否、クロスサイトスクリプティング攻撃などがあります。

このチュートリアルでは、シンプルな関数のファズテストを作成し、goコマンドを実行し、コード内の問題をデバッグして修正します。

このチュートリアル全体で使われる用語については、Goファジング用語集を参照してください。

以下のセクションを進めていきます。

  1. コード用のフォルダーを作成します。
  2. テストするコードを追加します。
  3. ユニットテストを追加します。
  4. ファズテストを追加します。
  5. 2つのバグを修正します。
  6. 追加のリソースを探ります。

注:他のチュートリアルについては、チュートリアルをご覧ください。

注: Goファジングは現在、Goファジングのドキュメントに記載されている組み込み型の一部のみをサポートしており、将来的にさらに多くの組み込み型がサポートされる予定です。

前提条件

  • Go 1.18以降のインストールが必要です。インストール手順については、Goのインストールを参照してください。
  • コードを編集するツール。 任意のテキストエディターで問題ありません。
  • コマンドターミナル. Go は Linux および Mac の任意のターミナル、および Windows の PowerShell または cmd でうまく動作します。
  • ファジングをサポートする環境が必要です。Goのファジングとカバレッジ計測は、現在AMD64およびARM64アーキテクチャでのみ利用可能です。

コード用のフォルダーを作成する

まず、作成するコード用のフォルダーを作成します。

  1. コマンドプロンプトを開き、ホームディレクトリに移動します。

    LinuxまたはMacの場合

    $ cd
    

    Windowsの場合

    C:\> cd %HOMEPATH%
    

    このチュートリアルの残りの部分では、$がプロンプトとして表示されます。使用するコマンドはWindowsでも機能します。

  2. コマンドプロンプトから、fuzzという名前のコード用のディレクトリを作成します。

    $ mkdir fuzz
    $ cd fuzz
    
  3. コードを保持するためのモジュールを作成します。

    新しいコードのモジュールパスを指定して、go mod initコマンドを実行します。

    $ go mod init example/fuzz
    go: creating new go.mod: module example/fuzz
    

    注: プロダクションコードの場合は、より具体的なモジュールパスをニーズに合わせて指定します。詳細については、依存関係の管理を参照してください。

次に、文字列を反転させるシンプルなコードを追加します。これは後でファジングします。

テストするコードを追加する

このステップでは、文字列を反転させる関数を追加します。

コードを書く

  1. テキストエディタを使用して、fuzzディレクトリにmain.goというファイルを作成します。

  2. main.go のファイルの先頭に、以下のパッケージ宣言を貼り付けます。

    package main
    

    スタンドアロンプログラム (ライブラリとは対照的に) は常にパッケージ main にあります。

  3. パッケージ宣言の下に、以下の関数宣言を貼り付けます。

    func Reverse(s string) string {
        b := []byte(s)
        for i, j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1 {
            b[i], b[j] = b[j], b[i]
        }
        return string(b)
    }
    

    この関数はstringを受け取り、byte単位でループし、最後に反転された文字列を返します。

    注: このコードはgolang.org/x/example内のstringutil.Reverse関数に基づいています。

  4. main.goの先頭、パッケージ宣言の下に、以下のmain関数を貼り付けて、文字列を初期化し、反転させ、出力を表示し、繰り返します。

    func main() {
        input := "The quick brown fox jumped over the lazy dog"
        rev := Reverse(input)
        doubleRev := Reverse(rev)
        fmt.Printf("original: %q\n", input)
        fmt.Printf("reversed: %q\n", rev)
        fmt.Printf("reversed again: %q\n", doubleRev)
    }
    

    この関数はいくつかのReverse操作を実行し、その結果をコマンドラインに出力します。これは、コードが動作している様子を確認したり、デバッグしたりするのに役立ちます。

  5. main関数はfmtパッケージを使用するため、インポートする必要があります。

    コードの最初の行は次のようになります。

    package main
    
    import "fmt"
    

コードを実行する

main.goを含むディレクトリのコマンドラインから、コードを実行します。

$ go run .
original: "The quick brown fox jumped over the lazy dog"
reversed: "god yzal eht revo depmuj xof nworb kciuq ehT"
reversed again: "The quick brown fox jumped over the lazy dog"

元の文字列、それを反転させた結果、そして再び反転させた結果(これは元の文字列と等しい)を見ることができます。

コードが実行できるようになったので、テストする時が来ました。

ユニットテストを追加する

このステップでは、Reverse関数の基本的なユニットテストを作成します。

コードを書く

  1. テキストエディタを使用して、fuzzディレクトリにreverse_test.goというファイルを作成します。

  2. 以下のコードをreverse_test.goに貼り付けます。

    package main
    
    import (
        "testing"
    )
    
    func TestReverse(t *testing.T) {
        testcases := []struct {
            in, want string
        }{
            {"Hello, world", "dlrow ,olleH"},
            {" ", " "},
            {"!12345", "54321!"},
        }
        for _, tc := range testcases {
            rev := Reverse(tc.in)
            if rev != tc.want {
                    t.Errorf("Reverse: %q, want %q", rev, tc.want)
            }
        }
    }
    

    この簡単なテストは、リストされた入力文字列が正しく反転されることを検証します。

コードを実行する

go testを使ってユニットテストを実行します。

$ go test
PASS
ok      example/fuzz  0.013s

次に、ユニットテストをファズテストに変更します。

ファズテストを追加する

ユニットテストには限界があります。つまり、各入力は開発者によってテストに追加される必要があります。ファジングの利点の1つは、コードの入力を生成し、開発者が考えつかなかったエッジケースを特定できる可能性があることです。

このセクションでは、ユニットテストをファズテストに変換し、少ない労力でより多くの入力を生成できるようにします!

ユニットテスト、ベンチマーク、ファズテストを同じ*_test.goファイルに保持できますが、この例ではユニットテストをファズテストに変換します。

コードを書く

テキストエディタで、reverse_test.go内のユニットテストを以下のファズテストに置き換えます。

func FuzzReverse(f *testing.F) {
    testcases := []string{"Hello, world", " ", "!12345"}
    for _, tc := range testcases {
        f.Add(tc)  // Use f.Add to provide a seed corpus
    }
    f.Fuzz(func(t *testing.T, orig string) {
        rev := Reverse(orig)
        doubleRev := Reverse(rev)
        if orig != doubleRev {
            t.Errorf("Before: %q, after: %q", orig, doubleRev)
        }
        if utf8.ValidString(orig) && !utf8.ValidString(rev) {
            t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
        }
    })
}

ファジングにもいくつかの制限があります。ユニットテストでは、`Reverse`関数の期待される出力を予測し、実際の出力がそれらの期待を満たしていることを検証できました。

例えば、テストケース`Reverse("Hello, world")`では、ユニットテストは戻り値を`"dlrow ,olleH"`と指定します。

ファジングでは、入力に対する制御がないため、期待される出力を予測することはできません。

ただし、ファズテストで検証できるReverse関数のプロパティがいくつかあります。このファズテストでチェックされる2つのプロパティは、以下の通りです。

  1. 文字列を2回反転すると元の値が維持されること
  2. 反転された文字列が有効なUTF-8としての状態を維持すること。

ユニットテストとファズテストの構文の違いに注目してください。

  • 関数は`TestXxx`の代わりに`FuzzXxx`で始まり、`*testing.T`の代わりに`*testing.F`を受け取ります。
  • `t.Run`の実行を期待する箇所に、代わりに`f.Fuzz`があります。これは`*testing.T`とファズする型をパラメータとするファズターゲット関数を受け取ります。ユニットテストからの入力は、`f.Add`を使用してシードコーパス入力として提供されます。

新しいパッケージ`unicode/utf8`がインポートされていることを確認してください。

package main

import (
    "testing"
    "unicode/utf8"
)

ユニットテストがファズテストに変換されたので、もう一度テストを実行する時が来ました。

コードを実行する

  1. ファジングせずにファズテストを実行し、シード入力が通過することを確認します。

    $ go test
    PASS
    ok      example/fuzz  0.013s
    

    そのファイルに他のテストがあり、ファズテストだけを実行したい場合は、`go test -run=FuzzReverse`を実行することもできます。

  2. ファジング付きで`FuzzReverse`を実行し、ランダムに生成された文字列入力が失敗を引き起こすかどうかを確認します。これは、新しいフラグ`-fuzz`をパラメータ`Fuzz`に設定して`go test`を使用して実行されます。以下のコマンドをコピーしてください。

    $ go test -fuzz=Fuzz
    

    もう一つの便利なフラグは`-fuzztime`で、ファジングにかかる時間を制限します。例えば、以下のテストで`-fuzztime 10s`を指定すると、それ以前に失敗が発生しない限り、テストはデフォルトで10秒経過後に終了します。他のテストフラグについては、cmd/goドキュメントのこのセクションを参照してください。

    では、今コピーしたコマンドを実行してください。

    $ go test -fuzz=Fuzz
    fuzz: elapsed: 0s, gathering baseline coverage: 0/3 completed
    fuzz: elapsed: 0s, gathering baseline coverage: 3/3 completed, now fuzzing with 8 workers
    fuzz: minimizing 38-byte failing input file...
    --- FAIL: FuzzReverse (0.01s)
        --- FAIL: FuzzReverse (0.00s)
            reverse_test.go:20: Reverse produced invalid UTF-8 string "\x9c\xdd"
    
        Failing input written to testdata/fuzz/FuzzReverse/af69258a12129d6cbba438df5d5f25ba0ec050461c116f777e77ea7c9a0d217a
        To re-run:
        go test -run=FuzzReverse/af69258a12129d6cbba438df5d5f25ba0ec050461c116f777e77ea7c9a0d217a
    FAIL
    exit status 1
    FAIL    example/fuzz  0.030s
    

    ファジング中にエラーが発生し、問題の原因となった入力はシードコーパスファイルに書き込まれます。このファイルは、次回`go test`が呼び出されたときに、`-fuzz`フラグなしでも実行されます。エラーの原因となった入力を表示するには、testdata/fuzz/FuzzReverseディレクトリに書き込まれたコーパスファイルをテキストエディタで開いてください。シードコーパスファイルには異なる文字列が含まれているかもしれませんが、形式は同じです。

    go test fuzz v1
    string("泃")
    

    コーパスファイルの最初の行はエンコーディングバージョンを示します。それに続く各行は、コーパスエントリを構成する各型の値を表します。ファズターゲットは1つの入力しか取らないため、バージョン以降の値は1つだけです。

  3. `-fuzz`フラグなしで`go test`を再度実行すると、新しい失敗するシードコーパスエントリが使用されます。

    $ go test
    --- FAIL: FuzzReverse (0.00s)
        --- FAIL: FuzzReverse/af69258a12129d6cbba438df5d5f25ba0ec050461c116f777e77ea7c9a0d217a (0.00s)
            reverse_test.go:20: Reverse produced invalid string
    FAIL
    exit status 1
    FAIL    example/fuzz  0.016s
    

    テストが失敗したので、デバッグする時が来ました。

無効な文字列エラーを修正する

このセクションでは、失敗をデバッグし、バグを修正します。

先に進む前に、時間をかけてこの問題について考え、自分で修正を試してみてください。

エラーを診断する

このエラーをデバッグする方法はいくつかあります。VS Codeをテキストエディタとして使用している場合は、デバッガを設定して調査できます。

このチュートリアルでは、役立つデバッグ情報をターミナルにログ出力します。

まず、utf8.ValidStringのドキュメントを検討してください。

ValidString reports whether s consists entirely of valid UTF-8-encoded runes.

現在の`Reverse`関数は文字列をバイト単位で反転させており、そこに問題があります。元の文字列のUTF-8エンコードされたルーンを保持するためには、代わりに文字列をルーン単位で反転させる必要があります。

入力(この場合は中国語の文字)が`Reverse`関数によって反転されたときに無効な文字列を生成する理由を調べるには、反転された文字列内のルーンの数を検査することができます。

コードを書く

テキストエディタで、`FuzzReverse`内のファズターゲットを次のように置き換えます。

f.Fuzz(func(t *testing.T, orig string) {
    rev := Reverse(orig)
    doubleRev := Reverse(rev)
    t.Logf("Number of runes: orig=%d, rev=%d, doubleRev=%d", utf8.RuneCountInString(orig), utf8.RuneCountInString(rev), utf8.RuneCountInString(doubleRev))
    if orig != doubleRev {
        t.Errorf("Before: %q, after: %q", orig, doubleRev)
    }
    if utf8.ValidString(orig) && !utf8.ValidString(rev) {
        t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
    }
})

この`t.Logf`行は、エラーが発生した場合、または`-v`でテストを実行した場合にコマンドラインに出力され、この特定の問題のデバッグに役立ちます。

コードを実行する

go testを使ってテストを実行します

$ go test
--- FAIL: FuzzReverse (0.00s)
    --- FAIL: FuzzReverse/28f36ef487f23e6c7a81ebdaa9feffe2f2b02b4cddaa6252e87f69863046a5e0 (0.00s)
        reverse_test.go:16: Number of runes: orig=1, rev=3, doubleRev=1
        reverse_test.go:21: Reverse produced invalid UTF-8 string "\x83\xb3\xe6"
FAIL
exit status 1
FAIL    example/fuzz    0.598s

シードコーパス全体は、すべての文字が単一バイトである文字列を使用していました。しかし、泃のような文字は複数バイトを必要とする場合があります。したがって、文字列をバイト単位で反転させると、マルチバイト文字が無効になります。

注: Goが文字列をどのように扱うかについて興味がある場合は、より深く理解するためにブログ記事Goにおける文字列、バイト、ルーン、文字を読んでください。

バグをよりよく理解した上で、`Reverse`関数のエラーを修正します。

エラーを修正する

`Reverse`関数を修正するために、バイトではなくルーンで文字列をたどってみましょう。

コードを書く

テキストエディタで、既存の`Reverse()`関数を以下に置き換えてください。

func Reverse(s string) string {
    r := []rune(s)
    for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
        r[i], r[j] = r[j], r[i]
    }
    return string(r)
}

主な違いは、`Reverse`が各`byte`ではなく、文字列内の各`rune`を反復処理するようになったことです。これは単なる例であり、結合文字を正しく処理しないことに注意してください。

コードを実行する

  1. go testを使ってテストを実行します。

    $ go test
    PASS
    ok      example/fuzz  0.016s
    

    テストは合格しました!

  2. `go test -fuzz`で再度ファジングし、新しいバグがないか確認します。

    $ go test -fuzz=Fuzz
    fuzz: elapsed: 0s, gathering baseline coverage: 0/37 completed
    fuzz: minimizing 506-byte failing input file...
    fuzz: elapsed: 0s, gathering baseline coverage: 5/37 completed
    --- FAIL: FuzzReverse (0.02s)
        --- FAIL: FuzzReverse (0.00s)
            reverse_test.go:33: Before: "\x91", after: "�"
    
        Failing input written to testdata/fuzz/FuzzReverse/1ffc28f7538e29d79fce69fef20ce5ea72648529a9ca10bea392bcff28cd015c
        To re-run:
        go test -run=FuzzReverse/1ffc28f7538e29d79fce69fef20ce5ea72648529a9ca10bea392bcff28cd015c
    FAIL
    exit status 1
    FAIL    example/fuzz  0.032s
    

    文字列が2回反転された後、元の文字列と異なることがわかります。今回は入力自体が無効なUnicodeです。文字列でファジングしているのに、なぜこのようなことが起こるのでしょうか?

    もう一度デバッグしてみましょう。

二重反転エラーを修正する

このセクションでは、二重反転の失敗をデバッグし、バグを修正します。

先に進む前に、時間をかけてこの問題について考え、自分で修正を試してみてください。

エラーを診断する

以前と同様に、この失敗をデバッグする方法はいくつかあります。この場合、デバッガを使用するのが良いアプローチでしょう。

このチュートリアルでは、Reverse関数に役立つデバッグ情報をログ出力します。

反転された文字列をよく見て、エラーを見つけてください。Goでは、文字列は読み取り専用のバイトスライスであり、有効なUTF-8ではないバイトを含むことができます。元の文字列は1バイト、`\x91`を含むバイトスライスです。入力文字列が`[]rune`に設定されると、GoはバイトスライスをUTF-8にエンコードし、そのバイトをUTF-8文字の�に置き換えます。置き換えられたUTF-8文字と入力バイトスライスを比較すると、明らかに等しくありません。

コードを書く

  1. テキストエディタで、`Reverse`関数を以下に置き換えます。

    func Reverse(s string) string {
        fmt.Printf("input: %q\n", s)
        r := []rune(s)
        fmt.Printf("runes: %q\n", r)
        for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
            r[i], r[j] = r[j], r[i]
        }
        return string(r)
    }
    

    これは、文字列をルーンのスライスに変換するときに何が問題になっているかを理解するのに役立ちます。

コードを実行する

今回は、ログを検査するために、失敗するテストのみを実行したいと考えています。これを行うために、`go test -run`を使用します。

FuzzXxx/testdata内の特定のコーパスエントリを実行するには、`-run`に`{FuzzTestName}/{filename}`を指定できます。これはデバッグに役立ちます。この場合、`-run`フラグを失敗したテストの正確なハッシュに設定します。ターミナルから一意のハッシュをコピーアンドペーストしてください。以下のものとは異なります。

$ go test -run=FuzzReverse/28f36ef487f23e6c7a81ebdaa9feffe2f2b02b4cddaa6252e87f69863046a5e0
input: "\x91"
runes: ['�']
input: "�"
runes: ['�']
--- FAIL: FuzzReverse (0.00s)
    --- FAIL: FuzzReverse/28f36ef487f23e6c7a81ebdaa9feffe2f2b02b4cddaa6252e87f69863046a5e0 (0.00s)
        reverse_test.go:16: Number of runes: orig=1, rev=1, doubleRev=1
        reverse_test.go:18: Before: "\x91", after: "�"
FAIL
exit status 1
FAIL    example/fuzz    0.145s

入力が無効なUnicodeであることを知ったので、`Reverse`関数のエラーを修正しましょう。

エラーを修正する

この問題を修正するには、`Reverse`への入力が有効なUTF-8でない場合にエラーを返すようにしましょう。

コードを書く

  1. テキストエディタで、既存の`Reverse`関数を以下に置き換えます。

    func Reverse(s string) (string, error) {
        if !utf8.ValidString(s) {
            return s, errors.New("input is not valid UTF-8")
        }
        r := []rune(s)
        for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
            r[i], r[j] = r[j], r[i]
        }
        return string(r), nil
    }
    

    この変更により、入力文字列に有効なUTF-8ではない文字が含まれている場合にエラーが返されます。

  2. `Reverse`関数がエラーを返すようになったので、`main`関数を変更して余分なエラー値を破棄するようにします。既存の`main`関数を以下に置き換えます。

    func main() {
        input := "The quick brown fox jumped over the lazy dog"
        rev, revErr := Reverse(input)
        doubleRev, doubleRevErr := Reverse(rev)
        fmt.Printf("original: %q\n", input)
        fmt.Printf("reversed: %q, err: %v\n", rev, revErr)
        fmt.Printf("reversed again: %q, err: %v\n", doubleRev, doubleRevErr)
    }
    

    これらの`Reverse`への呼び出しは、入力文字列が有効なUTF-8であるため、nilエラーを返すはずです。

  3. errorsとunicode/utf8パッケージをインポートする必要があります。main.goのインポートステートメントは次のようになるはずです。

    import (
        "errors"
        "fmt"
        "unicode/utf8"
    )
    
  4. reverse_test.goファイルを変更し、エラーをチェックし、エラーが生成された場合はreturnしてテストをスキップするようにします。

    func FuzzReverse(f *testing.F) {
        testcases := []string {"Hello, world", " ", "!12345"}
        for _, tc := range testcases {
            f.Add(tc)  // Use f.Add to provide a seed corpus
        }
        f.Fuzz(func(t *testing.T, orig string) {
            rev, err1 := Reverse(orig)
            if err1 != nil {
                return
            }
            doubleRev, err2 := Reverse(rev)
            if err2 != nil {
                 return
            }
            if orig != doubleRev {
                t.Errorf("Before: %q, after: %q", orig, doubleRev)
            }
            if utf8.ValidString(orig) && !utf8.ValidString(rev) {
                t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
            }
        })
    }
    

    returnする代わりに、`t.Skip()`を呼び出してそのファズ入力の実行を停止することもできます。

コードを実行する

  1. go testを使ってテストを実行します

    $ go test
    PASS
    ok      example/fuzz  0.019s
    
  2. `go test -fuzz=Fuzz`でファジングし、数秒経過したら`ctrl-C`でファジングを停止します。`-fuzztime`フラグを渡さない限り、ファズテストは失敗する入力に遭遇するまで実行されます。デフォルトでは、失敗がない場合は永久に実行され、プロセスは`ctrl-C`で中断できます。

$ go test -fuzz=Fuzz
fuzz: elapsed: 0s, gathering baseline coverage: 0/38 completed
fuzz: elapsed: 0s, gathering baseline coverage: 38/38 completed, now fuzzing with 4 workers
fuzz: elapsed: 3s, execs: 86342 (28778/sec), new interesting: 2 (total: 35)
fuzz: elapsed: 6s, execs: 193490 (35714/sec), new interesting: 4 (total: 37)
fuzz: elapsed: 9s, execs: 304390 (36961/sec), new interesting: 4 (total: 37)
...
fuzz: elapsed: 3m45s, execs: 7246222 (32357/sec), new interesting: 8 (total: 41)
^Cfuzz: elapsed: 3m48s, execs: 7335316 (31648/sec), new interesting: 8 (total: 41)
PASS
ok      example/fuzz  228.000s
  1. `go test -fuzz=Fuzz -fuzztime 30s`でファジングを実行すると、失敗が見つからない場合は30秒間ファジングして終了します。

    $ go test -fuzz=Fuzz -fuzztime 30s
    fuzz: elapsed: 0s, gathering baseline coverage: 0/5 completed
    fuzz: elapsed: 0s, gathering baseline coverage: 5/5 completed, now fuzzing with 4 workers
    fuzz: elapsed: 3s, execs: 80290 (26763/sec), new interesting: 12 (total: 12)
    fuzz: elapsed: 6s, execs: 210803 (43501/sec), new interesting: 14 (total: 14)
    fuzz: elapsed: 9s, execs: 292882 (27360/sec), new interesting: 14 (total: 14)
    fuzz: elapsed: 12s, execs: 371872 (26329/sec), new interesting: 14 (total: 14)
    fuzz: elapsed: 15s, execs: 517169 (48433/sec), new interesting: 15 (total: 15)
    fuzz: elapsed: 18s, execs: 663276 (48699/sec), new interesting: 15 (total: 15)
    fuzz: elapsed: 21s, execs: 771698 (36143/sec), new interesting: 15 (total: 15)
    fuzz: elapsed: 24s, execs: 924768 (50990/sec), new interesting: 16 (total: 16)
    fuzz: elapsed: 27s, execs: 1082025 (52427/sec), new interesting: 17 (total: 17)
    fuzz: elapsed: 30s, execs: 1172817 (30281/sec), new interesting: 17 (total: 17)
    fuzz: elapsed: 31s, execs: 1172817 (0/sec), new interesting: 17 (total: 17)
    PASS
    ok      example/fuzz  31.025s
    

    ファジングが成功しました!

    `-fuzz`フラグに加えて、いくつかの新しいフラグが`go test`に追加されており、ドキュメントで確認できます。

    ファジング出力で使用される用語の詳細については、Goファジングを参照してください。たとえば、「new interesting」とは、既存のファズテストコーパスのコードカバレッジを拡大する入力を指します。「new interesting」入力の数は、ファジング開始時に急増し、新しいコードパスが発見されるたびに数回ピークに達し、その後時間とともに減少すると予想されます。

まとめ

よくできました!Goでのファジング入門を終えましたね。

次のステップは、コード内でファジングしたい関数を選択し、試してみることです!ファジングでコードにバグが見つかった場合は、トロフィーケースに追加することを検討してください。

問題が発生した場合、または機能に関するアイデアがある場合は、問題を提出してください

この機能に関する議論や一般的なフィードバックについては、Gophers Slackの#fuzzingチャンネルに参加することもできます。

さらに学習するには、go.dev/security/fuzzのドキュメントを参照してください。

完成したコード

— main.go —

package main

import (
    "errors"
    "fmt"
    "unicode/utf8"
)

func main() {
    input := "The quick brown fox jumped over the lazy dog"
    rev, revErr := Reverse(input)
    doubleRev, doubleRevErr := Reverse(rev)
    fmt.Printf("original: %q\n", input)
    fmt.Printf("reversed: %q, err: %v\n", rev, revErr)
    fmt.Printf("reversed again: %q, err: %v\n", doubleRev, doubleRevErr)
}

func Reverse(s string) (string, error) {
    if !utf8.ValidString(s) {
        return s, errors.New("input is not valid UTF-8")
    }
    r := []rune(s)
    for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
        r[i], r[j] = r[j], r[i]
    }
    return string(r), nil
}

— reverse_test.go —

package main

import (
    "testing"
    "unicode/utf8"
)

func FuzzReverse(f *testing.F) {
    testcases := []string{"Hello, world", " ", "!12345"}
    for _, tc := range testcases {
        f.Add(tc) // Use f.Add to provide a seed corpus
    }
    f.Fuzz(func(t *testing.T, orig string) {
        rev, err1 := Reverse(orig)
        if err1 != nil {
            return
        }
        doubleRev, err2 := Reverse(rev)
        if err2 != nil {
            return
        }
        if orig != doubleRev {
            t.Errorf("Before: %q, after: %q", orig, doubleRev)
        }
        if utf8.ValidString(orig) && !utf8.ValidString(rev) {
            t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
        }
    })
}

トップへ戻る