runtime/secretパッケージのsecretモードが、Do配下で起動したgoroutineにも自動的に引き継がれるようになった
runtime/secret
概要
runtime/secretパッケージ(GOEXPERIMENT=runtimesecretでのみ有効な実験的パッケージ)が提供するsecret.Doは、渡した関数の呼び出しツリーで使われるスタック・レジスタ・ヒープ確保領域を、実行後にできるだけ速やかに消去する仕組みである。この変更により、Doに渡した関数の実行中にgo文で新たに起動されたgoroutineも、起動元と同じくsecretモードで実行されるようになった。これまではDo配下で起動したgoroutine自身はsecretモードを引き継がず、保護を続けたい場合は起動先でも改めてsecret.Doに包む必要があった。
使用例
Before
package main
import (
"fmt"
"runtime/secret"
)
func handleRequest(key []byte) {
secret.Do(func() {
result := process(key)
// secretモードは自動的には引き継がれないため、
// 起動するgoroutine側でも改めてDoに包む必要があった。
go secret.Do(func() {
logResult(result)
})
})
}
func process(key []byte) []byte { return key }
func logResult(b []byte) {}
func main() {
handleRequest([]byte("s3cr3t"))
fmt.Println("done")
}
After
package main
import (
"fmt"
"runtime/secret"
)
func handleRequest(key []byte) {
secret.Do(func() {
result := process(key)
// Do配下で起動したgoroutineは自動的にsecretモードを
// 引き継ぐため、起動先で改めてDoに包む必要がない。
go func() {
logResult(result)
}()
})
}
func process(key []byte) []byte { return key }
func logResult(b []byte) {}
func main() {
handleRequest([]byte("s3cr3t"))
fmt.Println("done")
}
移行時の注意
secret.Do配下で起動した既存のgo secret.Do(...)のような二重の呼び出しは、ネストしたsecretモードとして扱われるため引き続き動作する(壊れない)が、以後は冗長になる。また、secretモードは起動されたgoroutineすべてに伝播するため、Do配下で起動する非同期処理が増えるほど、スタック消去などのコストが波及する範囲も広がる点に注意が必要である。なお本パッケージはドキュメントに記載の通り現状linux/amd64とlinux/arm64のみサポートしており、それ以外の環境ではDoはfを直接呼び出すだけで保護は行われない。
実装解説
goroutineへのsecretモードの伝播は、go文でgoroutineを生成するruntime.newproc1に実装されている。生成元のgoroutine(callergp)のsecretカウンタが0より大きい場合、新しいgoroutine(newg)のsecretフィールドを1にセットすることで、新goroutineが最初からsecretモードに入った状態でスケジュールされる。
goroutineがsecretモードのままruntime.Goexitで終了する場合は、通常のDoの後処理(スタック消去)を経由しないため、runtime.goexit0がgp.secret > 0を検知してスタック全体を明示的に消去し、その後gdestroyがgp.secretを0にリセットしてgoroutine構造体を再利用可能な状態に戻している(該当コード)。
secret.Do自身が使うcount/inc/dec/eraseSecretsはgo:linknameでruntimeパッケージの実装(getg().secretを読み書きする関数群)に直結しており、これらはruntime/secret.goに定義されている。