Go界隈で巻き起こった go:linkname 騒動について

お久しぶりです、ANDPADボードの tomtwinkle です。

この記事はGoの go:linkname 騒動は 6/18に行われた Go Bash で話した内容を要約したものです。

そもそも go:linkname とは何かといえば internal packageやprivate var/funcなど普通はアクセスできないオブジェクトシンボルをエイリアス出来るようCompilerに指示して、アクセス可能にするcompiler directiveです。

go:linkname はprivateな変数へアクセス可能な便利なものでしたが unsafe packageのimportを必須とする通り、せっかく互換性や安全を考慮して作られているGoプログラムを簡単に破壊できる諸刃の剣でした。

詳細は発表スライドを見てください。

go:linkname 禁止騒動

Go 1.23 のリリースまで2ヶ月を切ったタイミングで突如吹き上がった #67401 というissueが一部のOSS開発を行っているGopher達をザワつかさせていました。

github.com

事の発端は今から1ヶ月程前、 goccy/go-json というGoでは有名なOSSのjson libraryに、GoのコントリビューターであるIan Lance Taylor氏がとあるissueが立ち上げました。

github.com

goccy/go-json は 標準packageで提供されている encoding/json コンパチのまま高速化することを売りにしているlibraryです。

README の Unique speed-up technique の項に記述されている通りreflect周りのエスケープ処理をあえて無視する結構攻めたhackを一部処理で差し込んでおり、Goの標準packageであるreflectのprivate structにアクセスするため go:linkname を利用しています。

//go:linkname rtype_Method reflect.(*rtype).Method
//go:noescape
func rtype_Method(*Type, int) reflect.Method

func (t *Type) Method(a0 int) reflect.Method {
    return rtype_Method(t, a0)
}

go:linknameと runtime.fastrand64() の課題

先述のissueは Go 1.23 のリリースでreflectの内部実装が変わるため go:linkname でprivate structを呼んでいる goccy/go-json が動作しなくなる旨を警告するものでした。

GitHubを探してみると //go:linkname を利用しているlibraryは他にもたくさん見つかりますがでは何故ここで goccy/go-json が警告の対象になったのかというと、 goccy/go-json がKubernetesなど非常に多くのOSSで利用されており影響範囲が大きかったためです。

というわけで、一番最初の Russ Cox氏 による golang/go の issue #67401 に立ち戻ります。

github.com

Goは互換性を非常に大事にしている言語であるため、Goのメジャーバージョンが上がったことにより今まで動いていたGoプログラムが動かなくようなことがないよう非常に気を使って開発されています。 go:linkname のような下手すればビルドエラーにもならずに実行時に初めて深刻なメモリエラーが発生するような危険な実装は、プロダクションコード内で動作するようなlibraryで実装されることを想定していなかったのかもしれません。

go:linkname も ユーザーのlibrary内で閉じた範囲でunsafe なことを承知で使うという場合は問題なかったのですが Go 1.20 までのmath/rand.Uint64() はlock処理がボトルネックになっており、seedの初期値などを生成するために頻繁に乱数を生成する場合、同一のgoroutine内で処理可能でより高速に動作する内部実装 runtime.fastrand()runtime.fastrand64()go:linkname で内部利用する記述が割と一般的に行われていたりしました。

弊社の小島さんが Go Conference 2024 で発表したSwissTableでも SwissTableのサンプル実装にはseed値の作成のために go:linknameruntime.fastrand64() が利用されています。 speakerdeck.com

package swiss

import "unsafe"

import _ "unsafe"

//go:linkname fastrand64 runtime.fastrand64
func fastrand64() uint64map.gofunc (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())

Go 1.21 での math/rand ステルス修正

先ほど Go 1.20 までのmath/rand.Uint64()は と書きましたが Go 1.21 より math/rand.Uint64() は内部的に runtime.fastrand64() を呼ぶように修正されています。

– 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() }

この事にGo 1.21時点で触れている記事は自分が観測する限りでは存在せず、自分自身も今回調べて初めて知ったのですが それもそのはず、Go 1.21のリリースノートにはそももそ書かれていませんでした。

go.dev

この事は golang/go の issue を細かくwatchしている Gopherのみ知っている情報だったかもしれません。

github.com

これによりGo 1.21 よりわざわざ math/rand.Uint64() の代わりに runtime.fastrand64() を利用するために go:linkname を利用する必要がなくなっていました。

Go 1.22 で実装された math/rand/v2 も同様の実装を行っているため、go:linkname で無理やりruntimeにアクセスせず、素直に標準packageを使っとけという状態に立ち返ったのでした。

もしかすると Russ Cox氏らは go:linknameruntime.fastrand64() の課題を事前に把握した上で修正版を出し 課題が解決したことで今回Go 1.23 で満を期して一気に禁止という流れに行ったのかもしれません。

Genericsやiterの件でもそうですがGoチームは結構こういった根気の入った根回しをするんですよね。

go:linkname 禁止騒動のその後

#67401 でも go:linkname は Handshake方式の場合のみは許可すると前置きされていました。 Handshakeとはgo:linknameを利用するPush先のオブジェクトシンボルとPull元のオブジェクトシンボルがお互いに go:linkname で利用するpackageをホワイトリスト形式で記載している場合です。

結局 go:linkname を現状使っている著名なOSSがどうなったかというと、「不名誉殿堂入りリスト」という名のもと、標準package内にHandshake方式でpackage名が記載される流れとなりました。

こうしてみると色々なlibraryがruntimeに依存していたことが分かりますね。 今回は「不名誉殿堂入りリスト」入りだけで許してやるが新規libraryは許さんという感じの決着になっています。

Go 1.23のDraft版リリースノートにも割と強めに Any new references to standard library internal symbols will be disallowed. と書かれているので今後新しく「不名誉殿堂入りリスト」するlibraryが現れることは恐らくないんじゃないかなと思います。

Goを使ってカリカリにパフォーマンスチューニングしたい場合は、今「不名誉殿堂入りリスト」に入ってるlibraryを利用するか大人しくissueを立てて議論するしかなさそうです。

tip.golang.org

 

アンドパッドでは Go の issue や開発模様をウォッチするほど、 Go を愛してやまない Gopher を募集しています!

hrmos.co