Share
Embed
ssa packageを用いたSpannerにおける現在時刻誤用の静的検出 (Go Night Talks - After Conference)
2025/10/14 Go Night Talks - After Conference で発表した内容です
https://mercari.connpass.com/event/367075/
kobaryo
October 14, 2025
Transcript
31 slides
-
1 ssa packageを用いたSpannerにおける 現在時刻誤用の静的検出 2025/10/14 Go Night Talks - After
Conference @kobaryo -
2 • 2024年にメルペイに新卒入社 • 残高管理基盤を扱うBalance Team の バックエンドエンジニア
• 静的解析とプログラム検証に興味が あります @kobaryo(小林 亮太) -
6 Spanner Google Cloudが提供する分散DB • External Consistency [1] を満たしている ◦
トランザクションT1の完了後にT2が始まると、T1のcommit timestampよりT2のcommit timestampが後になる -
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}) -
8 Spannerにおける現在時刻誤用 誤ってspanner.CommitTimestampではなくtime.Now()を入 れると......? → エラーになったりならなかったりする • commit timestampより未来の時刻を入れることが できないため、サーバーとSpannerの時刻のずれの
度合いによって発生したりしなかったりする spanner: code = "FailedPrecondition", desc = "Cannot write timestamps in the future … -
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()の可能性が あるので、検知したい -
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に挿入されるかをチェック -
13 ssa package [3] GoプログラムのSSAを提供するpackage • SSA(静的単一代入) : プログラムの中間表現の一つで、各変 数への代入が一つになる形式
◦ Goにはポインタがあるので、同じ変数でも代入なしで値が変 わる可能性がある • 変数の定義や使用がシンプルになるので、値がどのように 伝わっていくか追いやすい -
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に 代入される値が変わる -
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 -
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()の返り値をマー クして、マークした変数が影響 する変数をまた マークする、という操作を繰り 返す -
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をマーク -
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をマーク -
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をマーク -
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をマーク -
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をマーク -
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をマーク -
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まで辿る必要 があるが、今回は省略 -
27 まとめ • Spannerのallow_commit_timestampである列には time.Now()を入れてはいけない • ssa packageを用いることで特定の値がどのように伝わるか 追跡できる ◦
マークした変数が影響する変数をマークする 、という 操作を繰り返す ◦ このアプローチ自体はSpannerに限った話ではなく、 汎用的 -
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 参照) -
30 他の方法でチェックできないの? • 型でチェックできない? ◦ spanner.CommitTimestampもtime.Time型 • 値をバリデーションしてspanner.CommitTimestampしか入れ ないようにできる? ◦
allow_commit_timestampでも過去の時間は入れられる ため、そのようなユースケースがあると困る • time.Now()をgrepすれば十分? ◦ allow_commit_timestampでない列も混ざっていると time.Now()を使いたくなる