この記事はGo1.18連載の3記事目です。
伊藤真彦です、最近CSIGのVulsチームで働くようになりました。
crypto/elliptic (CVE-2022-23806)、math/big (CVE-2022-23772)、cmd/go (CVE-2022-23773)
へのセキュリティFixを含むGo 1.17.7と1.16.14が先日リリースされました。急にセキュリティエンジニアっぽい事を言うようになるのは恐縮ですが忘れずアップデートしていきましょう。
脇道に逸れましたがこの記事ではFuzzingについて紹介します。
Go1.18から追加されたFuzzingとは
Go1.18からFuzzingという機能が追加されます、Genericsのインパクトが大きいですが、Go1.18ではこういった大きな変更も加わっています。
機能追加に伴いランディングページが作成されました、リリースノートでfuzzing landing page
と紹介されています。今後もドキュメントと呼ぶよりランディングページと呼ぶ方が伝わりやすいかもしれません。
Fuzzingとはテスト手法の1つで、プログラムの機能が想定していない入力を与える事でバグを発見するアプローチの事を指します。想定していない入力とは具体的にはランダムな値を乱数を基に生成する事で生み出した値ですが、ランダムな値であること自体はFuzzingの本質ではありません。テスト入力に人力で用意したデータを追加する事もあります。ともかくテストケースの不足や実装の考慮漏れを炙り出すための工夫ということですね。
なおFuzzingはGo独自の概念ではありません。
Fuzzingの使い方
ランディングページに説明がありますが、端的に説明すると下記の通りです。
- Fuzzing機能を利用するテストを記載する
-fuzz
オプションをつけてテストを実行する
それほど難しいものではありません。
Fuzzingの書き方
テストの書き方もランディングページにある画像を見ればすぐに理解できると思います。
func FuzzFoo(f *testing.F)
のようにFuzzから始まり、Go1.18から新しく追加された*testing.F
を引数に取るテストコードを記載します。
f.Fuzz
の中にテストの内容を記載します、テストコードのFuzzing arguments
はランダム生成された値になります。
下記の型のデータであれば任意の個数用意する事が可能です。
- string, []byte
- int, int8, int16, int32/rune, int64
- uint, uint8/byte, uint16, uint32, uint64
- float32, float64
- bool
任意の値の組み合わせを必ず実行したい、という場合はf.Add
で入力を指定できます。
Fuzzingの実行方法
テストコードが用意出来たら-fuzz
オプションをつけてテストを実行します。
go test -fuzz=FuzzTestName |
テスト名を指定しなくても実行する事は可能です。
go test -fuzz . |
-fuzztime
のデフォルト値は無期限です、設定しないとCtrl + C
で中断するまで永遠にテストが続きますのでご注意ください。
また、下記のような内容がリリースノートに記載されています。
ファジングは大量のメモリを消費する可能性があり、実行中のマシンのパフォーマンスに影響を与える可能性があることに注意してください。現在、ファズキャッシュに書き込まれる可能性のあるファイルの数または合計バイト数に制限はないため、ファズキャッシュは大量のストレージ(場合によっては数GB)を占有する可能性があります。
実際に簡単なFuzzingテストコードを実行したところ一瞬でCPU使用率が100%になりました。
メモリ使用量も2GBほど増えました、実用上差し支える事例が出た場合は今後のアップデートで調整が入る、またはオプション項目が増えるかもしれません。
Fuzzingはどこで使うと良いのか
ランダムな値でテストを実行できることはわかったけど実際どのように使うと良いのだろうか、と疑問に思う方もいるかもしれません。
困ったら標準ライブラリを見ると良い、という事で執筆時点でのGoのmasterブランチを覗いてみました。
下記のパッケージでFuzzingが採用されていました。
archive/tar
archive/zip
compress/gzip
crypto/elliptic
encoding/json
image/gif
image/jpeg
image/png
net/netip
runtime/mgcpacer_test
runtime/debug_test
testing
主にエンコード、デコード、パースといった機能に相当する部分が多い印象です。
net/netip
はそれ自体がGo1.18の新機能ですね、f.Add
を有効活用する事例やFuzzingにおける高度な検証の手法としてとても参考になりますね。
参考までにお世話になる事が多いであろうencoding/json
のテストコードを見てみましょう。
func FuzzUnmarshalJSON(f *testing.F) { |
- そもそもデコード(Unmarshal)できない入力は無視する
- デコードできた場合、それを正常にエンコード(Marshal)できる
- エンコードできた場合、それをまたデコードできる
というroudtrip
な性質をテストしているようです。このような考え方は様々な処理のテストで応用できそうですね。
runtime、testing
のように既存のテストファイルにFuzzingのコードを書いても動作しますが、標準パッケージにおいてはfuzz_test.go
というファイルを用意する形を取っているようです。
標準パッケージの流儀に必ず従う必要はありませんが是非参考にしてみてください。
まとめ
- Go1.18からFuzzingが追加された
- リリースに伴いランディングページが用意された
- テストコードを追加し
-fuzz
オプション付きでテストするだけでFuzzingを利用できる - 標準パッケージでは
fuzz_test.go
に切り分けているものが多い
どれほど真剣に取り組んでもバグは尽きぬものです、新機能を有効活用して安全に動くアプリケーションを目指していきたいですね。