Proses Menemukan Bug di Kompiler Go untuk ARM64
(blog.cloudflare.com)- Cloudflare menemukan bug race condition langka pada kompiler Go yang berjalan di platform arm64 saat memantau trafik berskala besar
- Bug ini muncul dalam bentuk layanan yang tiba-tiba masuk ke kondisi panic saat proses stack unwinding, atau terjadi kesalahan akses memori
- Dalam proses pelacakan penyebabnya, dipastikan bahwa masalah terjadi antara asynchronous preemption (preemption paksa) di runtime Go dan dua instruksi penyesuaian stack pointer yang dihasilkan kompiler
- Melalui kode reproduksi minimal, mereka membuktikan bahwa bug ini adalah masalah pada runtime Go itu sendiri, dan mengungkap adanya race condition sebesar satu instruksi saat stack pointer berubah secara tidak lengkap
- Isu ini telah ditambal pada versi go1.23.12, go1.24.6, go1.25.0, dan pendekatan baru tersebut secara mendasar menutup race condition dengan menghindari manipulasi stack pointer yang tidak bisa diubah sekaligus
Analisis Bug Kompiler Go ARM64 yang Ditemukan di Cloudflare
Pusat data Cloudflare memproses 84 juta permintaan HTTP per detik di lebih dari 330 kota di seluruh dunia, dan lingkungan trafik sebesar ini memiliki karakteristik bahwa bahkan bug yang sangat langka pun dapat sering terekspos. Artikel ini menganalisis secara rinci masalah race condition yang terjadi pada kode hasil keluaran kompiler Go di platform arm64 beserta contoh kasus nyata.
Investigasi Gejala Panic yang Aneh
- Di dalam jaringan Cloudflare, ada layanan yang menerapkan pemrosesan trafik produk seperti Magic Transit dan Magic WAN ke kernel
- Pada mesin arm64, sistem pemantauan mendeteksi pesan fatal panic yang jarang tetapi terus berulang
- Hasil analisis awal menunjukkan adanya pelanggaran integritas saat proses stack unwinding terdeteksi (panic sering terjadi pada kode lama yang menggunakan pola panic/recover)
- Struktur panic/recover sempat dihapus sementara untuk menurunkan frekuensi panic, tetapi kemudian fatal panic yang mencurigakan justru semakin sering terjadi
- Karena itu, mereka menilai perlu analisis akar penyebab yang lebih mendalam, bukan sekadar pelacakan pola sederhana
Gambaran Struktur Data Runtime dan Scheduler Go
- Go mengadopsi struktur penjadwalan M:N dengan scheduler ringan di ruang pengguna (memetakan banyak goroutine ke sejumlah kecil thread kernel)
- Struktur inti scheduler berpusat pada
g (goroutine),m (mesin/thread kernel), danp (processor) - Kegagalan stack unwinding atau kesalahan akses memori dapat terjadi saat stack pointer atau return address berubah secara tidak normal
Penyebab Struktural Kesalahan Saat Stack Unwinding
- Dari berbagai analisis backtrace, semuanya terjadi dalam proses stack unwinding di fungsi
(*unwinder).next - Pada satu kasus, return address bernilai null sehingga dikenali sebagai stack tidak valid dan program dihentikan dengan kesalahan fatal; pada kasus lain, terjadi segmentation fault saat mengakses field
incgodari strukturmmilik scheduler Go di dalam stack frame - Crash terjadi cukup jauh dari titik kemunculan bug yang sebenarnya, sehingga pelacakan penyebabnya menjadi sulit
Pola yang Diamati dan Keterkaitannya dengan Library Go Netlink
- Setelah meninjau stack trace, mereka memastikan bahwa crash terkonsentrasi pada saat preemption terjadi di fungsi
NetlinkSocket.Receivemilik library Go Netlink - Dari sana, mereka membuat dua hipotesis
- Kemungkinan bug berasal dari penggunaan unsafe.Pointer di Go Netlink
- Kemungkinan bug berasal dari asynchronous preemption dan stack unwinding pada runtime Go itu sendiri
- Audit kode dilakukan, tetapi tidak ditemukan pola kerusakan memori secara langsung, sehingga diduga inti masalah berada pada runtime dan strategi pengelolaan stack
Asynchronous Preemption dan Race Condition
- Fitur asynchronous preemption yang diperkenalkan sejak Go 1.14 mengirim sinyal (SIGURG) ke thread OS untuk membuat titik penjadwalan secara paksa pada goroutine yang berjalan lama
- Jika preemption ini terjadi di antara dua instruksi assembly yang menyesuaikan pointer frame stack, stack pointer dapat tertahan di keadaan antara
- Saat stack di-unwind untuk garbage collection, penanganan panic, atau pembuatan stack trace, sistem dapat membaca lokasi yang salah dan menafsirkan alamat fungsi atau data secara keliru
Pembuatan Kode Reproduksi Minimal
- Dengan mengatur ukuran alokasi stack frame dan menulis fungsi yang secara eksplisit menyesuaikan stack (
big_stack) serta kode yang terus-menerus memanggil garbage collection, race condition berhasil direproduksi - Secara nyata, stack pointer disesuaikan oleh dua instruksi ADD di kode assembly, dan bila asynchronous preemption terjadi di antaranya, akan timbul crash selama proses stack unwinding
- Cacat ini dapat direproduksi hanya dengan kode pustaka standar murni, membuktikan bahwa ini adalah kelemahan sebesar satu instruksi yang melekat pada kode yang dihasilkan kompiler Go
Penyebab Jendela Race di Level Kompiler ARM64
- Karena arsitektur ARM64 memiliki panjang instruksi tetap dan batas immediate value, penyesuaian stack pointer bisa memerlukan dua instruksi atau lebih
- Pada representasi menengah internal (IR) Go, panjang immediate value seperti ini tidak diketahui, dan instruksi terbelah baru disisipkan saat konversi ke machine code yang sebenarnya
- Akibatnya, dua instruksi dipakai untuk pengembalian stack frame (
ADD RSP, RSP), sehingga tercipta jendela satu instruksi yang rentan terhadap preemption - Unwinder sangat bergantung pada keakuratan stack pointer, dan jika eksekusi berhenti di tengah instruksi-instruksi itu, interpretasi nilai bisa salah dan memicu kegagalan fatal
- Alur crash yang sebenarnya tersusun sebagai berikut:
- Asynchronous preemption terjadi di antara dua instruksi ADD
- Rutinitas stack unwinding berjalan karena GC atau penyebab lain
- Sistem menelusuri posisi stack pointer yang tidak lazim dan salah menafsirkan alamat fungsi
- Runtime crash
Perbaikan Bug dan Penyempurnaan Mendasar
- Tim Cloudflare melaporkan masalah ini ke repositori resmi Go berdasarkan kode reproduksi minimal dan analisis detail mereka, lalu isu tersebut segera ditambal dan dirilis
- Pada versi setelah go1.23.12, go1.24.6, go1.25.0, seluruh offset lebih dulu dihitung di register sementara, lalu stack pointer diubah dengan satu instruksi, sehingga kerentanan terhadap preemption dihilangkan
- Kini stack pointer selalu dijamin berada dalam keadaan valid, sehingga race condition diblokir secara struktural
LDP -8(RSP), (R29, R30)
MOVD $32, R27
MOVK $(1<<16), R27
ADD R27, RSP, RSP
RET
Kesimpulan dan Implikasi
- Bug ini adalah contoh benturan tak terduga antara code generation kompiler pada arsitektur tertentu dan manajemen konkurensi (asynchronous preemption)
- Ini menjadi kasus menarik karena race condition level instruksi yang sangat langka dan biasanya hanya muncul di lingkungan berskala besar berhasil dilacak melalui data nyata dan penalaran ilmiah
- Jika Anda mengoperasikan layanan berbasis lingkungan Go terbaru dan arsitektur ARM64, penting untuk melakukan upgrade ke versi Go terkait
1 komentar
Komentar Hacker News
addagar penyesuaian SP tetap atomik. Memang jadi menambah satu instruksi, tetapi atomisitasnya terjamin. Alternatifnya, bisa juga dihitung dulu lewat register sementara lalu dipindahkan kembali.signals)sscanfdi toolchain Ubuntu GCC ARM sambil dikejar deadline; saat itu tidak menyenangkan, tetapi setelah masalahnya teridentifikasi dengan tepat dan regression test-nya ditulis, rasanya sangat memuaskan.