Upgrade to Pro — share decks privately, control downloads, hide ads and more …

ssa packageを用いたSpannerにおける現在時刻誤用の静的検出 (Go Night...

Avatar for kobaryo kobaryo
October 14, 2025
28

ssa packageを用いたSpannerにおける現在時刻誤用の静的検出 (Go Night Talks - After Conference)

2025/10/14 Go Night Talks - After Conference で発表した内容です
https://mercari.connpass.com/event/367075/

Avatar for kobaryo

kobaryo

October 14, 2025
Tweet

Transcript

  1. 2 • 2024年にメルペイに新卒入社 
 • 残高管理基盤を扱うBalance Team の バックエンドエンジニア 


    • 静的解析とプログラム検証に興味が 
 あります
 @kobaryo(小林 亮太) 

  2. 6 Spanner Google Cloudが提供する分散DB • External Consistency [1] を満たしている ◦

    トランザクションT1の完了後にT2が始まると、T1のcommit timestampよりT2のcommit timestampが後になる
  3. 7 allow_commit_timestamp オプション [2] このオプションがある列にはSpannerのcommit timestampを 入れることができる • commit timestampの順序からトランザクションの順序が復元

    できるので、 changelogを作るのに役立つ mutation := spanner.Insert("document_histories", []string{"id", "document_id", "delta", "timestamp"}, []any{id, doc.id, delta, spanner.CommitTimestamp}) _, err := db.client.Apply(db.ctx, []*spanner.Mutation{mutation})
  4. 10 今回の検証対象プログラム var now time.Time if isNow { now =

    time.Now() } else { now = time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) } _, err := client.Apply(ctx, []*spanner.Mutation{ spanner.Insert("Users", []string{"name", "created_at"}, []interface{}{"Alice", now}, ), }) return err isNowはtrueかfalseか 分からない time.Now()の可能性が あるので、検知したい
  5. 11 素朴なアイデア var now time.Time if isNow { now =

    time.Now() } else { now = time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) } _, err := client.Apply(ctx, []*spanner.Mutation{ spanner.Insert("Users", []string{"name", "created_at"}, []interface{}{"Alice", now}, ), }) return err nowはtime.Now()の返り値なのでマーク マークしたnowが Spannerに挿入されるの で検知 time.Now()の結果が伝わる変数を辿って いき、Spannerに挿入されるかをチェック
  6. 13 ssa package [3] GoプログラムのSSAを提供するpackage • SSA(静的単一代入) : プログラムの中間表現の一つで、各変 数への代入が一つになる形式

    ◦ Goにはポインタがあるので、同じ変数でも代入なしで値が変 わる可能性がある • 変数の定義や使用がシンプルになるので、値がどのように 伝わっていくか追いやすい
  7. 14 SSAへの変換例 func abs(x int) int { if x <

    0 { x = -x } return x } 0: t0 = x < 0:int if t0 goto 1 else 2 1: t1 = -x jump 2 2: t2 = phi [0: x, 1: t1] #x return t2 func abs(x int) int: xへの再代入は新しい 変数への代入に変換 分岐に応じてt2に 代入される値が変わる
  8. 16 検証対象の SSAへの変換 var now time.Time if isNow { now

    = time.Now() } else { now = time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) } _, err := client.Apply(ctx, []*spanner.Mutation{ spanner.Insert("Users", []string{"name", "created_at"}, []interface{}{"Alice", now}, ), }) return err 2: t1 = phi [1: t0, 3: t20] #now t2 = new [1]*single_func/vendor/cloud.google.com/go/spanner.Mutation (slicelit) t3 = &t2[0:int] t4 = new [2]string (slicelit) t5 = &t4[0:int] *t5 = "name":string t6 = &t4[1:int] *t6 = "created_at":string t7 = slice t4[:] t8 = new [2]interface{} (slicelit) t9 = &t8[0:int] t10 = make interface{} <- string ("Alice":string) *t9 = t10 t11 = &t8[1:int] t12 = make interface{} <- time.Time (t1) *t11 = t12 t13 = slice t8[:] t14 = single_func/vendor/cloud.google.com/go/spanner.Insert("Users":string, t7, t13) *t3 = t14 t15 = slice t2[:] t16 = (*single_func/vendor/cloud.google.com/go/spanner.Client).Apply(client, ctx, t15, nil:[]single_func/vendor/cloud.google.com/go/spanner.ApplyOption...) t17 = extract t16 #0 t18 = extract t16 #1 return t18 0: if isNow goto 1 else 3 3: t19 = *time.UTC t20 = time.Date(2020:int, 1:time.Month, 1:int, 0:int, 0:int, 0:int, 0:int, t19) time.Time jump 2 1: t0 = time.Now() jump 2
  9. 17 1: t0 = time.Now() jump 2 データフローの追跡例 2: t1

    = phi [1: t0, 3: t20] #now t2 = new [1]*single_func/vendor/cloud.google.com/go/spanner.Mutation (slicelit) t3 = &t2[0:int] t4 = new [2]string (slicelit) t5 = &t4[0:int] *t5 = "name":string t6 = &t4[1:int] *t6 = "created_at":string t7 = slice t4[:] t8 = new [2]interface{} (slicelit) t9 = &t8[0:int] t10 = make interface{} <- string ("Alice":string) *t9 = t10 t11 = &t8[1:int] t12 = make interface{} <- time.Time (t1) *t11 = t12 t13 = slice t8[:] t14 = single_func/vendor/cloud.google.com/go/spanner.Insert("Users":string, t7, t13) *t3 = t14 t15 = slice t2[:] t16 = (*single_func/vendor/cloud.google.com/go/spanner.Client).Apply(client, ctx, t15, nil:[]single_func/vendor/cloud.google.com/go/spanner.ApplyOption...) t17 = extract t16 #0 t18 = extract t16 #1 return t18 0: if isNow goto 1 else 3 3: t19 = *time.UTC t20 = time.Date(2020:int, 1:time.Month, 1:int, 0:int, 0:int, 0:int, 0:int, t19) time.Time jump 2 time.Now()の返り値をマー クして、マークした変数が影響 する変数をまた マークする、という操作を繰り 返す
  10. 18 データフローの追跡例 2: t1 = phi [1: t0, 3: t20]

    #now t2 = new [1]*single_func/vendor/cloud.google.com/go/spanner.Mutation (slicelit) t3 = &t2[0:int] t4 = new [2]string (slicelit) t5 = &t4[0:int] *t5 = "name":string t6 = &t4[1:int] *t6 = "created_at":string t7 = slice t4[:] t8 = new [2]interface{} (slicelit) t9 = &t8[0:int] t10 = make interface{} <- string ("Alice":string) *t9 = t10 t11 = &t8[1:int] t12 = make interface{} <- time.Time (t1) *t11 = t12 t13 = slice t8[:] t14 = single_func/vendor/cloud.google.com/go/spanner.Insert("Users":string, t7, t13) *t3 = t14 t15 = slice t2[:] t16 = (*single_func/vendor/cloud.google.com/go/spanner.Client).Apply(client, ctx, t15, nil:[]single_func/vendor/cloud.google.com/go/spanner.ApplyOption...) t17 = extract t16 #0 t18 = extract t16 #1 return t18 0: if isNow goto 1 else 3 3: t19 = *time.UTC t20 = time.Date(2020:int, 1:time.Month, 1:int, 0:int, 0:int, 0:int, 0:int, t19) time.Time jump 2 1: t0 = time.Now() jump 2 t0 = time.Now() time.Now()の返り値なの で、t0をマーク
  11. 19 1: t0 = time.Now() jump 2 データフローの追跡例 2: t1

    = phi [1: t0, 3: t20] #now t2 = new [1]*single_func/vendor/cloud.google.com/go/spanner.Mutation (slicelit) t3 = &t2[0:int] t4 = new [2]string (slicelit) t5 = &t4[0:int] *t5 = "name":string t6 = &t4[1:int] *t6 = "created_at":string t7 = slice t4[:] t8 = new [2]interface{} (slicelit) t9 = &t8[0:int] t10 = make interface{} <- string ("Alice":string) *t9 = t10 t11 = &t8[1:int] t12 = make interface{} <- time.Time (t1) *t11 = t12 t13 = slice t8[:] t14 = single_func/vendor/cloud.google.com/go/spanner.Insert("Users":string, t7, t13) *t3 = t14 t15 = slice t2[:] t16 = (*single_func/vendor/cloud.google.com/go/spanner.Client).Apply(client, ctx, t15, nil:[]single_func/vendor/cloud.google.com/go/spanner.ApplyOption...) t17 = extract t16 #0 t18 = extract t16 #1 return t18 0: if isNow goto 1 else 3 3: t19 = *time.UTC t20 = time.Date(2020:int, 1:time.Month, 1:int, 0:int, 0:int, 0:int, 0:int, t19) time.Time jump 2 t1 = phi [1: t0, 3: t20] t1にはt0かt20が代入される ため、t1をマーク
  12. 20 2: t1 = phi [1: t0, 3: t20] #now

    t2 = new [1]*single_func/vendor/cloud.google.com/go/spanner.Mutation (slicelit) t3 = &t2[0:int] t4 = new [2]string (slicelit) t5 = &t4[0:int] *t5 = "name":string t6 = &t4[1:int] *t6 = "created_at":string t7 = slice t4[:] t8 = new [2]interface{} (slicelit) t9 = &t8[0:int] t10 = make interface{} <- string ("Alice":string) *t9 = t10 t11 = &t8[1:int] t12 = make interface{} <- time.Time (t1) *t11 = t12 t13 = slice t8[:] t14 = single_func/vendor/cloud.google.com/go/spanner.Insert("Users":string, t7, t13) *t3 = t14 t15 = slice t2[:] t16 = (*single_func/vendor/cloud.google.com/go/spanner.Client).Apply(client, ctx, t15, nil:[]single_func/vendor/cloud.google.com/go/spanner.ApplyOption...) t17 = extract t16 #0 t18 = extract t16 #1 return t18 1: t0 = time.Now() jump 2 データフローの追跡例 0: if isNow goto 1 else 3 3: t19 = *time.UTC t20 = time.Date(2020:int, 1:time.Month, 1:int, 0:int, 0:int, 0:int, 0:int, t19) time.Time jump 2 t12 = make interface{} <- time.Time (t1) t12にはt1を値として持つ interface{}のインスタンス となるため、t12をマーク
  13. 21 2: t1 = phi [1: t0, 3: t20] #now

    t2 = new [1]*single_func/vendor/cloud.google.com/go/spanner.Mutation (slicelit) t3 = &t2[0:int] t4 = new [2]string (slicelit) t5 = &t4[0:int] *t5 = "name":string t6 = &t4[1:int] *t6 = "created_at":string t7 = slice t4[:] t8 = new [2]interface{} (slicelit) t9 = &t8[0:int] t10 = make interface{} <- string ("Alice":string) *t9 = t10 t11 = &t8[1:int] t12 = make interface{} <- time.Time (t1) *t11 = t12 t13 = slice t8[:] t14 = single_func/vendor/cloud.google.com/go/spanner.Insert("Users":string, t7, t13) *t3 = t14 t15 = slice t2[:] t16 = (*single_func/vendor/cloud.google.com/go/spanner.Client).Apply(client, ctx, t15, nil:[]single_func/vendor/cloud.google.com/go/spanner.ApplyOption...) t17 = extract t16 #0 t18 = extract t16 #1 return t18 1: t0 = time.Now() jump 2 データフローの追跡例 0: if isNow goto 1 else 3 3: t19 = *time.UTC t20 = time.Date(2020:int, 1:time.Month, 1:int, 0:int, 0:int, 0:int, 0:int, t19) time.Time jump 2 *t11 = t12 ポインタt11の指す先がt12 となるため、t11をマーク
  14. 22 2: t1 = phi [1: t0, 3: t20] #now

    t2 = new [1]*single_func/vendor/cloud.google.com/go/spanner.Mutation (slicelit) t3 = &t2[0:int] t4 = new [2]string (slicelit) t5 = &t4[0:int] *t5 = "name":string t6 = &t4[1:int] *t6 = "created_at":string t7 = slice t4[:] t8 = new [2]interface{} (slicelit) t9 = &t8[0:int] t10 = make interface{} <- string ("Alice":string) *t9 = t10 t11 = &t8[1:int] t12 = make interface{} <- time.Time (t1) *t11 = t12 t13 = slice t8[:] t14 = single_func/vendor/cloud.google.com/go/spanner.Insert("Users":string, t7, t13) *t3 = t14 t15 = slice t2[:] t16 = (*single_func/vendor/cloud.google.com/go/spanner.Client).Apply(client, ctx, t15, nil:[]single_func/vendor/cloud.google.com/go/spanner.ApplyOption...) t17 = extract t16 #0 t18 = extract t16 #1 return t18 1: t0 = time.Now() jump 2 データフローの追跡例 0: if isNow goto 1 else 3 3: t19 = *time.UTC t20 = time.Date(2020:int, 1:time.Month, 1:int, 0:int, 0:int, 0:int, 0:int, t19) time.Time jump 2 t11 = &t8[1:int] t8の1番目の要素がt11を 指すので、t8をマーク
  15. 23 2: t1 = phi [1: t0, 3: t20] #now

    t2 = new [1]*single_func/vendor/cloud.google.com/go/spanner.Mutation (slicelit) t3 = &t2[0:int] t4 = new [2]string (slicelit) t5 = &t4[0:int] *t5 = "name":string t6 = &t4[1:int] *t6 = "created_at":string t7 = slice t4[:] t8 = new [2]interface{} (slicelit) t9 = &t8[0:int] t10 = make interface{} <- string ("Alice":string) *t9 = t10 t11 = &t8[1:int] t12 = make interface{} <- time.Time (t1) *t11 = t12 t13 = slice t8[:] t14 = single_func/vendor/cloud.google.com/go/spanner.Insert("Users":string, t7, t13) *t3 = t14 t15 = slice t2[:] t16 = (*single_func/vendor/cloud.google.com/go/spanner.Client).Apply(client, ctx, t15, nil:[]single_func/vendor/cloud.google.com/go/spanner.ApplyOption...) t17 = extract t16 #0 t18 = extract t16 #1 return t18 1: t0 = time.Now() jump 2 データフローの追跡例 0: if isNow goto 1 else 3 3: t19 = *time.UTC t20 = time.Date(2020:int, 1:time.Month, 1:int, 0:int, 0:int, 0:int, 0:int, t19) time.Time jump 2 t13 = slice t8[:] t13は配列t8をsliceにした ものなので、t13をマーク
  16. 24 2: t1 = phi [1: t0, 3: t20] #now

    t2 = new [1]*single_func/vendor/cloud.google.com/go/spanner.Mutation (slicelit) t3 = &t2[0:int] t4 = new [2]string (slicelit) t5 = &t4[0:int] *t5 = "name":string t6 = &t4[1:int] *t6 = "created_at":string t7 = slice t4[:] t8 = new [2]interface{} (slicelit) t9 = &t8[0:int] t10 = make interface{} <- string ("Alice":string) *t9 = t10 t11 = &t8[1:int] t12 = make interface{} <- time.Time (t1) *t11 = t12 t13 = slice t8[:] t14 = single_func/vendor/cloud.google.com/go/spanner.Insert("Users":string, t7, t13) *t3 = t14 t15 = slice t2[:] t16 = (*single_func/vendor/cloud.google.com/go/spanner.Client).Apply(client, ctx, t15, nil:[]single_func/vendor/cloud.google.com/go/spanner.ApplyOption...) t17 = extract t16 #0 t18 = extract t16 #1 return t18 1: t0 = time.Now() jump 2 データフローの追跡例 0: if isNow goto 1 else 3 3: t19 = *time.UTC t20 = time.Date(2020:int, 1:time.Month, 1:int, 0:int, 0:int, 0:int, 0:int, t19) time.Time jump 2 t14 = …/spanner.Insert( "Users":string, t7, t13 ) t14はt13を引数とした spanner.Insertのため、 検知!! 本当はspanner.Applyまで辿る必要 があるが、今回は省略
  17. 27 まとめ • Spannerのallow_commit_timestampである列には time.Now()を入れてはいけない • ssa packageを用いることで特定の値がどのように伝わるか 追跡できる ◦

    マークした変数が影響する変数をマークする 、という 操作を繰り返す ◦ このアプローチ自体はSpannerに限った話ではなく、 汎用的
  18. 28 参考文献 [1] Spanner: TrueTime and external consistency https://cloud.google.com/spanner/docs/true-time-external-consistency. (2025-10-14

    参照) [2] Commit timestamps in GoogleSQL-dialect databases. https://cloud.google.com/spanner/docs/commit-timestamp. (2025-10-14 参照) [3] ssa package. https://pkg.go.dev/golang.org/x/tools/go/ssa. (2025-10-14 参照)
  19. 30 他の方法でチェックできないの? • 型でチェックできない? ◦ spanner.CommitTimestampもtime.Time型 • 値をバリデーションしてspanner.CommitTimestampしか入れ ないようにできる? ◦

    allow_commit_timestampでも過去の時間は入れられる ため、そのようなユースケースがあると困る • time.Now()をgrepすれば十分? ◦ allow_commit_timestampでない列も混ざっていると time.Now()を使いたくなる