【Go】Go1.18で学ぶFuzzing入門
Go1.18ではGenericsが最も注目されていますが、同じくfuzzing(testing.F)が採用される事になりました。(正確にはGo 1.18Beta1から)
個人的にはfuzzingという手法自体を初めて知り、 手軽で面白いなと感じたのでfuzzingとはどの様なものなのかを含め纏めておきます。
Fuzzingとは
ファジング(英語:fuzzing)とは、コンピュータプログラムヘの入力として、無効なデータ、予期しないデータ、ランダムなデータを用いた自動化ないし半自動化されたソフトウェアテストの手法である。コンピュータプログラムにファズ(「予測不可能な入力データ」の意、英語: fuzz))を与えることで、意図的に例外的な状況を発生させ、その例外的な状況での挙動を確認するという方法を用いる。
出典: フリー百科事典『ウィキペディア(Wikipedia)』
fuzzingはブラックボックス(あるいはグレーボックス)テストの一種で、プログラムに様々な入力を行うことで予期せぬ挙動が起こらないかを調べる手法である。
fuzzingを行うツールの事をファザー(fuzzer)と呼ぶ。
主にセキュリティやロバストネスチェックに使われ、プログラムの脆弱性を明らかにするために活用されることが多い。 テストの特性上、バグが存在しないことではなく、バグが存在することを示すために使用される点に注意が必要です。(テストでエラーが検出されなくても、問題が発生する可能性はある)
現在あるサービスでも多く活用され実績があり、 Google ChromeやMicrosoft Edge、Internet Explorerなどでも大規模なfuzzingテストが実施され、かなりの数のバグが発見されている。
用語
- fuzz 入力されるデータ、様々なfuzzによってテストを行うことでエラーを探す
- fuzzer fuzzingを行うツール、商用やOSSなど様々なfuzzerが存在する
- corpus fuzzのデータセット群 複数のfuzzを入力するが、多くのfuzzerは対象関数のテストのカバレッジ情報を取得しており、カバレッジを上げられる入力が得られるとcorpusに登録し、テストを網羅的にしていく。
活用目的
バグの発見、静的解析、テストケースの削減、メモリリークの検出、Webブラウザのセキュリティ診断 など
代表的なテスト対象
コマンドライン、環境変数、ファイル形式、ネットワーク・プロトコル、Webアプリケーション など
有名なFuzzer
商用やOSSのFuzzerが存在する。
など
Goでのfuzzing
Go1.18より前に存在しているgo-fuzzはAFLをベースに開発され、標準パッケージで100以上、golang.org/x/パッケージで40以上、
その他を含めると300以上のバグを発見するという実績を残している。
参照:https://github.com/dvyukov/go-fuzz#trophies
これらの実績があり、標準ライブラリにfuzzingを採用する流れのようです。
fuzzとcorpus
testing.Fにおいて、テストする関数の引数(fuzz)は現在、以下のプリミティブ型のものに絞られます。
- string, []byte
- int, int8, int16, int32/rune, int64
- uint, uint8/byte, uint16, uint32, uint64
- float32, float64
- bool
fuzzingには様々なアルゴリズムがあるようだが、
go-fuzzでは、ランダムに生成するfuzzにはランダムな値を生成するのではなく、正常な引数の一部を変更し、ランダムな値を生成して利用する。
AST(抽象構文木)を使い、対象関数のテストのカバレッジ情報を取得し、カバレッジを上げる様な入力があればcopusに登録することで網羅的なテストを行っている。
標準パッケージのtesting.Fでも、Design Draft: First Class Fuzzing によると、
FuzzingEngineがcopusから新たなcopusを生成し、成長させながらテストを行う仕組みとなっている。
一度利用したfuzzをキャッシュとしてtestdata/corpus/FuzzTarget 配下および $GOCACHE/fuzz 配下に保存し、再利用され、
go-fuzzと同様、カバレッジ情報を考慮しているので優れたseed copusを保持していると効果的なテストができます。
詳細な内容については言及されていないようでした。
fuzzingの実行
主にfuzzerでは事前に用意したfuzzを実行し終わるまでテストを回したり、
テスト時間を指定してfuzzを生成し続け、エラーが発生しないことを確認することが一般的です。
処理性能やcopusのキャッシュ、エラーログのためのデータ容量を圧迫するので注意が必要です。
参照
公式ドキュメント
実際の使い方については公式から丁寧なtutorialが公開されているので、こちらを参照してください。
- Tutorial: Getting started with fuzzing - The Go Programming Language
【書籍】エンジニアリング組織論への招待
本書は以前読んだものだったが、本書を用いて社内での研修用の輪読会を開催してくれないかと依頼があり、 改めて読み直してみたので、改めてまとめや所感などを述べようと思う。
どのような書籍か
全体を俯瞰して感想をまとめるのであれば、筋道を立てて輪読や精読するのに向いている本だと感じた。
その根拠は以下のようなものである。
・目次がしっかりしていて、目次から凡その話の流れが汲み取れる
・Chapter1で全体感を伝え、その後の章でも順序立てて話が広がっていくので分かりやすい
・重要な単語についての説明が明確になされており、最後まで表記揺れなく読める
・章を追うごとに論点が個人から組織へと拡大していき、理解しやすい
・大事な単語や文言がきちんとまとまっており、筆者の癖が読み取りやすい
・参考文献がしばしば登場するので論理的で知識を広げるきっかけがある
きちんと筆者の言葉の意図を紐解きながら忠実に精読していけば、しっかり読み込める本となっているように感じた。
エンジニアリング組織論への招待 ~不確実性に向き合う思考と組織のリファクタリング | 広木 大地 |本 | 通販 | Amazon
概要
内容について個人的に一番注目したいのは「Chapter1 思考のリファクタリング」である。
まず本書が扱うのは個人または組織で「エンジニアリング」を行うための思考法である。
本書では、エンジニアリングとは、不確実性を下げ、情報を生み出す過程であり、問題解決や物事の実現を行う科学分野であると述べられている。
Chapter1ではまず全体感について、重要な単語や根底となる思想について触れられている。
分からないものや問題が発生する場合、それは不確実性がある事がその大きな要因である。
具体的に納期や手順、個人のタスクなどが明確な場合、不明瞭なものはないので不安や問題の可能性は低くなる。
本書でもっともよく使われる単語である「不確実性」とは、
・未来は分からないという環境不確実性
・他人への伝達や行動が全てを掌握出来ないというコミュニケーション不確実性
の2つがあり、
これを下げること、つまり情報を生み出すことが不確実性を下げる手段であるとしている。
本書は一貫してこの不確実性へのアプローチを主として説明してくれている。
組織の中で各々が見えている問題だけへの局所的合理性が、組織全体の合理性とかけ離れてしまうことで各々が衝突してしまったり、問題が解決できなかったりする。
自分自身が何に囚われ、合理的な判断ができなくなってしまうかについて考えるのと共に、どの様にこの不確実性を下げ、組織全体が自律した組織として機能するようにすればよいか論じてくれている本となっている。
まとめ
まずChapter1だけでも精読してみる価値があると感じた。
分かりやすい例えや、他の書籍などへの紹介も含め、自分自身が普段身の回りで起こっている問題に対しての置き換えやアプローチが出来る実践的な書籍のように思える。
組織に所属したばかりの新卒が読むのには少々実感が湧かないかもしれないが、2〜3年目以降の社会人は業種問わず読む価値のある本だと思う。
最後に一つだけ欠点を挙げるとすれば、多くの参考文献を文中で紹介してくれているので、奥付前に参考文献一覧を載せてくれなかった点が惜しい。
Echoリファレンスの詳しく知りたかったところ1~Context~
先日社内勉強会でGoのWebフレームワーク「Echo」の入門をテーマに発表を行ったのでその際に改めてリファレンスを読み込んでみました。 リファレンスはとても簡潔でざっと一通り読むと使い方がイメージできるようなものです。
ですが厳密にこれってどうなってるんだ?違いは?使う場面は?と考えてしまう部分がいくつかあったので その辺りについて簡単に触れてみようと思います。 日本語での記事が少なく、調べづらかった点も含めて自分なりにゆるく調べた内容を扱います。
Context、Bind、Middleware、静的ファイルの扱い、Loggerなどについて触れる予定です。 自分の為の備忘録にしたいので多分気が乗れば続きます。
Echo について
GoのWebフレームワークで、高性能で拡張可能、最小限の構成になっていて
堅牢かつスケーラブルなRESTfulAPIを構築できることを謳っています。
Echo - High performance, minimalist Go web framework
Context
echo.Contextはリクエスト・レスポンスや、パス、パスパラメータ、データ、登録されたHandler、リクエストとレスポンスに対してのAPIなどをデフォルトで保持します。 またContext自体はInterfaceでしかないので拡張できるとあります。
ContextのInterface定義を見てみると様々なパラメータや、メソッドが定義されていることが分かります。 APIやMiddlewareで使用されるHandlerFunc定義ではecho.Contextを持ち回して処理を行うため、 リクエストで受け取った内容を引き出したり、レスポンスにデータを詰めるなどすることが想定されています。
単純に受け取ったリクエストからレスポンスを生成するような用途の場合、拡張する必要はないかもしれません。 ですが、例えばContextによってHandlerやMiddlewareで参照したい共通の値などがあった場合、Contextに詰めるのが楽です。 また、Context共通の処理としてメソッドを定義したい場合もあると思います。
そのためにはContextを拡張する必要があります。
基本的な拡張の仕方はリファレンスに記載があるので参考にしてください。
Context | Echo - High performance, minimalist Go web framework
例1. パラメータを付加する
type User struct { Name string `json:"name" form:"name" query:"name" validate:"required"` } type CustomContext struct { echo.Context UserId int } func NewCustomContext(c echo.Context) *CustomContext { return &CustomContext{ Context: c, } } func main() { e := echo.New() // Contextの設定はCustomContextを使用する他のmiddlewareよりも早く行う必要がある e.Use(func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { cc := NewCustomContext(c) return next(cc) } }) e.Use(func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { cc := c.(*CustomContext) // 何かしらの認証、UserIdを詰める cc.UserId = 12345 return next(cc) } }) e.GET("/", func(c echo.Context) error { cc := c.(*CustomContext) // UserIDの取り出し return cc.String(200, strconv.Itoa(cc.UserId)) }) // サーバ起動 e.Logger.Fatal(e.Start(":1323")) }
例2. 共通の関数を付加する
type User struct { Name string `json:"name" form:"name" query:"name" validate:"required"` } type CustomContext struct { echo.Context } func NewCustomContext(c echo.Context) *CustomContext { return &CustomContext{ Context: c, } } // 付加するメソッド よく使うBindとValidateをまとめた例 func (c *CustomContext) BindAndValidate(i interface{}) error { if err := c.Bind(i); err != nil { return c.String(http.StatusBadRequest, err.Error()) } if err := c.Validate(i); err != nil { return c.String(http.StatusBadRequest, err.Error()) } return nil } func main() { e := echo.New() // コンテキストの設定はCustomContextを使用する他のmiddlewareよりも早く行う必要がある e.Use(func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { cc := NewCustomContext(c) return next(cc) } }) e.GET("/", func(c echo.Context) error { cc := c.(*CustomContext) u := User{} // 共通の処理 if err := cc.BindAndValidate(u); err != nil { return err } return cc.String(200, u.Name) }) // サーバ起動 e.Logger.Fatal(e.Start(":1323")) }
まとめ
恐らく最も使うことが多いであろう例を2つ記載しました。
サービスを考えると、Middlewareでの認証で作ったデータなどを詰めたりすることが多いのではないでしょうか。
またメソッドを付加する例ではコードを短縮する例を取り上げてみました。
どちらも分かりやすく、もうちょっと工夫することで冗長なコードの削減なども可能だと思います。
Echoはベースがかなりシンプルなので拡張性が高く、 工夫すれば様々な書き方が出来ると思うので何がベストとは定義しづらいですが、 リファレンスだけでなく元の定義をある程度見てみた上で考えていければ模索する余地がありそうだと感じました。
今回は割と内容が簡単なものから扱いました。
次書く気力が湧くように頑張ります。
【Go】Go1.14で追加されたt.Cleanup()の基本の使い方
golangのv1.14からtestingパッケージにt.Cleanup()が追加されました。
golang.org
これはテストの初期化などの後処理をシンプルにするための関数です。
どのような場面で使うか基本的な例を実際に書いてみます。
使い方
恐らく大抵、単体テストなどではテストの依存関係を設定し、テストを実行、その結果を期待する値と比較するような順序でテストを書きます。
この依存関係についてリポジトリやDB、プロセスなどに状態を持つものの場合、
それらをテストケースごとに初期化(復元)する必要があります。
依存関係を初期化(復元)しない場合、テストやサブテスト同士の結果が影響して、テストが失敗したり意図せぬ結果になってしまいます。
DBにデータをInsertするようなリポジトリの例で考えてみます。 この場合、テストごとにInsertした内容を初期化しないとサブテストや、テスト結果をキャッシュした場合にデータが残ってしまう結果になります。
まずテストをシンプルにするために依存関係を設定する関数を書いてみます。
また、依存関係の初期化(復元)をするための関数を返却するようにします。
func NewRepository(t *testing.T) (*Repository, func()) { repository := &Repository { config : Config{ // ... }, } return repository, func () { if err := repository.Reset(); err != nil { t.Error("reset err, err:", err) } } }
テスト本体はこの様になります。
func TestRepository_Insert(t *testing.T) { repository, reset := NewRepository(t) defer reset() ctx := context.Background() err := repository.Insert(ctx, User{ UserID: int64(1), Name: "TestUser", }) if err != nil { t.Error("insert error, err:", err) } }
NewRepositoryで返ってきたreset()をdeferで呼び出すことで依存関係の復元を行っています。
この例ではdeferを必ず呼び出す必要があるので、この様な依存関係を初期化しなければいけないものが増えたり、複雑になるとテストコードを圧迫し始めます。
deferはテスト本体に書かないといけないので切り分けることも難しく、それぞれのdeferが必ず呼び出されることや、順番、どこかのdeferでパニックしないかなど考慮しなければいけません。
テストロジックに集中できなくなり可読性が下がっていきます。
これをt.Cleanup()を使って実装してみます。
func NewRepository(t *testing.T) *Repository { repository := &Repository { config : Config{ // ... }, } t.Cleanup(func() { if err := repository.Reset(); err != nil { t.Error("reset err, err:", err) } }) return repository } func TestRepository_Insert(t *testing.T) { repository := NewRepository(t) // スッキリした! ctx := context.Background() err := repository.Insert(ctx, User{ UserID: int64(1), Name: "TestUser", }) if err != nil { t.Error("insert error, err:", err) } }
戻り値としてのReset関数も消えて大分シンプルになりました!
メインのテスト本体に復元の処理を書く必要がなくなるのが一番大きな変化で、テストで考慮しなくて良いのはとても嬉しいですね。
挙動と並列処理 t.Parallel
基本的にはdeferのように動きます。
- テストの途中でpanic()したとしても通る
- 複数ある場合はスタックされているのでFILOの順で呼び出される
ただし、注意が必要なのがサブテストなどを並列で処理した場合です。
テストやサブテストはt.Parallel()を使うことで別ゴルーチンによって同時に実行することができます。
その時Cleanupをサブテストなどで使うと、トランザクションを使用していないので1つのサブテストの結果が他のテストに影響してしまいます。
別ゴルーチンの復元が終わっていなかったり、パニックすると呼び出されずに終了してしまいます。
t.Parallel()で実行する際はトランザクションを考慮した内容にする必要があるようです。
func NewRepository(t *testing.T) *Repository { repository := &Repository { config : Config{ // ... }, } t.Cleanup(func() { if err := repository.Reset(); err != nil { t.Error("reset err, err:", err) } }) return repository } func TestRepository_Insert_Parallel(t *testing.T) { ctx := context.Background() for i := 0; i < 10; i++ { t.Run("test", func(t *testing.T) { t.Parallel() repository := NewRepository(t) err := repository.Insert(ctx, User{ UserID: int64(1), Name: "TestUser", }) if err != nil { t.Error("insert error, err:", err) } }) } }
上記のコードにInsertとResetについて実行された時にログを出力するようにしました。
実行してみると以下のようなログが出力され、insertとresetがセットで実行されていないことが分かります。
(実行ログは一部です)
=== RUN TestRepository_Insert_Parallel/test#06 === PAUSE TestRepository_Insert_Parallel/test#06 === CONT TestRepository_Insert_Parallel/test#06 insert:8 --- PASS: TestRepository_Insert_Parallel/test#06 (8.00s) === RUN TestRepository_Insert_Parallel/test#07 === PAUSE TestRepository_Insert_Parallel/test#07 === CONT TestRepository_Insert_Parallel/test#07 insert:9 reset:1 reset:2 reset:3 reset:4 reset:5 reset:6 reset:7 reset:8 reset:9
まとめ
deferよりも、簡単で可読性の高いテストが書けそうです。
個人的には並列で処理するテーブルテストばかり書いているので使う場面にはやや困るのですが、
場面によって使い分けたり、トランザクションを使った上手いテストを模索していこうと思います。
【書籍】レバレッジ・リーディング
概要
この本は、速読や多読を目的とするのではなく、「1冊の本を投資として読み、何を得て成果をあげられるようにするか」に着目した本である。 速度や読む量を重視するよりも、如何に重要なポイントを抑えて生かせる様に読むかを教えてくれる。
どちらかというと物語の過程を楽しむ小説ではなく、 何か目的を持って読む傾向にあるビジネス書やリベラルアーツ本のための読書術だ。
読書の価値、読書の前の本の選び方、読む前にどの様なことを考えてどの様な部分から本の価値を推し量るか、 それに対して読書から得られるものの確度を上げていく方法、細かなTipsまで順を追って説明してくれている。
ざっくり内容の概要(ごく一部)
- 読書を投資として捉える
- 本を読む時間がないのではなく、本を読まずに1から自分だけで試行錯誤するから読書する時間がない
- 読むべき本は目的を持って探すべき
- 読む前に読む目的をはっきりさせておくことで、その内容が目に止まるし、不要な文を飛ばす判断が出来る
- 読みながら内容を自分に対して想像する
- 読書後のフォローは必要
- 読書後のフォローのため、内容を振り返る「レバレッジメモ」を作る
要点
これまで読書術について書かれた本は数多く読んできたが、 特にこの本は実践的で、かつ根拠がしっかりしていてレバレッジ・リーディングで読むのにまさに適した本になっている。
この本で最も特徴的な要素である「レバレッジメモ」は、ただの読書感想文でも要約でもなく、自分がその本から得たい内容を身につけるために必要な情報を高い精度で抜き出し、 読書後にも活用することで身につけることを目的としたものだ。
レバレッジとは「てこの作用」を意味するもので、なるべく少ない労力で書籍の価値を大きく得ることを目的としている。
本書では、本に対してどう向き合うか、本の価値から始まり、本の選び方、レバレッジメモを使った読書価値の向上について実践的に説明してくれている。
書評と感想
他の読書術を謳う本と比較しても、今日からやってみようと実践でき、成果を得られる内容の多い書籍だと素直に感じた。 レバレッジメモを活用して読む事に向いている書籍である。 読書をしても本の内容を忘れてしまう、あまり成果を得られていないと悩む方や、ただ消費するように読書をしている方にとって、今後の読書観を変える様な価値ある本だと思う。
読書術は個人の主観や好みに偏る事が多いジャンルの技術だと感じるが、それに対して本書は根拠が明確で、それに対して自分の判断がしやすい点も良書だと感じた。 それも、まさにレバレッジ・リーディングで解説してくれている「情報の取捨選択」である。
私もこの本を読む前から本の価値を得る方法に大分紆余曲折あり、現在はEvernoteなどに似たような読書メモを作る事はしていたが、より精度の高いメモの作り方と、その活用方法を得ることができた。 もっと前にこの本に出会いたかった。この記事もその改良されたレバレッジメモを元に書いている。
私個人的には全ての書籍に同じ、読書方法で臨む必要は全くないと考えている。だが、自己投資になるような書籍を読みたい時、自分の成果にとって価値ある本を選びたい時、読書による目的が明確な時などには、この書籍の内容が助けてくれるのではないかと思う。