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

about #67401 //go:linkname

about #67401 //go:linkname

tomtwinkle @tomtwinklestar
2024 年 6 月 18 日
Go Bash

ANDPAD inc

June 18, 2024
Tweet

More Decks by ANDPAD inc

Other Decks in Programming

Transcript

  1. © 2024 ANDPAD All Rights Reserved. Confidential ANDPADとは 社内 現場

    営業 / 監督 / 設計 事務 / 管理職 職人 / 業者 メーカー / 流通 現場の効率化から経営改善まで一元管理できる クラウド型建設プロジェクト管理サービス 案件管理 資料 工程表 写真 報告 チャット 黒板 図面 受発注 • • • 
 

  2. © 2024 ANDPAD All Rights Reserved. Confidential Go を間接的に使っているプロダクト ANDPAD

    の Go のプロダクト Go がメインのプロダクト The Go gopher was designed by Renée French. 施工管理 図面 引合粗利管理 検査 黒板 受発注 ボード 資料承認 おうちノート …
  3. Copyright © 2024 ANDPAD Inc. Go 1.23 highlight • Compiler:

    PGOによるオーバーヘッドの削減 • Timer/Ticker: 明⽰的なStop/Resetが不要になった • Timer/Ticker: バッファ付きチャネル利⽤取りやめ、それによる遅延解消 • unique package 爆誕 • iter package 爆誕 ◦ slices package に iter を利⽤する関数が追加 ◦ maps package に iter を利⽤する関数が追加 • slices: Repeat実装 • sync.Map: Clear実装 • structs package 爆誕(⾃作ライブラリの名前被り多発事案) • tcrypto/tls: Encrypted Client Hello(draft)に対応、SNIの暗号化が可能に • crypto/x509: OID type 実装、ASN.1の⽂字列⇔バイト配列の変換が容易に • database/sql: driver.ValuerのErrorがWrapされるように変更 • reflect: 桁溢れ検知関数(OverflowXX)の追加 • go mod tidy -diff • and more...
  4. © 2024 ANDPAD All Rights Reserved. 序章 goccy/go-json パッケージがGoのランタイムの内部APIの大部分をコピーしていたことが問題とな り、Goの開発者である

    ianlancetaylor によってIssueが開かれました。このパッケージが非常に危 険でサポートされていない操作を行っていると指摘され、その一つが GoのHEAD(将来の1.23リリー ス)で壊れてしまったと述べられました。 この問題は、Goの開発者である rsc の注意を引きました。彼は、Goの標準ライブラリの内部(特に ランタイムの内部)に //go:linkname を使ってアクセスすることが多すぎると指摘しました。これによ り、内部的には重要でないはずの標準ライブラリの変更が、 Goのエコシステムの大部分に依存して いるパッケージを壊す可能性があると彼は警告しました。 この問題を解決するために、rsc は新たな //go:linkname ベースの依存関係を防ぎ、既存のものを 制約する作業を追跡するIssueを開きました。 (Copilotによる意訳)
  5. © 2024 ANDPAD All Rights Reserved. https://github.com/golang/go/issues/67401 //go:linkname (Handshake以外で) 使うの禁止しよう!

    意訳 • Kubernetesなどで利用されている goccy/go-json は //go:linkname を用いてランタ イム内部のprivate struct(reflect.rType)を利用 していたためCL583756により壊れた • Goは互換性に気を配って開発されており、 Go のバージョンアップにより Goプログラムが破壊 される状況を望んでいない • Go 1.23で -checklinkname=1 フラグを cmd/link に追加しデフォルト有効化する ハンドシェイク形式以外の //go:linkname を禁 止する
  6. © 2024 ANDPAD All Rights Reserved. //go:linknameとは package main import

    "fmt" "time" _ "unsafe" ) //go:linkname MockNow time.Now func MockNow() time.Time { return time.Date(2024, 6, 1, 10, 0, 0, 0, time.Local) } func main() { fmt.Printf("mockTime:%s", time.Now()) } package main import ( "fmt" "time" _ "unsafe" ) //go:linkname MockNow time.Now func MockNow() time.Time { return time.Date(2024, 6, 1, 10, 0, 0, 0, time.Local) } func main() { fmt.Printf("mockTime:%s", time.Now()) // mockTime:2024-06-01 10:00:00 +0900 JST } unsafeが必須 Goのcompiler directiveであるgo:linknameは private(internal)な変数または関数のオブジェク トシンボルを定義した別名のものと同じもののよ うにエイリアス出来るようコンパイラに指示でき ます。 型システムやモジュール性を破壊するため unsafe packageを明示的にimportする必要が あります。 この例ではパッケージ内でグローバルに標準ラ イブラリのtime.Now関数を独自のMockNow関 数を上書きします。
  7. © 2024 ANDPAD All Rights Reserved. https://github.com/golang/go/issues/67401 //go:linkname Push https://go.dev/play/p/jTjcvGARWNM

    – hoge/hoge.go – package hoge import _ "unsafe" //go:linkname getHoge <project>/fuga.getHoge func getHoge() string { return "hoge" } – fuga/fuga.go – package fuga import _ "<project>/hoge" func getHoge() string – fuga/dummy.s – var/funcオブジェクトシンボ ルを別packageのシンボ ルで利用できるようPush する time.NowのmockはPush を利用している そのままではコンパイラがpackage fugaの buildを行えないため、go tool compileに渡 さないように *.s ファイル(拡張子がsであれ ばファイル名は何でも良い)をpackage内に 配置する
  8. © 2024 ANDPAD All Rights Reserved. https://github.com/golang/go/issues/67401 //go:linkname Pull https://go.dev/play/p/LRaTfIRnC8m

    – hoge/hoge.go – package hoge func getHoge() string { return "hoge" } – fuga/fuga.go – package fuga import ( _ "unsafe" _ "<project>/hoge" ) //go:linkname getHoge <project>/hoge.getHoge func getHoge() string 別packageのvar/funcオブ ジェクトシンボルを package内のシンボルに Pullする 後述する runtime.fastrand64などを 利用する際に記載されて いる事が多い
  9. © 2024 ANDPAD All Rights Reserved. https://github.com/golang/go/issues/67401 //go:linkname Handshake https://go.dev/play/p/Yr3bkzDKqI8

    – hoge/hoge.go – package hoge import _ "unsafe" //go:linkname getHoge <project>/fuga.getHoge func getHoge() string { return "hoge" } – fuga/fuga.go – package fuga import ( _ "unsafe" _ "<project>/hoge" ) //go:linkname getHoge func getHoge() Pushしたいオブジェクトシ ンボルとPullするオブジェ クトシンボル双方に go:linknameを指定する お互い関数の利用を意識 された設計
  10. © 2024 ANDPAD All Rights Reserved. //go:linknameの問題点 • internal packageやprivateな関数や変数を利用

    できる ⇨可視性/スコープの破壊 – internal/hoge.go – // projectA package internal func GetHoge() string { return "hoge" } – go.mod – module projectA – fuga.go – // projectB package fuga import projectA/internal/hoge //go:linkname GetHoge projectA/hoge.GetHoge func GetHoge() string – go.mod – module projectB internal packageの場合 本来import出来ないが利 用できてしまう
  11. © 2024 ANDPAD All Rights Reserved. //go:linknameの問題点 • Pull対象のオブジェクトが存在しない場合以外 はコンパイルエラーにならない

    • プログラム自体は動作してしまうため、深刻な 型関連のメモリエラーが発生する • 有害・危険だからこその “unsafe” https://go.dev/play/p/GGK8eC3J396 – hoge/hoge.go – package hoge func getHoge() int { return 100 } – fuga/fuga.go – package fuga import ( _ "unsafe" _ "<project>/hoge" ) //go:linkname getHoge <project>/hoge.getHoge func getHoge() string
  12. © 2024 ANDPAD All Rights Reserved. //go:linknameが必要な処理 Handshakeを利用するパターン • runtimeを利用する標準package内では今後も

    必須 • 互換性担保が難しいため公開したくないが内部 では利用したい便利関数が含まれる共通ライブ ラリなどのinternal package (でも、内部ライブラリなら publicでよくない…?) • 同じ処理をコピペで増やす必要もなく、テストも 1 箇所で済む • Hanshakeで公開先が明示的に示されている場 合は今後も利用可能であるため、このケースの 利用は許容される – internal/hoge.go – package internal //go:linkname getHoge <project>/hoge.getHoge func getHoge() string { // とてつもなく複雑なHogeの処理 return hoge } – hoge.go – package hoge import ( _ "unsafe" _ "<project>/runtime/hoge" ) //go:linkname getHoge func getHoge() string func SomethingHoge() { var hoge = getHoge() // 省略 }
  13. © 2024 ANDPAD All Rights Reserved. //go:linknameが必要な処理 Pullが必要である恐らく唯一のパターン • runtime内でユーザー定義のエントリポイント関

    数を利用したい場合 プログラムのエントリポイントは runtime内にあ る必要があるが、runtime内ではmain package はインポートできないため、 //go:linkname で処 理をバイパスする • こんなパターンはそもそも使うべきではない – runtime/hoge.go – package runtime //go:linkname main_main main.main func main_main() func main() { main_main() } – main.go – package main func main() { // runtimeから呼びたいユーザーの処理 } runtimeからユーザーの main packageはイン ポートできない https://www.cnblogs.com/apocelipes/p/18195214
  14. © 2024 ANDPAD All Rights Reserved. //go:linkname と math/rand Go

    1.20 まで • math/randは別goroutineで走ることも考慮して 毎回lockかけているのでlockのオーバヘッドが 発生し遅い • globalRandにlockedSource利用しているので 全体的にオーバヘッドのかかる処理になってい た https://cs.opensource.google/go/go/+/refs/tags/go1.20.1 4:src/math/rand/rand.go;l=403 – src/math/rand/rand.go – L304: var globalRand = New(new(lockedSource)) L334: // Uint64 returns a pseudo-random 64-bit value as a uint64 L335: // from the default Source. L336: func Uint64() uint64 { return globalRand.Uint64() } L403: type lockedSource struct { L404: lk sync.Mutex L405: s *rngSource // nil if not yet allocated L406: } L435: func (r *lockedSource) Uint64() (n uint64) { L436: r.lk.Lock() L437: n = r.source().Uint64() L438: r.lk.Unlock() L439: return L440: }
  15. © 2024 ANDPAD All Rights Reserved. //go:linkname と math/rand Go

    1.20 まで • Seed作成するための乱数生成器に高速スレッ ドローカル擬似乱数生成器である runtime.fastrand64 を利用している • runtime.fastrand64 は呼び出し元のgoroutine のcontextで動作するため、lockの必要がなく math/rand.Uint64() と fastrand64.Uint64() で は環境によりますが大体 5倍程度は早くなる https://cs.opensource.google/go/go/+/refs/tags/go1.20.1 4:src/math/rand/rand.go;l=403 – src/math/rand/rand.go – L408: //go:linkname fastrand64 L409: func fastrand64() uint64 L410: L411: var randautoseed = godebug.New("randautoseed") L412: L413: // source returns r.s, allocating and seeding it if needed. L414: // The caller must have locked r. L415: func (r *lockedSource) source() *rngSource { L416: if r.s == nil { L417: var seed int64 L418: if randautoseed.Value() == "0" { L419: seed = 1 L420: } else { L421: seed = int64(fastrand64()) L422: } L423: r.s = newSource(seed) L424: } L425: return r.s L426: }
  16. © 2024 ANDPAD All Rights Reserved. //go:linkname と math/rand #54880

    Go 1.21 • GODEBUG=randautoseed=0 を指定してSeed を明示的に指定しない限り globalRand が runtime.fastrand64 を利用する ようになった • runtime.fastrand64 を利用したいサードパー ティ製ライブラリは math/rand.Uint64() を使え ばよいので //go:linkname を利用する必要はな くなった https://cs.opensource.google/go/go/+/refs/tags/go1.21.0 :src/math/rand/rand.go;l=354;bpv=0 – src/math/rand/rand.go – L318: func globalRand() *Rand { L330: r = &Rand{ L331: src: &fastSource{}, L332: s64: &fastSource{}, L333: } L347: } L349: //go:linkname fastrand64 L350: func fastrand64() uint64 L351: L352: // fastSource is an implementation of Source64 that uses the runtime L353: // fastrand functions. L354: type fastSource struct { L355: // The mutex is used to avoid race conditions in Read. L356: mu sync.Mutex L357: } L367: func (*fastSource) Uint64() uint64 { L368: return fastrand64() L369:} L431: func Uint64() uint64 { return globalRand().Uint64() }
  17. © 2024 ANDPAD All Rights Reserved. swiss table と runtime.fastrand64

    ちなみに、cockroachdbによるswiss tableのサンプル 実装にも runtime.fastrand64 が利用されている go 1.21以降を利用するなら //go:linkname は消せる はず https://github.com/cockroachdb/swiss/blob/232b93a2b8 29cb6cbf31dba46703479d7d598bef/runtime_go1.20.go #L25-L30 – runtime_go1.20.go – //go:build go1.20 && !go1.24 package swiss import "unsafe" import _ "unsafe" //go:linkname fastrand64 runtime.fastrand64 func fastrand64() uint64 – map.go – func (m *Map[K, V]) Init(initialCapacity int, options ...Option[K, V]) { seed: uintptr(fastrand64()), func (m *Map[K, V]) All(yield func(key K, value V) bool) { // Randomize iteration order by starting iteration at a random bucket and // within each bucket at a random offset. offset := uintptr(fastrand64())
  18. © 2024 ANDPAD All Rights Reserved. //go:linkname と math/rand/v2 https://cs.opensource.google/go/go/+/refs/tags/go1.22.0:src/math/rand/rand.go;l=

    354;bpv=1;bpt=0 https://cs.opensource.google/go/go/+/refs/tags/go1.22.0:src/runtime/rand.go;l=12 6-127;bpv=1 Go 1.22 から • math/rand/v2が登場し (fastSourceと似たよう な実装の) runtimeSource を利用するように なった • math/randも併せて fastSource から runtimeSource へリネーム • math/rand, math/rand/v2 双方が内部的に runtime.fastrand64(runtime.rand) を利用する ようになったため – src/math/rand/rand.go – L318: func globalRand() *Rand { L330: r = &Rand{ L331: src: &runtimeSource{}, L332: s64: &runtimeSource{}, L333: } L349: //go:linkname runtime_rand runtime.rand L350: func runtime_rand() uint64 L351: L352: // runtimeSource is an implementation of Source64 that uses the runtime L353: // fastrand functions. L354: type runtimeSource struct { L355: // The mutex is used to avoid race conditions in Read. L356: mu sync.Mutex L357: } L367: func (*runtimeSource) Uint64() uint64 { L368: return runtime_rand() L369: } L431: func Uint64() uint64 { return globalRand().Uint64() }
  19. © 2024 ANDPAD All Rights Reserved. //go:linkname と runtime.fastrand64 math/rand,

    math/rand/v2 双方が内部的に runtime.fastrand64(runtime.rand) を利用 するようになった つまり、//go:linkname を利用するための理由の1つであるruntime.fastrand64 の利用 は Go1.21以降は既にmath/rand 及び math/rand/v2 を利用すればよいため消えてい る
  20. © 2024 ANDPAD All Rights Reserved. https://github.com/golang/go/issues/67401 Go1.23 で開発者は何をすれば良いか 意訳

    • Kubernetesなどで利用されている goccy/go-json は //go:linkname を用いてランタ イム内部のprivate struct(reflect.rType)を利用 していたためCL583756により壊れた • Goは互換性に気を配って開発されており、 Go のバージョンアップにより Goプログラムが破壊 される状況を望んでいない • Go 1.23で -checklinkname=1 フラグを cmd/link に追加しデフォルト有効化する ハンドシェイク形式以外の //go:linkname を禁 止する
  21. © 2024 ANDPAD All Rights Reserved. Go1.23 で開発者は何をすれば良いか • コードサーチで//go:linknameを利用している箇所がないか探す、//go:linknameの

    処理を削除する • -checklinkname=1フラグ追加後 (Go1.23より前になりそう)、-checklinkname=1を つけてbuildしエラーにならないか確認 • どうしてもruntime内で利用したいprivate関数があり、合理的な理由がある場合は 公式にissueを立ててpublicにして欲しいとお願いしてみる(望みは薄いが) • -ldflags=-checklinkname=0をつけてしばらく耐える
  22. © 2024 ANDPAD All Rights Reserved. 不名誉殿堂入りリスト • 悲しいことに不名誉殿堂入りしてしまったpackageは"Notable members

    of the hall of shame include"で検索すると色々出てきます https://cs.opensource.google/search?q=%22Notable%20members%20of%20the %20hall%20of%20shame%20include%22&ss=go%2Fgo • 理由は分かりませんがとても動きが早かったので、rscは相当怒ってそう • 「不名誉殿堂入り」ホワイトリストに入れられたlibraryはHandshake出来るので buildは出来る(許されたとは言っていない)
  23. © 2024 ANDPAD All Rights Reserved. https://tip.golang.org/doc/go1.23 go:linkname 騒動のその後 意訳

    • //go:linkname で runtimeなど内部シンボルにア クセスできなくするよ • 「不名誉殿堂入りリスト」に入れた OSSは取り敢 えず今まで通り使えるように Handshakeで許可 するよ • ただし、新しく作るのは許さん
  24. © 2024 ANDPAD All Rights Reserved. https://tip.golang.org/doc/go1.23 go:linkname 騒動のその後 意訳

    • //go:linkname で runtimeなど内部シンボルにア クセスできなくするよ • 「不名誉殿堂入りリスト」に入れた OSSは取り敢 えず今まで通り使えるように Handshakeで許可 するよ • ただし、新しく作るのは許さん
  25. © 2024 ANDPAD All Rights Reserved. go build -overlay スレッドセーフなテスト用の時間を固定するライブラリを作った

    | tenntenn.dev https://tenntenn.dev/ja/posts/2021-07-06-testtime/ Goでモンキーパッチするライブラリを作った | Plan 9とGo言語のブログ https://blog.lufia.org/entry/2024/05/28/214447 • tenntenn/testtime, lufia/plug が利用しているのは //go:linkname コンパイラディレ クティブ ではなく go build の -overlay オプション • ユーザーの明示的な指定で動作するため //go:linknameのように即座に消される 事はないだろう(たぶん) • モンキーパッチはできるだけ避けたいがMock用途で //go:linknameを使っている 場合はこっちの逃げ道はある
  26. © 2024 ANDPAD All Rights Reserved. go build -overlay go

    build -overlay <json> • jsonでbuild時に置き換えたいファイルを指定 する • 同一package名である必要あり • json内で ${GOROOT} 書いても動かないの でtime packageなどをmockしたい場合はフ ルパスで書く必要あり • 同一package内のファイルは基本的には全コ ピーしないと動かない (事が多い) – main.go – package main import "overlaytest/hoge" func main() { hoge.Print() } – hoge/hoge.go – package hoge func Print() { println("hoge!") } – mock/hoge/hoge.go – package hoge func Print() { println("Mock hoge!") } – overlay.json – { "Replace": { "./hoge/hoge.go": "./mock/hoge/hoge.go" } } > go run . hoge! > go run -overlay=overlay.json . Mock hoge!
  27. © 2024 ANDPAD All Rights Reserved. Confidential Copyright © 2023

    ANDPAD Inc. いま建築‧建設業界で “ものづくり” に携わる⽅の⼈⼿不⾜や ⻑時間労働が社会問題となっています。 今後これらの課題に対して、デジタルシフトによる⽣産性向上や、 就労者数の底上げを急ぐ必要があります。 本来、ものづくりに携わる⼈々は、誰かに幸せを届ける⼈たちです。 そんな⽅々がもっとクリエイティブに、もっと豊かに働けるよう、 私たちは熱い想いで⽇々現場に向き合っています。