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



2021/03/03


おなじみC/C++から使えるJSONライブラリを紹介するコーナー。まずは過去のまとめ。

これまで C++ から扱える JSON パーサを数多く紹介してきましたが、いずれもコメントを扱えるものはありませんでした。今回紹介する jsoncpp はコメントも扱えます。

GitHub - open-source-parsers/jsoncpp: A C++ library for interacting with JSON.

JsonCpp JSON is a lightweight data-interchange format. It can represent numbers, strings, ordered se...

https://github.com/open-source-parsers/jsoncpp

特徴は以下の通り。

  • コメントが扱える
  • Amalgamated code を生成できる
  • 古いコンパイラをサポートしている
  • Public Domain

触ってみた感じは json.hpp に近いです。

#include "json/json.h"
#include 

int
main(int argc, char* argv[]) {
  const std::string rawJson = R"(
  {
    "Age": 20,
    "Name": "mattn" // your name
  }
  )";
  Json::Value root;
  Json::CharReaderBuilder builder;
  builder["collectComments"] = true;
  JSONCPP_STRING errs;
  auto reader = builder.newCharReader();
  if (!reader->parse(rawJson.c_str(), rawJson.cend().base(), &root, &errs)) {
    std::cout << errs << std::endl;
    return EXIT_FAILURE;
  }
  std::cout << root["Age"].asInt64() << std::endl;
  std::cout << root["Name"].asString() << std::endl;
  std::cout << root["Name"].getComment(Json::CommentPlacement::commentAfterOnSameLine) << std::endl;
  return EXIT_SUCCESS;
}

collectComments を true にする事でパース時にコメントを収集してくれます。getComment に指定する commentBefore, commentAfterOnSameLine, commentAfter によりノードの前方、同一行または後方、後方、のいずれかのコメントを得られます。

jsoncpp にはこれだけでなく、とても便利な色々なオプションが用意されています。

collectComments: false or true

true でコメントを収集し、シリアライズ時にコメントを戻す

allowComments: false or true

コメントを許可

allowTrailingCommas: false or true

ケツカンマを許可

strictRoot: false or true

ルートノートに配列またはオブジェクトを必須とする

allowDroppedNullPlaceholders: false or true

"null" という文字列をシリアライズ時にドロップする。

allowNumericKeys: false or true

オブジェクトに数値のキーを許可

allowSingleQuotes: false or true

シングルクオートを許可

stackLimit: 数値

階層の限界を指定。オーバーすると例外が発生。

failIfExtra: false or true

余計な空白をエラーとして扱う。

rejectDupKeys: false or true

オブジェクトのキーに重複を許可。

allowSpecialFloats: false or true

NaN や inf の様な特殊な浮動小数点を許可。


これだけあれば多少ユルユル仕様な設定ファイル向けの JSON ファイルも読み込めると思います。

また jsoncpp は通常、ライブラリ形式(.a)が提供されますが、付属の amalgamate.py を使う事で、ヘッダファイル1つとソースファイル1つに纏められた形式にする事もできる為、以下の様に簡単にソースコードをビルドする事ができます。

$ ls -R
.:
json  jsoncpp.cpp  main.cpp

./json:
json.h  json-forwards.h

$ gcc -I. -o app main.cpp jsoncpp.cpp

jsoncpp は基本、パブリックドメインのライセンスで提供されています。しかし地域によってはパブリックドメインは実在しない扱いとなります。jsoncpp ではその様な地域の場合は MIT ライセンスのもと配布されるという事になっています。

ここまで融通の効く C++ 向け JSON パーサは僕が知る限り他には無いと思います。

Posted at by