Cloudflareの330都市に広がるデータセンター全体で、毎秒8400万件のHTTPリクエストが送信されています。そのため、稀なバグでも頻繁に発生する可能性があります。実際、最近、GoのARM64コンパイラでバグが発見され、生成されたコードで競合状態が発生するのは当社のスケールが原因でした。
この記事では、私たちが最初にバグを発見し、調査し、最終的に根本原因を特定した経緯について説明します。
    
    Magic TransitやMagic WANなどの一部の製品のトラフィックを処理するためにカーネルを設定するサービスをネットワーク内で実行しています。Cloudflareの監視により、Arm64マシン上で非常に散発的なパニックが観測されるようになりました。
最初に目にしたのは、トレースバックが完全にアンウィンドにならなかったという重大なエラーです。このエラーは、おそらくスタックの破損が原因で、スタックを横断する際に不変量が侵害されたことを示唆しています。簡単な調査の後、これはおそらく稀なスタックメモリの破損であると判断されました。これは概してアイドル状態のコントロールプレーンサービスであり、計画外の再起動による影響はごくわずかでした。そのため、それを継続する限り、フォローアップは優先事項ではないと考えました。
そして、それが続くのです。
    
    
          
          
          最初にこのバグを回復した時、重大なエラーがパニックと相関があることがわかりました。これらは、パニック/リカバリをエラー処理として使用していた一部の古いコードが原因でした。
この時点で、当社の理論は次のとおりでした。
重大なパニックはすべてスタックアンファイル内で起こります。
回復したパニックの量の増加と、これらの重大なパンデミックの発生と相関関係にありました。
パニックを回復することで、遅延された関数を呼び出すために、ルーティングスタックが解除されます。
Goに関連する問題(#73259)では、ARM64スタックのアンウィンドクラッシュが報告されました。
エラー処理にpanic/recoverの使用をやめて、アップストリームの修正を待ちましょう?
そこで、当社はこれを実行し、リリースがロールアウトするにつれて重大なパニックが発生するのを防ぎました。致命的なパニックはなくなり、理論上の軽減策が有効になっているようで、これはもはや私たちの問題ではなくなりました。私たちはアップストリームの問題を購読し、問題が解決されたら更新し、それを念頭から置くことにしました。
しかし、これは予想以上に奇妙なバグであることが判明しました。同じクラスの致命的なパニックがはるかに高い割合で再び出現してきたため、それを考えることは時期尚早でした。1か月後、実際に識別可能な原因のない、重大なパニックが毎日30件以上発生しました。データセンターの1日あたり1台のマシンしか使用していないかもしれませんが、私たちが原因を理解していないのは懸念点です。最初に確認したのは、以前のパターンと一致させるために、回復されたパニックの数でしたが、ありませんでした。さらに興味深いことに、重大なパニック的割合の増加が他と相関することはありませんでした。リリース?インフラストラクチャの変更Marsの役職 
私たちはこの時点で、根本的な原因を理解するためにさらに深く掘り下げる必要があると感じました。パターンマッチングと期待が明らかに不十分でした。
このバグには、無効なメモリにアクセスした際のクラッシュと、明示的に確認された重大なエラーという2つのクラスのバグが見られました。
    
    
            goroutine 153 gp=0x4000105340 m=324 mp=0x400639ea08 [GC worker (active)]:
/usr/local/go/src/runtime/asm_arm64.s:244 +0x6c fp=0x7ff97fffe870 sp=0x7ff97fffe860 pc=0x55558d4098fc
runtime.systemstack(0x0)
       /usr/local/go/src/runtime/mgc.go:1508 +0x68 fp=0x7ff97fffe860 sp=0x7ff97fffe810 pc=0x55558d3a9408
runtime.gcBgMarkWorker.func2()
       /usr/local/go/src/runtime/mgcmark.go:1102
runtime.gcDrainMarkWorkerIdle(...)
       /usr/local/go/src/runtime/mgcmark.go:1188 +0x434 fp=0x7ff97fffe810 sp=0x7ff97fffe7a0 pc=0x55558d3ad514
runtime.gcDrain(0x400005bc50, 0x7)
       /usr/local/go/src/runtime/mgcmark.go:212 +0x1c8 fp=0x7ff97fffe7a0 sp=0x7ff97fffe6f0 pc=0x55558d3ab248
runtime.markroot(0x400005bc50, 0x17e6, 0x1)
       /usr/local/go/src/runtime/mgcmark.go:238 +0xa8 fp=0x7ff97fffe6f0 sp=0x7ff97fffe6a0 pc=0x55558d3ab578
runtime.markroot.func1()
       /usr/local/go/src/runtime/mgcmark.go:887 +0x290 fp=0x7ff97fffe6a0 sp=0x7ff97fffe560 pc=0x55558d3acaa0
runtime.scanstack(0x4014494380, 0x400005bc50)
       /usr/local/go/src/runtime/traceback.go:447 +0x2ac fp=0x7ff97fffe560 sp=0x7ff97fffe4d0 pc=0x55558d3eeb7c
runtime.(*unwinder).next(0x7ff97fffe5b0?)
       /usr/local/go/src/runtime/traceback.go:566 +0x110 fp=0x7ff97fffe4d0 sp=0x7ff97fffe490 pc=0x55558d3eed40
runtime.(*unwinder).finishInternal(0x7ff97fffe4f8?)
       /usr/local/go/src/runtime/panic.go:1073 +0x38 fp=0x7ff97fffe490 sp=0x7ff97fffe460 pc=0x55558d403388
runtime.throw({0x55558de6aa27?, 0x7ff97fffe638?})
runtime stack:
fatal error: traceback did not unwind completely
       stack=[0x4015d6a000-0x4015d8a000
runtime: g8221077: frame.sp=0x4015d784c0 top=0x4015d89fd0
            
    
    
            goroutine 187 gp=0x40003aea80 m=13 mp=0x40003ca008 [GC worker (active)]:
       /usr/local/go/src/runtime/asm_arm64.s:244 +0x6c fp=0x7fff2afde870 sp=0x7fff2afde860 pc=0x55557e2d98fc
runtime.systemstack(0x0)
       /usr/local/go/src/runtime/mgc.go:1489 +0x94 fp=0x7fff2afde860 sp=0x7fff2afde810 pc=0x55557e279434
runtime.gcBgMarkWorker.func2()
       /usr/local/go/src/runtime/mgcmark.go:1112
runtime.gcDrainMarkWorkerDedicated(...)
       /usr/local/go/src/runtime/mgcmark.go:1188 +0x434 fp=0x7fff2afde810 sp=0x7fff2afde7a0 pc=0x55557e27d514
runtime.gcDrain(0x4000059750, 0x3)
       /usr/local/go/src/runtime/mgcmark.go:212 +0x1c8 fp=0x7fff2afde7a0 sp=0x7fff2afde6f0 pc=0x55557e27b248
runtime.markroot(0x4000059750, 0xb8, 0x1)
       /usr/local/go/src/runtime/mgcmark.go:238 +0xa8 fp=0x7fff2afde6f0 sp=0x7fff2afde6a0 pc=0x55557e27b578
runtime.markroot.func1()
       /usr/local/go/src/runtime/mgcmark.go:887 +0x290 fp=0x7fff2afde6a0 sp=0x7fff2afde560 pc=0x55557e27caa0
runtime.scanstack(0x40042cc000, 0x4000059750)
       /usr/local/go/src/runtime/traceback.go:458 +0x188 fp=0x7fff2afde560 sp=0x7fff2afde4d0 pc=0x55557e2bea58
runtime.(*unwinder).next(0x7fff2afde5b0)
goroutine 0 gp=0x40003af880 m=13 mp=0x40003ca008 [idle]:
PC=0x55557e2bea58 m=13 sigcode=1 addr=0x118
SIGSEGV: segmentation violation
            これで、いくつかの明確なパターンが観察されるようになりました。(*unwinder).nextでスタックを解除する際に、両方のエラーが発生します。あるケースでは、ランタイムがアンウィンドを完了できず、スタックが悪性状態にあることを識別し、意図的な重大なエラーが発生しました。別のケースでは、スタックを解約しようとしているときに、直接メモリアクセスエラーが発生しました。このセグメント違反は GitHub の問題で議論され、Go エンジニアはこれを、アンワインド時の Go スケジューラ構造体 m の逆参照であると特定しました。  
    
      Go Scheduler structsのレビュー
      
        
      
     
    Goは、軽量ユーザー空間スケジューラを使用して、同時実行性を管理します。多くのゴルーチンは、より少数のカーネルスレッドでスケジュールされます。これはしばしばM:Nスケジューリングと呼ばれます。どんなカーネルスレッドでも、個々のgoroutingをスケジュールすることができます。スケジューラには3つのコアタイプがあります。g(ゴルーチン)、m(カーネルスレッド「マシン」)、p(物理的実行コンテキスト「プロセッサ」)です。ゴルーチンをスケジュールするには、無料のmが無料のpを取得し、それがgを実行する必要があります。各gには、現在実行中の場合はmのフィールドが含まれ、それ以外の場合はnilになります。この記事に必要なコンテキストはこれだけですが、Goのランタイムドキュメントではさらに包括的に考察しています。
この時点で、何が起こっているかについて推論し始めることができます。プログラムがクラッシュするのは、無効なゴルーチンスタックを解除しようとするからです。最初のバックトレースにおいて、リターンアドレスがNULLの場合、スタックが完全にアンワインドされていないため、finishInternalを呼び出してアボートする。2番目のバックトレースのセグメンテーション違反のケースは、少し興味深いものです。代わりに、リターンアドレスがゼロ以外の場合、unwinderコードは、goroutineが現在実行中であると仮定します。次に、m.incgoにアクセスすることでmとaultを解除します(struct mとincgoのオフセットは0x118であり、メモリアクセスが故障しています)。 
では、何が原因で実現したのでしょうか?トレースから有用なものを得るのは困難でした。当社のサービスには、何千ものアクティブなガルーチンがあり、何百もあるのです。パニックが実際のバグから離れたものであることは最初から明らかでした。クラッシュはすべて、スタックを復元する際に観測されたものであり、これが問題だというなら、スタックがARM64上で復元されたときに、さらに多くのサービスで問題が発生するでしょう。スタックのアンウィンドが正しく行われていると確信していましたが、無効なスタック上にあると私たちは確信していました。
この時点で、私たちの調査は、推測を続けたり、推測をしたり、パニック発生率が上がったかどうか、あるいは何も変化していないかを推測しようとする中で、しばらく停滞してしまいました。GoのGitHubのIssue Trackerで既知の問題があり、それがほぼ当社の症状と一致するものの、彼らが議論した内容はほとんど当社がすでに把握していたものでした。ある時点で、リンクされたスタックトレースを調べてみたところ、そのクラッシュが私たちも使用していたライブラリの古いバージョン「Go Netlink」を参照していることに気づきました。
            goroutine 1267 gp=0x4002a8ea80 m=nil [runnable (scan)]:
runtime.asyncPreempt2()
        /usr/local/go/src/runtime/preempt.go:308 +0x3c fp=0x4004cec4c0 sp=0x4004cec4a0 pc=0x46353c
runtime.asyncPreempt()
        /usr/local/go/src/runtime/preempt_arm64.s:47 +0x9c fp=0x4004cec6b0 sp=0x4004cec4c0 pc=0x4a6a8c
github.com/vishvananda/netlink/nl.(*NetlinkSocket).Receive(0x14360300000000?)
        /go/pkg/mod/github.com/!data!dog/netlink@v1.0.1-0.20240223195320-c7a4f832a3d1/nl/nl_linux.go:803 +0x130 fp=0x4004cfc710 sp=0x4004cec6c0 pc=0xf95de0
            いくつかのスタックトレースをスポットチェックし、このNetlinkライブラリの存在を確認しました。ログを照会したところ、ライブラリを共有しただけでなく、当社が観測したセグメンテーションの不具合がすべて、 NetlinkSocket.Receiveをプリエンプトしている間に発生していたことがわかりました。
    
    Go(以前は1.13以下)の時代には、ランタイムは協同組合でスケジュール設定されていました。ゴルーチンは、スケジューラに収束する準備ができたと判断するまで実行されます。これは通常、runtime.Gosched()への明示的な呼び出しや、関数呼び出し/IO操作に離脱点が注入されているためです。Go 1.14以降は、ランタイムは代わりにasyncプリエンプションを行います。Goランタイムには、ゴルーチンの実行時間を追跡するスレッドsysmonがあり、(書き込み時点で)10ms以上実行されたものをプリエンプトします。これは、SIGURGをOSスレッドに送信することで行われます。シグナルハンドラはプログラムカウンターとスタックを変更し、asyncPreemptへの呼び出しを模倣します。
この時点で、私たちは2つの大きな理論がありました。
アップストリームで公表されている同じバグを見つけた後、私たちはこのバグがGoランタイムのバグによって引き起こされていると確信しました。しかし、両方の問題が同じ機能を示唆していることを知り、私たちはより懐疑的と感じました。特に、Go Netlinkライブラリは安全でないものを使用しているのです。では、その理由は不明でしたが、メモリ破損は妥当な説明かもしれませんでした。
コード監査が失敗した後に、私たちは壁に突き当たりました。クラッシュは稀で、根本原因からは程遠いものでした。これらのクラッシュは、ランタイムのバグによって引き起こされたかもしれませんし、Go Netlinkのバグによって引き起こされたかもしれません。明らかにコードのこの領域に何か問題があるように思われましたが、コード監査はうまくいきませんでした。 
    
    この時点で、私たちはクラッシュしているものが何かをよく理解していましたが、なぜそれが起きているのかについてはほとんど理解していませんでした。スタックアンウィンダーのクラッシュの根本的な原因が、実際のクラッシュからリモートであり、(*NetlinkSocket). receivedに関係していることは明らかですが、なぜでしょう?本番環境のクラッシュのコアダンプをキャプチャし、デバッガーで表示することができました。バックトレースは、私たちがすでに知っていたことを確認しました。つまり、スタックを解凍する際にセグメンテーションの欠陥があるということです。この問題の核心は、(*NetlinkSocket).Receiveを呼び出す中に先制されているゴルーチンを調べた時に明らかになりました。    
            (dlv) bt
0  0x0000555577579dec in runtime.asyncPreempt2
   at /usr/local/go/src/runtime/preempt.go:306
1  0x00005555775bc94c in runtime.asyncPreempt
   at /usr/local/go/src/runtime/preempt_arm64.s:47
2  0x0000555577cb2880 in github.com/vishvananda/netlink/nl.(*NetlinkSocket).Receive
   at
/vendor/github.com/vishvananda/netlink/nl/nl_linux.go:779
3  0x0000555577cb19a8 in github.com/vishvananda/netlink/nl.(*NetlinkRequest).Execute
   at 
/vendor/github.com/vishvananda/netlink/nl/nl_linux.go:532
4  0x0000555577551124 in runtime.heapSetType
   at /usr/local/go/src/runtime/mbitmap.go:714
5  0x0000555577551124 in runtime.heapSetType
   at /usr/local/go/src/runtime/mbitmap.go:714
...
(dlv) disass -a 0x555577cb2878 0x555577cb2888
TEXT github.com/vishvananda/netlink/nl.(*NetlinkSocket).Receive(SB) /vendor/github.com/vishvananda/netlink/nl/nl_linux.go
        nl_linux.go:779 0x555577cb2878  fdfb7fa9        LDP -8(RSP), (R29, R30)
        nl_linux.go:779 0x555577cb287c  ff430191        ADD $80, RSP, RSP
        nl_linux.go:779 0x555577cb2880  ff434091        ADD $(16<<12), RSP, RSP
        nl_linux.go:779 0x555577cb2884  c0035fd6        RET
            関数エピローチの2つのオプコード間で、ゴルーチンが一時停止されていました。スタックを解約するプロセスは、スタックフレームが一貫した状態にあることに依存しているため、スタックポインターを調整する途中で先回りしたのは即座に疑わしく感じました。ゴルーチンは、ADD $80, RSP, RSP と ADD $(16<<12), RSP, RSP の間、0x555577cb2880で一時停止されていました。
私たちは、この理論を確認するためにサービスログを照会しました。これは分離されたものではありません。スタックトレースの大半は、この同じオプコードがプリエンプトされていることを示していました。これはもう、再現不可能な本番環境のクラッシュではありませんでした。この2つのスタックポインタの調整の間、Goランタイムがプリエンプトした時にクラッシュが発生しました。Cloudflareには我々よりもユースケースが必要でした。
    
    この時点で、私たちは、これは実際には実行時のバグであり、依存関係なく、隔離された環境で再現できるはずだとかなり確信していました。この時点の理論は次のとおりです。
ガベージコレクションによってスタックのアンウィンドがトリガーされます
スプリットスタックポインター調整間の非同期プリエンプションによりクラッシュが発生
調整を分割する関数を作って、ループの中で呼び出すはどうなるでしょうか。
            package main
import (
	"runtime"
)
//go:noinline
func big_stack(val int) int {
	var big_buffer = make([]byte, 1 << 16)
	sum := 0
	// prevent the compiler from optimizing out the stack
	for i := 0; i < (1<<16); i++ {
		big_buffer[i] = byte(val)
	}
	for i := 0; i < (1<<16); i++ {
		sum ^= int(big_buffer[i])
	}
	return sum
}
func main() {
	go func() {
		for {
			runtime.GC()
		}
	}()
	for {
		_ = big_stack(1000)
	}
}
            この関数は、16ビットで表現できるよりも少し大きなスタックフレームになります。そのため、ARM64は、スタックポインタ調整を2つのオプコードに分割します。これらのオプコード間でランタイムがプリエンプトする場合、スタックアンウィンダーは無効なスタックポインターを読み取り、クラッシュします。
            ; epilogue for main.big_stack
ADD $8, RSP, R29
ADD $(16<<12), R29, R29
ADD $16, RSP, RSP
; preemption is problematic between these opcodes
ADD $(16<<12), RSP, RSP
RET
            これを数分間実行した後、プログラムは予想通りにパニックになりました!
            SIGSEGV: segmentation violation
PC=0x60598 m=8 sigcode=1 addr=0x118
goroutine 0 gp=0x400019c540 m=8 mp=0x4000198708 [idle]:
runtime.(*unwinder).next(0x400030fd10)
        /home/thea/sdk/go1.23.4/src/runtime/traceback.go:458 +0x188 fp=0x400030fcc0 sp=0x400030fc30 pc=0x60598
runtime.scanstack(0x40000021c0, 0x400002f750)
        /home/thea/sdk/go1.23.4/src/runtime/mgcmark.go:887 +0x290 
[...]
goroutine 1 gp=0x40000021c0 m=nil [runnable (scan)]:
runtime.asyncPreempt2()
        /home/thea/sdk/go1.23.4/src/runtime/preempt.go:308 +0x3c fp=0x40003bfcf0 sp=0x40003bfcd0 pc=0x400cc
runtime.asyncPreempt()
        /home/thea/sdk/go1.23.4/src/runtime/preempt_arm64.s:47 +0x9c fp=0x40003bfee0 sp=0x40003bfcf0 pc=0x75aec
main.big_stack(0x40003cff38?)
        /home/thea/dev/stack_corruption_reproducer/main.go:29 +0x94 fp=0x40003cff00 sp=0x40003bfef0 pc=0x77c04
Segmentation fault (core dumped)
real    1m29.165s
user    4m4.987s
sys     0m43.212s
            Standardライブラリのみで再現可能なクラッシュ?これは、私たちの問題がランタイムのバグであることを決定的に証明したように感じられました。
これは非常に特殊な再現ツールでした。バグとその修正について十分に理解したとしても、それでも理解できない動作もあります。これは1つの指示による競合状態であり、小さな変更が大きな影響を与えることは当然のことです。たとえば、この再現者は当初、Go 1.23.4で書かれ、テストされました。1.23.9(本番稼働中のバージョン)でコンパイルした時にクラッシュすることはありませんでした。バイナリをオブジェクトダンプして、分割ADDがまだ存在しているのを見ることができたのです!この挙動については明確な説明ができません。バグが存在しても、競合状態に遭遇する可能性に影響する未知の変数がいくつかあります。
    
    Arm64は、固定長の4バイトの命令セットアーキテクチャです。これはコード生成に多くの影響を与えますが、このバグに最も関連しているのは、直接の長さが制限されているという事実です。 addでは12-bitimmediadを取得し、movでは16-bitimmediadなどになります。演算子が合致しない場合、アーキテクチャはこれにどのように対処するのでしょうか。場合によってです。特に、ADDは「12バイトまでにシフトする」ためのビットを確保しているため、24ビットの加算は2つのオプコードに分解できます。他の指示も同様に分解され、単に最初にレジスタに即時読み込む必要があります。
マシンコードを生成する前のGoコンパイラーの最後のステップには、プログラムをobj.Prog structsに変換することが含まれます。これは非常に低レベルの中間表現(IR)であり、ほとんどの場合、マシンコードに変換されます。
            //https://github.com/golang/go/blob/fa2bb342d7b0024440d996c2d6d6778b7a5e0247/src/cmd/internal/obj/arm64/obj7.go#L856
// Pop stack frame.
// ADD $framesize, RSP, RSP
p = obj.Appendp(p, c.newprog)
p.As = AADD
p.From.Type = obj.TYPE_CONST
p.From.Offset = int64(c.autosize)
p.To.Type = obj.TYPE_REG
p.To.Reg = REGSP
p.Spadj = -c.autosize
            特に、このIRは直接の長さの制限を認識していません。その代わり、これはasm7.goで発生し、Goの内部中間表現がam64マシンコードに変換されます。アセンブラはビットサイズに基づいてコンクラスの即時を分類し、必要に応じて追加で指示を発行する際にそれを使用します。
Goアクセシビリティは、16ビットの即値に適合する一部のaddに対して(mov, add )オペコードの組み合わせを使用し、16ビット以上の即時にとって(add, add + lsl 12)オプコードを優先します。  
1<<15より少し大きいスタックを比較する:
            ; //go:noinline
; func big_stack() byte {
; 	var big_stack = make([]byte, 1<<15)
; 	return big_stack[0]
; }
MOVD $32776, R27
ADD R27, RSP, R29
MOVD $32784, R27
ADD R27, RSP, RSP
RET
            1<<16のスタックの場合:
            ; //go:noinline
; func big_stack() byte {
; 	var big_stack = make([]byte, 1<<16)
; 	return big_stack[0]
; } 
ADD $8, RSP, R29
ADD $(16<<12), R29, R29
ADD $16, RSP, RSP
ADD $(16<<12), RSP, RSP
RET
            より大きなスタックのケースでは、ADD x, RSP, RSP オプコードの間に、スタックポインタがスタックフレームの先端を指していないポイントがあります。私たちは当初、これはメモリの破損の問題だと考えました。非同期のプリエンプションを処理する際に、ランタイムが関数呼び出しをスタックにプッシュし、スタックの中央を破損させるのではないかと思いました。しかし、このゴルーチンはすでに関数エキサイティングにあり、私たちが破損したデータはすべて積極的に破棄されます。では、何が問題なのでしょうか?  
Goのランタイムは、スタックを解除する必要があることが多いです。それは、関数呼び出しのチェーンを逆方向にたどることを意味します。例えば、ガベージコレクションは、スタック上のライブリファレンスを見つけるために使用し、パニックは待機関数を評価するために使用し、スタックトレースを生成するにはコールスタックを表示する必要があります。これが機能するには、スタックポインターがアンウィンド時に正確でなければなりません。これは、golang dereferencesspが呼び出し関数を決定する方法だからです。スタックポインターが部分的に変更された場合、アンウィンダーはスタックの中央にある呼び出し関数を探します。基礎となるデータは、親スタックフレームへの指示として解釈されると意味がなく、その場合ランタイムがクラッシュする可能性が高くなります。
            //https://github.com/golang/go/blob/66536242fce34787230c42078a7bbd373ef8dcb0/src/runtime/traceback.go#L373
if innermost && frame.sp < frame.fp || frame.lr == 0 {
    lrPtr = frame.sp
    frame.lr = *(*uintptr)(unsafe.Pointer(lrPtr))
}
            非同期プリエンプションが発生すると、関数呼び出しをスタックにプッシュしますが、プリエンプションが発生したときspが部分的に調整されただけなので、親スタックフレームは正しくありません。クラッシュのフローは次のようになります:  
非同期プリエンプションは、xを追加する2つのオプコード間で発生し、rspは
ガートナーコレクションがスタックを解読する(Heapオブジェクトの有効性を確認するため)
Unwinderは、問題のあるゴルーチンのスタックを横断し始め、問題のある関数まで正しくアンロックされます。
Unwinder Dereference sp設定を用いて親関数を決定
ほぼ間違いなく sp の背後にあるデータは関数ではない
クラッシュ
          
          
          先ほど、(*NetlinkSocket).Receive で終了するスタックトレースの異常を確認しました。この場合は、親フレームを決定しようとしている間に、スタックを Cloudflareで仕組みを入手することができました。   
            goroutine 90 gp=0x40042cc000 m=nil [preempted (scan)]:
runtime.asyncPreempt2()
/usr/local/go/src/runtime/preempt.go:306 +0x2c fp=0x40060a25d0 sp=0x40060a25b0 pc=0x55557e299dec
runtime.asyncPreempt()
/usr/local/go/src/runtime/preempt_arm64.s:47 +0x9c fp=0x40060a27c0 sp=0x40060a25d0 pc=0x55557e2dc94c
github.com/vishvananda/netlink/nl.(*NetlinkSocket).Receive(0xff48ce6e060b2848?)
/vendor/github.com/vishvananda/netlink/nl/nl_linux.go:779 +0x130 fp=0x40060b2820 sp=0x40060a27d0 pc=0x55557e9d2880
            根本的な原因を特定した後は、それを再現ツールに報告し、バグはすぐに修正されました。このバグは、go1.23.12、go1.24.6、およびgo1.25.0で修正されています。以前は、goコンパイラーは単一のadd x, rsp命令を出力し、アセンブラに依存して、必要に応じて即時を複数のopcodeに分割しました。この変更の後、1<<12を超えるスタックは一時的なレジスタにオフセットを構築し、それを単一の不可分なオペコードでrspに加算します。ゴルーチンは、スタックポインタの変更前または変更後にプリエンプションすることができますが、変更中は決して先述できません。つまり、スタックポインターは常に有効であり、競合状態は発生しません。
            LDP -8(RSP), (R29, R30)
MOVD $32, R27
MOVK $(1<<16), R27
ADD R27, RSP, RSP
RET
            これはデバッグがとても楽しい問題でした。コンパイラーを正確に非難できるバグはあまり見られません。デバッグに数週間かかり、私たちは通常、人が考える必要のないGoランタイムの領域について学びなければなりませんでした。これは、稀な競合状態の良い例であり、大規模でしか定量化できないバグの一種です。
私たちは、このような捜査が好きな人を常に探しています。当社のエンジニアリングチームは募集を行っています。