形式手法を使って、 発見しにくいバグを一網打尽にしよう

形式手法を使って、 発見しにくいバグを一網打尽にしよう

builderscon tokyo 2019で話したときに使った資料です。
YouTube: https://www.youtube.com/watch?v=D7GccAn6R94

[2020/04/17追記]
この資料をさらにブラッシュアップした資料を公開しました。
この資料よりも次の資料をご覧いただくことをおすすめします。
https://www.slideshare.net/dena_tech/dena-techcon-2020-230372486

693ed679c8dde3eccbc682ff44f357e1?s=128

Hodaka Suzuki

August 30, 2019
Tweet

Transcript

  1. 2.

    自己紹介 • 鈴木 穂高(すずき ほだか) • 経歴 − 2014年DeNA新卒入社 −

    2014年8月〜 大規模アプリゲーム開発 − 2018年10月〜 SWET − Androidテストチーム − テストの普及のための活動 − Game Testing Revolution(GTR)チーム − 仕様品質を向上させるための技術的なアプローチ研究 2
  2. 3.

    自己紹介 • 鈴木 穂高(すずき ほだか) • 経歴 − 2014年DeNA新卒入社 −

    2014年8月〜 大規模アプリゲーム開発 − 2018年10月〜 SWET − Androidテストチーム − テストの普及のための活動 − Game Testing Revolution(GTR)チーム − 仕様品質を向上させるための技術的なアプローチ研究 3
  3. 4.

    お話すること • 動機 • 形式手法とは • 私が考える適用可能性 4 Main CFP:

    https://builderscon.io/builderscon/tokyo/2019/session/e14fce7f-7ccc-42e5-b240-062e8719fa83
  4. 11.

    形式手法にも種類が色々ある 11 種類 説明 代表的な記述言語 形式仕様記述 矛盾がなく論理的に正しい仕様を作 成する VDM++/Event-B/ Z/Alloy

    etc. モデル検査 プログラムの状態をモデル化すること で、プログラムが期待される性質を満 たすことを検証する Promela/TLA+ etc. 他もありますが、今回は割愛。
  5. 14.

    byte n = 0; active proctype P() { n =

    1; printf(“process P, n = %d\n”, n) } active proctype Q() { n = 2; printf(“process Q, n = %d\n”, n) } 14 「Spinモデル検査入門」より引用 ・・・① ・・・② ・・・③ ・・・④ モデル検査の例(Spin/Promela) 「異なるプロセスP, Qが、globalなnへの代入とprintを 実行する」をモデル化
  6. 15.

    byte n = 0; active proctype P() { n =

    1; assert(n == 1); printf(“process P, n = %d\n”, n) } active proctype Q() { n = 2; printf(“process Q, n = %d\n”, n) } 15 モデル検査の例 assertを入れ、Spinの状態空間で成り立つことを 確認する
  7. 16.

    モデル検査の例 システムが取りうる状態・パスを自動で網羅的に 探索する 16 1 2 3 4 5 6

    ① n = 1 ① n = 1 ① n = 1 ③ n = 2 ③ n = 2 ③ n = 2 ② printf(P) ③ n = 2 ③ n = 2 ④ printf(Q) ① n = 1 ① n = 1 ③ n = 2 ② printf(P) ④ printf(Q) ① n = 1 ④ printf(Q) ② printf(P) ④ printf(Q) ④ printf(Q) ② printf(P) ② printf(P) ② printf(P) ④ printf(Q)
  8. 17.

    $ spin -a -o2 sample.pml $ gcc -o pan pan.c

    $ ./pan -e pan:1: assertion violated (n==1) (at depth 4) pan: wrote sample.pml1.trail pan: wrote sample.pml2.trail ... 17 モデル検査の例 ツールをかませる
  9. 18.

    $ spin -a -o2 sample.pml $ gcc -o pan pan.c

    $ ./pan -e pan:1: assertion violated (n==1) (at depth 4) pan: wrote sample.pml1.trail pan: wrote sample.pml2.trail ... 18 モデル検査の例 assertionが満たされないパスが2つあることが わかる
  10. 19.

    $ spin -pglsr -t1 sample.pml using statement merging 1: proc

    0 (P:1) sample.pml:4 (state 1) [n = 1] 2: proc 1 (Q:1) sample.pml:10 (state 1) [n = 2] process Q, n = 2 3: proc 1 (Q:1) sample.pml:11 (state 2) [printf('process Q, n = %d\\n',n)] 4: proc 1 terminates spin: sample.pml:5, Error: assertion violated spin: text of failed assertion: assert((n==1)) 5: proc 0 (P:1) sample.pml:5 (state 2) [assert((n==1))] spin: trail ends after 5 steps #processes: 1 n = 2 5: proc 0 (P:1) sample.pml:6 (state 3) 2 processes created 19 モデル検査の例 反例が出たときの計算の再構成に必要なデータ (trailファイル)をベースに再度シミュレーション
  11. 20.

    $ spin -pglsr -t1 sample.pml using statement merging 1: proc

    0 (P:1) sample.pml:4 (state 1) [n = 1] 2: proc 1 (Q:1) sample.pml:10 (state 1) [n = 2] process Q, n = 2 3: proc 1 (Q:1) sample.pml:11 (state 2) [printf('process Q, n = %d\\n',n)] 4: proc 1 terminates spin: sample.pml:5, Error: assertion violated spin: text of failed assertion: assert((n==1)) 5: proc 0 (P:1) sample.pml:5 (state 2) [assert((n==1))] spin: trail ends after 5 steps #processes: 1 n = 2 5: proc 0 (P:1) sample.pml:6 (state 3) 2 processes created 20 1つ目の反例
  12. 21.

    $ spin -pglsr -t2 sample.pml using statement merging 1: proc

    0 (P:1) sample.pml:4 (state 1) [n = 1] 2: proc 1 (Q:1) sample.pml:10 (state 1) [n = 2] spin: sample.pml:5, Error: assertion violated spin: text of failed assertion: assert((n==1)) 3: proc 0 (P:1) sample.pml:5 (state 2) [assert((n==1))] spin: trail ends after 3 steps #processes: 2 n = 2 3: proc 1 (Q:1) sample.pml:11 (state 2) 3: proc 0 (P:1) sample.pml:6 (state 3) 2 processes created 21 2つ目の反例
  13. 22.

    モデル検査の例 2つの反例が見つかった 22 1 2 3 4 5 6 ①

    n = 1 ① n = 1 ① n = 1 ③ n = 2 ③ n = 2 ③ n = 2 ② printf(P) ③ n = 2 ③ n = 2 ④ printf(Q) ① n = 1 ① n = 1 ③ n = 2 ② printf(P) ④ printf(Q) ① n = 1 ④ printf(Q) ② printf(P) ④ printf(Q) ④ printf(Q) ② printf(P) ② printf(P) ② printf(P) ④ printf(Q) assert(n==1)
  14. 32.

    32 フェーズとしては、αテストの最中 SWET Oyakata dev Team • エンジニアOnlyで構成 • プロダクト要求仕様書(

    PRD)も エンジニアが記述 ゲーム Team フィードバック プロダクト提供 PRDに対して形式仕様記述 コードに対してモデル検査 Oyakata含めた開発体制
  15. 45.

    形式仕様記述 abstract sig Role {} one sig General, AD extends

    Role {} sig User {} sig Oyakata { role: User -> one Role, } 45
  16. 46.

    形式仕様記述 abstract sig Role {} one sig General, AD extends

    Role {} sig UserId {} sig Oyakata { role: User -> one Role, } 46 sig : 要素(アトム)の集合を表す
  17. 47.

    形式仕様記述 abstract sig Role {} one sig General, AD extends

    Role {} sig User {} sig Oser { role: UserId -> one Role, } 47 Role General Admin Admin要素1 General 要素1
  18. 48.

    形式仕様記述 abstract sig Role {} one sig General, AD extends

    Role {} sig User {} sig User { role: UserId -> one Role, frozen: UserId -> one FreezeStatus } 48 User User 要素1 User 要素2 User 要素N ・・・
  19. 49.

    形式仕様記述 abstract sig Role {} one sig General, AD extends

    Role {} sig UserId {} sig Oyakata { role: User -> one Role, } 49 User Role Admin General
  20. 50.

    形式仕様記述 abstract sig Role {} one sig General, AD extends

    Role {} sig UserId {} sig Oyakata { role: User -> one Role, } 50 User Role Admin Oyakata role General 関係を表す
  21. 53.

    形式仕様記述 abstract sig FreezeStatus {} one sig Frozen, NotFrozen extends

    FreezeStatus {} abstract sig Role {} one sig General, AD extends Role {} sig User {} sig Oyakata { role: User -> one Role, frozen: User -> one FreezeStatus } 53
  22. 54.

    形式仕様記述 abstract sig FreezeStatus {} one sig Frozen, NotFrozen extends

    FreezeStatus {} abstract sig Role {} one sig General, AD extends Role {} sig UserId {} sig User { role: UserId -> one Role, frozen: UserId -> one FreezeStatus } 54 FreezeStatus Frozen NotFrozen Frozen要素1 NotFrozen要素1
  23. 55.

    形式仕様記述 abstract sig FreezeStatus {} one sig Frozen, NotFrozen extends

    FreezeStatus {} abstract sig Role {} one sig General, AD extends Role {} sig UserId {} sig Oyakata { role: User -> one Role, frozen: User -> one FreezeStatus } 55 User FreezeStatus NotFrozen Frozen
  24. 56.

    形式仕様記述 abstract sig FreezeStatus {} one sig Frozen, NotFrozen extends

    FreezeStatus {} abstract sig Role {} one sig General, AD extends Role {} sig UserId {} sig Oyakata { role: User -> one Role, frozen: User -> one FreezeStatus } 56 User FreezeStatus NotFrozen Oyakata frozen Frozen
  25. 57.

    形式仕様記述 続き pred freeze(o, o': Oyakata, u: User) { o'.role

    = o.role (u -> NotFrozen) in o.frozen o'.frozen = o.frozen ++ (u -> Frozen) } 57 形式仕様記述
  26. 58.

    形式仕様記述 続き pred freeze(u, u': User, uid: UserId) { u'.role

    = u.role (uid -> NotFrozen) in u.frozen u'.frozen = u.frozen ++ (uid -> Frozen) } 58 形式仕様記述 User FreezeStatus Oyakata o o’ NotFrozen Frozen frozen
  27. 59.

    形式仕様記述 続き pred freeze(u, u': User, uid: UserId) { u'.role

    = u.role (uid -> NotFrozen) in u.frozen u'.frozen = u.frozen ++ (uid -> Frozen) } 59 形式仕様記述 User FreezeStatus Oyakata o o’ NotFrozen Frozen frozen
  28. 60.

    形式仕様記述 続き pred freeze(u, u': User, uid: UserId) { u'.role

    = u.role (uid -> NotFrozen) in u.frozen u'.frozen = u.frozen ++ (uid -> Frozen) } 60 形式仕様記述 User FreezeStatus Oyakata o o’ NotFrozen Frozen frozen
  29. 68.

    やってみてわかったこと 68 • Alloyの文法自体がチェックリストになっている − Alloyでは関係を厳密に定義していく必要がある − 日本語の仕様に曖昧な部分があれば、Alloyで 書き起こすことを通して気づくことができる •

    結論ありきなモデリングになってしまうことが多々 • αテストのフェーズで形式仕様記述を 適用するのはやはり遅い • 仕様をインクリメンタルで書いていく形が向いている気 がした
  30. 73.

    nomsをモデリングしてみた結果...「挫折」 • 原因 − Promelaの表現力不足 − 高階関数を扱えない Promela で高階関数が多用された コードを表現するのが大変

    − nomsの設計がモデル検査に向いていない − 直感的でない抽象をもつので、簡易的なモデル作成の決 断を下せない − モデル対象の関数が5つ6つほどスタックを積むので モデル化の範囲が広すぎる 73
  31. 76.

    Go -> Promela変換の手引き書の作成 76 proctype Example() { bool panic =

    false // do something _panic: panic = true goto _defer _return: _defer: // NOTE: 関数の終了を明示する。シミュレーションランの際に // panic したのかどうかをわかりやすくするために // printf を入れている。 end: printf("Example: panic=%d\n", panic) } 例1: 関数宣言(Promela)
  32. 78.

    Go -> Promela変換の手引き書の作成 例2: panic関数(Promela) 78 proctype Example() { bool

    panic = false // do something // NOTE: panic は goto を使う。defer もちゃんと実行される。 goto _panic _panic: panic = true goto _defer _return: _defer: end: printf("Example: panic=%d\n", panic) }
  33. 80.

    Goでよくあるバグを検出してみる wg := &sync.WaitGroup{} wg.Add(5) for i := 0; i

    < 5; i++ { go func() { defer wg.Done() }() wg.Wait() } 80 Understanding Real-World Concurrency Bugs in Go
  34. 81.

    Goでよくあるバグを検出してみる wg := &sync.WaitGroup{} wg.Add(5) for i := 0; i

    < 5; i++ { go func() { defer wg.Done() }() wg.Wait() } 81 Understanding Real-World Concurrency Bugs in Go Waitでブロックしてしまうので、本来はforの外に出さないといけない
  35. 83.

    検査をしてみる $ spin -a -o2 ./main.pml $ gcc -DREACH -o

    ./pan ./pan.c $ ./pan -c0 -e pan:1: invalid end state (at depth 17) pan: wrote main.pml1.trail (以降略) 83
  36. 84.

    検査をしてみる % spin -t1 main.pml S →E spin: trail ends

    after 18 steps #processes: 2 rwmutexMaxReaders = 16384 _prev_golang_chan = -1 _golang_chans[0].closed = 0 (中略) _golang_chans[9].closed = 0 group.count = 2 18: proc 1 (main:1) ./../../promela-go/golang.pml:309 (state 19) 18: proc 0 (:init::1) main.pml:65 (state 5) <valid end state> 3 processes created 84
  37. 85.

    検査をしてみる 309| inline sync_WaitGroup_Wait(wg) { 310| wg.count == 0 311|

    } 85 WaitGroup.Wait から帰ってこない状況がありうることがわかる
  38. 87.

    今後検討していくこと 87 • モデル検査の利用法 − 設計が正しいかどうかを確認する − こちらは個人利用にとどまる予感 − 書いたプログラムを(部分的に)静的解析する

    − プロジェクトで運用することを見据えるならば こちらができると嬉しい • Promelaよりももっと適切なツールがあるかどうか