2021/09/21


みなさん Fuzz testing ってご存じでしょうか。

人間が作る物は必ずといっていいほどバグが存在します。そしてそのコードをテストする人間も必ずバグを見逃します。

想定していなかった境界値テスト等、人間には先入観という物があり、それが邪魔をして簡単にバグを見逃します。昨今、この様な誰も気付かなかったバグの隙間を突く様な脆弱性が沢山見つかっています。

物によっては重大インシデントに発展する物まであります。

こういった人間では想定できない様なバグを見付けてくれるのが Fuzz testing です。Fuzz testing を実施する事で、ソフトウェアは頑丈になり安全にもなりえます。

本日、Go の master ブランチに Fuzz testing の機能が入りました。

[dev.fuzz] Merge remote-tracking branch 'origin/dev.fuzz' into merge-… · golang/go@6e81f78 · GitHub

+1 −0 api/except.txt +36 −0 api/next.txt +63 −15 src/cmd/go/alldocs.go +10 −0 src/cmd/go/internal/ca...

https://github.com/golang/go/commit/6e81f78c0f1653ea140e6c8d008700ddad1fa0a5
これまで Go でテストを書いた事がある皆さんであれば、Go のテストは xxx_test.go というファイルに、 Test から始まり引数に *testing.T を持つ関数を定義される事はご存じだと思います。今回入る Fuzz はこれに似ており先頭が Fuzz で引数に *testing.F を持つ関数が実行対象となります。 package foo_test

import "testing"

func FuzzFoo(f *testing.F) {
    f.Log("all good here")
    f.Fuzz(func(*testing.T, []byte) {})
}

例えば以下の関数があったとしましょう。b のゼロ長さチェックはしていますが、2バイト必要かどうかのテストが抜けています。

func doSomething(s stringbool {
    if len(s) > 0 && s[0] == 0xff && s[1] == 0xff {
        return true
    }
    return false
}

以下の様に、f.Fuzz の第二引数に受け取りたい fuzz 値を得られる様にして実装します。

package foo_test

import "testing"

func doSomething(s stringbool {
    if len(s) > 0 && s[0] == 0xff && s[1] == 0xff {
        return true
    }
    return false
}

func FuzzFoo(f *testing.F) {
    f.Log("all good here")
    f.Fuzz(func(f *testing.T, b []byte) {
        doSomething(string(b))
    })
}

コマンドラインから以下の様に実行します。

$ go test -fuzz=.

すると以下の様に落ちる箇所を見付けてくれます。

gathering baseline coverage, elapsed: 0s, workers: 4, left: 2
fuzz: found a 31-byte crash input; minimizing...
FAIL
fuzz: elapsed: 1s, execs: 9807 (14334/sec), workers: 4, interesting: 0
--- FAIL: FuzzFoo (0.69s)
    foo_test.go:13: all good here
        foo_test.go:13: all good here
        --- FAIL: FuzzFoo (0.00s)
            testing.go:1244: panic: runtime error: index out of range [1] with length 1
                goroutine 3187 [running]:
                runtime/debug.Stack()
                    C:/go/src/runtime/debug/stack.go:24 +0x90
                testing.tRunner.func1()
                    C:/go/src/testing/testing.go:1244 +0x545
                panic({0x65c2e0, 0xc000014138})
                    C:/go/src/runtime/panic.go:814 +0x207
                github.com/mattn/go-fuzz-example_test.doSomething(...)
                    C:/Users/mattn/go/src/github.com/mattn/go-fuzz-example/foo_test.go:6
                github.com/mattn/go-fuzz-example_test.FuzzFoo.func1(0x0, {0xc000180000, 0x0, 0x4f37d9})
                    C:/Users/mattn/go/src/github.com/mattn/go-fuzz-example/foo_test.go:15 +0xfb
                reflect.Value.call({0x63c8e0, 0x67a178, 0x30}, {0x66a8b0, 0x4}, {0xc008d80090, 0x2, 0x18})
                    C:/go/src/reflect/value.go:542 +0x814
                reflect.Value.Call({0x63c8e0, 0x67a178, 0x5a33c0}, {0xc008d80090, 0x2, 0x2})
                    C:/go/src/reflect/value.go:338 +0xc5
                testing.(*F).Fuzz.func1.1(0x0)
                    C:/go/src/testing/fuzz.go:412 +0x20b
                testing.tRunner(0xc008d961a0, 0xc008d98090)
                    C:/go/src/testing/testing.go:1352 +0x102
                created by testing.(*F).Fuzz.func1
                    C:/go/src/testing/fuzz.go:401 +0x4df
                
        --- FAIL: FuzzFoo (0.00s)
            testing.go:1244: panic: runtime error: index out of range [1] with length 1
                goroutine 3189 [running]:
                runtime/debug.Stack()
                    C:/go/src/runtime/debug/stack.go:24 +0x90
                testing.tRunner.func1()
                    C:/go/src/testing/testing.go:1244 +0x545
                panic({0x65c2e0, 0xc000014180})
                    C:/go/src/runtime/panic.go:814 +0x207
                github.com/mattn/go-fuzz-example_test.doSomething(...)
                    C:/Users/mattn/go/src/github.com/mattn/go-fuzz-example/foo_test.go:6
                github.com/mattn/go-fuzz-example_test.FuzzFoo.func1(0x0, {0xc008d820b0, 0x0, 0x4f37d9})
                    C:/Users/mattn/go/src/github.com/mattn/go-fuzz-example/foo_test.go:15 +0xfb
                reflect.Value.call({0x63c8e0, 0x67a178, 0x30}, {0x66a8b0, 0x4}, {0xc008d80180, 0x2, 0x18})
                    C:/go/src/reflect/value.go:542 +0x814
                reflect.Value.Call({0x63c8e0, 0x67a178, 0x5a33c0}, {0xc008d80180, 0x2, 0x2})
                    C:/go/src/reflect/value.go:338 +0xc5
                testing.(*F).Fuzz.func1.1(0x0)
                    C:/go/src/testing/fuzz.go:412 +0x20b
                testing.tRunner(0xc008d96340, 0xc008d98120)
                    C:/go/src/testing/testing.go:1352 +0x102
                created by testing.(*F).Fuzz.func1
                    C:/go/src/testing/fuzz.go:401 +0x4df
                
    
    Crash written to testdata\fuzz\FuzzFoo\e3443030b91d09bbdd7fbe67dbbbc3093401b277aa711c82dd97ee80606f56ab
    To re-run:
    go test github.com/mattn/go-fuzz-example -run=FuzzFoo/e3443030b91d09bbdd7fbe67dbbbc3093401b277aa711c82dd97ee80606f56ab
FAIL
exit status 1
FAIL    github.com/mattn/go-fuzz-example    1.012s

バイト列以外の mutate に関しては現在は TODO の様です。

実装しておくだけで勝手にバグを見付けてくれるのですから、とても便利ですね。ぜひ活用していきましょう。

Posted at by



2021/05/04


Go ではポインタは扱えるけれど、ポインタ演算は扱えないというのが共通認識でした。もちろん unsafe.Pointeruintptr を使う事で、出来なくはなかったのですが簡単ではありませんでした。

package main

import (
    "unsafe"
)

type foo struct {
    k int64
    v int64
}

func main() {
    f := &foo{3,4}

    // unsafe.Pointer() で匿名ポインタにして
    // uintptr() で演算可能にして
    // +8 バイト(64bit)足して
    // unsafe.Pointer で匿名ポインタに戻して
    // そこにはフィールド v があるはずなので *int64 にキャストして
    // デリファレンスすれば出来上がり
    *(*int64)(unsafe.Pointer((uintptr(unsafe.Pointer(f))+8))) = 5 // グヒヒ

    println(f.v) // 5
}

Go 1.17 からは unsafe.Addunsafe.Slice が導入される事で、少しだけ簡単にポインタ演算そしてポインタからスライスを復元する事ができる様になります。(参照)

package main

import (
    "fmt"
    "unsafe"
)

func main() {
    b := []byte{12345}
    pb := &b[3]
    fmt.Println(unsafe.Slice(pb, 2)) // 4, 5

    // sizeof(byte) 分ポインタをずらす
    pb = (*byte)(unsafe.Add(unsafe.Pointer(pb), 1))
    fmt.Println(*pb) // 5

    v := []int{23456}
    pi := &v[3]
    // オフセット3からのスライスを得る
    fmt.Println(unsafe.Slice(pi, 2)) // 5, 6

    // sizeof(int) 分ポインタをずらす
    pi = (*int)(unsafe.Add(unsafe.Pointer(pi), 8))
    fmt.Println(*pi) // 6
}

もちろん不正なオフセットを指定して unsafe.Add を呼び出してデリファレンスしたり、不正なサイズを指定してスライスを復元したりすると panic する事になります。

Posted at by



2020/10/30


Go 言語はシングルバイナリをウリにしたプログラミング言語です。バイナリファイルを1つポンと scp で転送すれば動くのでとても便利です。シングルバイナリとなると当然、画像や HTML といったアセットをバイナリに埋め込みたくなります。

Go 言語ではこれまで go-assetsgo-bindatastatik というツールを使う事でファイルのコンテンツをバイナリ化し、変数からアクセスする様にしてきました。

しかしそれらには色々な流儀や OS 間でのまばらな動作など、ユーザにとって納得のいかない物がありました。昨日、Go 言語ではオフィシャルとしてこのファイル埋め込みをサポートする様になりました。Go 1.16 から使える様になります。

cmd/go: add //go:embed support · golang/go@25d28ec · GitHub

+3 −3 src/cmd/go/internal/fsys/fsys.go +1 −1 src/cmd/go/internal/fsys/fsys_test.go +2 −0 src/cmd/go/...

https://github.com/golang/go/commit/25d28ec55aded46e0be9c2298f24287d296a9e47
package main

import (
    _ "embed"
    "net/http"

    "github.com/labstack/echo"
)

//go:embed static/logo.png
var contents []byte

func main() {
    e := echo.New()
    e.GET("/"func(c echo.Context) error {
        return c.Blob(http.StatusOK, "image/png", contents)
    })
    e.Logger.Fatal(e.Start(":8989"))
}

変数に //go:embed [ファイル名] というコメントを付ける事でそのコンテンツを変数に埋め込む事ができます。開発者がやるのは embed をブランクインポートする事と go build だけです。ファイルが読み込めなかったり、記法が不正な場合はコンパイルエラーとなります。

pattern static/logo1.png: no matching files found

またバイト列ではなく文字列でも使えます。

package main

import (
    _ "embed"
    "fmt"
)

//go:embed message.txt
var message string

func main() {
    fmt.Println(message)
}

これまで go generate を使ってバイナリファイルを変数に埋め込んでいたので、それから比べるとずいぶん便利になりました。そしてファイルだけでなく、ファイルシステムとして埋め込む事もできる様になりました。

package main

import (
    "embed"
    "io"
    "log"
    "os"
    "path"
)

//go:embed static
var local embed.FS

func main() {
    fis, err := local.ReadDir("static")
    if err != nil {
        log.Fatal(err)
    }
    for _, fi := range fis {
        in, err := local.Open(path.Join("static", fi.Name()))
        if err != nil {
            log.Fatal(err)
        }
        out, err := os.Create("embed-" + path.Base(fi.Name()))
        if err != nil {
            log.Fatal(err)
        }
        io.Copy(out, in)
        out.Close()
        in.Close()
        log.Println("exported""embed-"+path.Base(fi.Name()))
    }
}

embed.FS は実際にファイルが開ける Open メソッドをサポートしている為、 http.FileSystem が返す様なファイルシステムとは互換性がなく、以下の様にウェブサーバに使う事はできないですが、いずれサードパーティからラップするライブラリが登場すると思います。

package main

import (
    "embed"
    "net/http"

    "github.com/labstack/echo"
)

//go:embed static
var local embed.FS

func main() {
    e := echo.New()
    e.GET("/", echo.WrapHandler(http.FileServer(local)))
    e.Logger.Fatal(e.Start(":8989"))
}

追記 http.FS という関数が既に用意されていました。以下のリポジトリの example4 にサンプルを足しています。

Go 1.16 が待ち遠しいですね。

Go 1.16 が待ち遠しいですね。

※大事な事なので2回言いました。

これらのサンプルは以下のリポジトリに置いてあります。

GitHub - mattn/go-embed-example

We use optional third-party analytics cookies to understand how you use GitHub.com so we can build b...

https://github.com/mattn/go-embed-example
改訂2版 みんなのGo言語 改訂2版 みんなのGo言語
松木 雅幸, mattn, 藤原 俊一郎, 中島 大一, 上田 拓也, 牧 大輔, 鈴木 健太
技術評論社 Kindle版 / ¥2,350 (2019年08月01日)
 
発送可能時間:

Posted at by