2 poin oleh GN⁺ 2025-10-09 | 1 komentar | Bagikan ke WhatsApp
  • 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), dan p (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 incgo dari struktur m milik 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.Receive milik 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:
    1. Asynchronous preemption terjadi di antara dua instruksi ADD
    2. Rutinitas stack unwinding berjalan karena GC atau penyebab lain
    3. Sistem menelusuri posisi stack pointer yang tidak lazim dan salah menafsirkan alamat fungsi
    4. 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

 
GN⁺ 2025-10-09
Komentar Hacker News
  • Ini benar-benar terasa seperti penemuan yang hebat, dan begitu melihat kode assembly saya langsung mengikuti alur debugging-nya. Sebenarnya pendekatan ini tidak harus hanya bisa dilakukan di assembly, mungkin juga bisa di tahap IR, tetapi karena berbagai alasan tidak begitu dilakukan. Kemampuan membaca assembly ARM jelas menjadi keuntungan besar. Saya juga sempat mempertimbangkan cara seperti melakukan push atau pop ukuran stack untuk mengurangi jumlah instruksi, tetapi saya tidak yakin karena tidak tahu persis apa yang diperiksa GC. Ingin mendengar pendapat lain.
    • Biasanya digunakan pseudo-instruction ARM “LDR Rd, =expr”. Untuk konstanta yang tidak bisa dibuat secara langsung, konstanta diletakkan pada lokasi PC-relative lalu dimuat ke register berdasarkan PC. Dengan ini, proses “menambahkan konstanta ke SP” bisa diubah menjadi 2 instruksi eksekusi, dan membutuhkan total 12 byte: 8 byte kode dan 4 byte area data (untuk konstanta 17-bit). Dokumentasi terkait: penjelasan pseudo-instruction LDR
    • Agak mengejutkan bahwa assembler tidak menangani bug ini sebagai kasus khusus saat menambahkan immediate ke RSP. Jika patch hanya diterapkan di sisi compiler, mungkin masalah yang sama masih ada di bagian lain assembly aarch64.
    • Ekspresi aneh dengan tanda dolar dalam sintaks assembly ARM itu bukan assembly standar AArch64, dan menurut saya akan lebih baik jika artikelnya juga menyebutkan aturan bahwa “stack hanya boleh dipindahkan sekali”.
    • Runtime seperti Java atau .NET menempatkan safepoint secara eksplisit untuk mencegah konteks berganti di tengah rangkaian instruksi.
    • Sepertinya solusi yang benar adalah compiler memecah konstanta menjadi dua tahap untuk dimasukkan ke register lalu melakukan satu add agar penyesuaian SP tetap atomik. Memang jadi menambah satu instruksi, tetapi atomisitasnya terjamin. Alternatifnya, bisa juga dihitung dulu lewat register sementara lalu dipindahkan kembali.
  • Bagi yang sedang buru-buru, saya bagikan tautan commit perbaikannya: tautan commit golang/go
    • Setelah melihat issue-nya, saya jadi penasaran apakah tim Go memakai bot bahasa alami, atau hanya mengecek kata kunci “backport” di komentar. Komentar terkait: github issue comment
  • Secara teknis ini blog yang luar biasa bagus, penjelasannya sangat jelas sehingga mudah dipahami dan malah terasa bikin lebih pintar. Sudah lama saya tidak menyentuh assembly sejak x86 assembly, tetapi tetap mudah diikuti. Dan dengan tim seperti ini, saya jadi percaya mereka selalu punya kemampuan dan kendali kualitas untuk menyelesaikan isu seperti ini. Saya sempat mempertimbangkan Ampere Altra juga untuk ekspansi server, tetapi karena ruang cukup longgar akhirnya memakai Epyc.
  • Saya rasa akan lebih mudah menemukan bug seperti ini jika Go punya mode yang men-single-step semua instruksi dan memicu interupsi GC di setiap instruksi.
  • Saya penasaran ARM64 server dipakai untuk apa. Tahun lalu mereka bilang akan merilis server Gen 12 berbasis AMD EPYC, tetapi tidak ada penyebutan ARM64. Sekarang ternyata ARM64 dipakai di production.
    • Saya bukan karyawan Cloudflare, tetapi dari sering membaca blog mereka setahu saya, dengan mempertimbangkan secure boot dan sebagainya, mereka sudah men-deploy Ampere berdampingan dengan AMD sejak beberapa tahun lalu. Tujuan operasionalnya tampaknya untuk efisiensi edge, meski bisa saja ada penggunaan lain. Info lebih lanjut bisa dilihat di tulisan desain server edge, Ampere Altra vs AWS Graviton2, dan evaluasi ARM Qualcomm.
    • Saya ingat pernah ada pembahasan bahwa Cloudflare meng-host sebagian komputasi non-edge di public cloud, misalnya control plane, jadi mungkin saja begitu.
  • Saya tadinya mengira Cloudflare sekarang 100% hanya memakai Rust dan x86 (EPYC), jadi menarik mengetahui mereka juga menggunakan Go dan ARM.
  • Setiap tulisan blog Cloudflare menurut saya selalu menjadi konten hebat yang menangkap esensi engineering tanpa sihir infrastruktur atau ML. Suatu hari saya ingin melamar ke sana. Bug compiler ternyata lebih umum daripada yang dibayangkan (dulu saya menemukan beberapa tiap tahun di gcc), tetapi sering kali kasus langka seperti yang di artikel ini baru terlihat pada skala besar. Kebanyakan orang tidak pernah masuk ke skala sebesar itu.
    • Kenapa tidak melamar hari ini?
  • Perlu ditekankan bahwa stack pointer harus selalu disesuaikan secara atomik.
    • Orang yang menulis preemption kemungkinan membuat kodenya dengan asumsi x86 sebagai acuan—di sana instruksi bisa memuat konstanta sehingga berlangsung atomik—lalu saat di-port ke ARM terjadi pemecahan otomatis di level tinggi dan lahirlah bug seperti ini. Tidak sepenuhnya salah siapa pun, tetapi hasilnya memang buruk.
    • Itu juga langsung jadi pikiran saya.
  • Saya kurang paham bagaimana machine thread bisa berhenti di tengah dua instruksi. Apakah ini memang mungkin pada bare metal?
    • Go menggunakan interupsi untuk notifikasi GC.
    • Sinyal (signals)
  • Soal kalimat “ini masalah yang sangat menyenangkan”, memang pasti terasa melegakan setelah masalah fundamental seperti ini berhasil dipecahkan, tetapi selama belum terpecahkan rasanya pasti sama sekali tidak menyenangkan. Bug seperti ini menguras semua perhatian. Ada budaya bahwa orang hampir tidak pernah mengira standard library atau compiler yang bermasalah, sehingga developer terus-menerus hanya mencurigai kodenya sendiri. Saya juga pernah menemukan bug di standard library, dan bahwa masalahnya ada di sisi SDK adalah hal terakhir yang saya curigai. Akibatnya waktu habis di tempat yang salah. Apalagi kalau seperti kasus ini berupa race condition, reproduksinya sulit, jadi selalu terasa seperti sudah hilang lalu muncul lagi.
    • Komentar ini menambahkan pengalaman serupa dari dirinya sendiri, tetapi dengan memaksakan bantahan soal rasa senang penulis, kesannya justru mengurangi sentuhan emosionalnya. Tiap orang bisa merasa hal yang berbeda itu menyenangkan.
    • Ada orang yang justru senang menghadapi debugging sangat aneh yang bagi orang lain terasa menyiksa. Yang bagi seseorang frustrasi, bagi orang lain bisa jadi hiburan.
    • Mungkin yang sebenarnya ingin disampaikan penulis adalah bukan “funny”, melainkan “satisfying”. Saya juga pernah mengejar bug sscanf di 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.
    • Menyelesaikan cacat yang dalam itu ketika akhirnya beres terasa sangat melegakan. Saya juga sering merasakan kesenangan terbesar saat berhasil menyelesaikan bug di compiler atau CPU.
    • Di bahasa terkelola, kalau terjadi segfault tanpa sama sekali memakai hal-hal seperti Unsafe, saya biasanya menganggap itu sinyal bahwa kemungkinan besar masalahnya bukan di kode saya.