7 poin oleh GN⁺ 2025-07-25 | 1 komentar | Bagikan ke WhatsApp
  • Keamanan memori dan keamanan thread bukanlah konsep yang bisa dipisahkan, dan tanpa keamanan thread, tidak mungkin mencapai keamanan memori yang sesungguhnya
  • Dalam kasus bahasa yang tidak thread-safe seperti Go, masalah thread saja sudah cukup untuk merusak keamanan memori
  • Beberapa bahasa seperti Java mengamankan keselamatan pada tingkat bahasa dengan menangani bahkan data race sebagai perilaku yang terdefinisi melalui model memori konkurensi
  • Go rentan terhadap data race, dan ada contoh nyata pelanggaran keamanan memori yang benar-benar terjadi
  • Sifat yang benar-benar penting untuk diperhatikan adalah tidak adanya Undefined Behavior (perilaku tak terdefinisi)

Tanpa keamanan thread, keamanan memori tidak bisa dijamin

Kebingungan konsep: keamanan memori vs keamanan thread

  • Belakangan ini keamanan memori mendapat sorotan besar, tetapi definisi tepatnya sering kali tidak jelas
  • Secara tradisional, keamanan memori merujuk pada bahasa yang mencegah akses memori use-after-free atau out-of-bounds
  • Sementara itu, keamanan thread berarti program bebas dari bug konkurensi, dan kedua konsep ini sering diperlakukan terpisah
  • Penulis berpendapat bahwa pemisahan ini pada praktiknya tidak berguna, dan menekankan bahwa yang sebenarnya kita inginkan adalah tidak adanya Undefined Behavior (UB)

Pelanggaran keamanan memori akibat data race: contoh Go

  • Untuk menunjukkan masalah dari memperlakukan keamanan memori dan keamanan thread secara terpisah, ditunjukkan contoh dalam bahasa Go
  • Go diklasifikasikan sebagai bahasa yang aman terhadap memori, tetapi pada program seperti berikut, kesalahan memori bisa terjadi hanya karena data race
Mengubah globalVar berulang kali ke nilai bertipe berbeda (Int, Ptr) sambil secara bersamaan membacanya dari goroutine lain dan memanggil metodenya
  • Karena dua thread saling tumpang tindih saat memperbarui dua pointer internal globalVar (data, vtable) secara terpisah, pembacaan di tengah proses dapat menghasilkan keadaan campuran yang memicu akses memori yang salah
  • Akibatnya, program mencoba merujuk alamat yang keliru (misalnya 0x2a; heksadesimal 42) lalu berhenti dengan error
  • Fenomena ini juga serupa pada interface, slice, dan struktur lain di Go, karena beberapa field tidak diperbarui secara atomik

Cara bahasa lain menangani konkurensi dan keamanan memori

  • Bahasa lain seperti Java juga bisa mengalami data race, tetapi mereka menerapkan model memori konkurensi yang terdefinisi sehingga program tidak merusak bahasa itu sendiri
    • Contoh: Java merancang model memorinya secara cermat agar bahkan di lingkungan multithread, program tidak jatuh ke error runtime seperti segment fault paksa
  • Kebanyakan bahasa mengendalikan masalah konkurensi dengan salah satu dari dua pendekatan berikut
    • Mendefinisikan model memori agar semua program konkurensi tetap memiliki perilaku yang konsisten (dengan konsekuensi pembatasan optimisasi compiler dan beban implementasi yang lebih besar)
      • Java, C#, OCaml, JavaScript, WebAssembly, dan lain-lain
    • Melarang sebagian besar data race melalui sistem tipe yang kuat, dan hanya menangani sedikit pengecualian secara aman (Rust, strict concurrency di Swift)
  • Go tidak mengikuti kedua opsi tersebut
    • Keamanan memori hanya dijamin jika tidak ada data race
    • Ada alat pendeteksi data race, tetapi pada program nyata ada batasan untuk memverifikasi semua situasi hanya lewat pengujian
    • Hasil riset dan pengalaman lapangan telah melaporkan banyak kasus nyata pelanggaran keamanan memori

Model memori Go dan masalah dokumentasi

  • Dokumen resmi model memori Go memang menyebut bahwa kebanyakan race menghasilkan akibat yang terbatas, tetapi tidak menjelaskan dengan tegas bahwa beberapa data race dapat memiliki hasil yang tak terbatas
  • Ada klaim bahwa perilakunya mirip Java/JavaScript, tetapi dua bahasa itu menginvestasikan jauh lebih banyak upaya dibanding Go untuk menjamin keamanan konkurensi
  • Hanya pada beberapa bagian detail dokumen disebutkan secara terbatas bahwa sebagian data race dapat memicu perilaku yang sepenuhnya tak terdefinisi

Kesimpulan: tidak adanya Undefined Behavior (UB) adalah tujuan yang sebenarnya

  • Pada praktiknya, sifat yang benar-benar diinginkan pengguna adalah program tidak merusak bahasa itu sendiri (tidak ada UB)
  • Berbagai kerentanan keamanan yang muncul akibat pelanggaran keamanan memori terjadi karena UB benar-benar terjadi
  • Begitu UB terjadi, semua perilaku setelahnya menjadi tak dapat diprediksi, dan penyerang bisa memanfaatkannya
  • Perbedaan mendasar antara bahasa yang 'aman' dan 'tidak aman' adalah apakah UB bisa terjadi
  • Daripada memisah-misahkan keamanan memori, keamanan thread, keamanan tipe, dan sebagainya, yang paling penting adalah apakah UB dapat terjadi
  • Dalam kenyataannya, keamanan juga memiliki spektrum; Go lebih aman daripada C, tetapi tidak menjamin keamanan yang sepenuhnya utuh
  • Berdasarkan data, sangat sulit untuk 'membuktikan' keamanan nyata Go, dan penting untuk benar-benar memahami konsekuensi yang tidak intuitif dari pilihan desain tiap bahasa

1 komentar

 
GN⁺ 2025-07-25
Komentar Hacker News
  • Ini kejadian yang pernah ada di tim Dropbox saya: menulis ke struktur data di server Go tanpa sinkronisasi, lalu engineer baru yang masuk berulang kali memicu segfault, semacam ritual perkenalan
    Swift juga punya masalah yang sama, dan saya pernah menulis program yang menunjukkan bahwa Swift bisa dengan sangat mudah memicu segfault saat mengakses struktur data bersama
    Mengatakan Go itu memory-safe seperti Rust atau Java agak berlebihan
  • Swift sedang berupaya menyelesaikan masalah ini, tetapi di dunia nyata sudah banyak kode tidak aman yang telanjur ada, jadi perubahannya sangat lambat dan menyakitkan
  • Saya penasaran, biasanya spesifikasi Go juga cukup jelas menyebutkan bahwa struktur dasar seperti map tidak thread-safe, jadi harus hati-hati saat memodifikasinya
    Saya ingin mendengar detail tentang situasi masalah yang terjadi di Dropbox
  • Saya ingin menekankan bahwa “memory safety dalam arti Rust atau Java” yang dibicarakan di sini bukan definisi istilah yang ketat
    Memory safety, alih-alih konsep PLT (teori bahasa pemrograman), lebih merupakan istilah keamanan perangkat lunak
    Pada akhirnya para programmer Go juga sangat memahami perbedaan ini, sehingga Go menjadikan pendekatan seperti “jangan berkomunikasi lewat berbagi, bagilah lewat komunikasi” sebagai premis dasarnya
    Tentu saja di kenyataan konsep ini tidak sepenuhnya terwujud, dan semua orang paham bahwa Go modern juga banyak memakai shared state dan memerlukan sinkronisasi
  • Untuk mendapatkan perspektif, saya ingin bertanya pada diri sendiri seberapa banyak kasus turunan di Go yang benar-benar tidak memory-safe, atau seberapa besar kemungkinan program Go benar-benar tidak memory-safe
  • Java juga tidak memory-safe sampai pada tingkat makna yang dimiliki Rust
  • Isu ini sering muncul berulang, agak mirip dengan masalah soundness hole di Rust; ini sama sekali bukan masalah remeh, tetapi kemungkinan bertemunya secara kebetulan cukup rendah
    Dalam praktiknya, setelah menjalankan Go selama bertahun-tahun, saya rasa hampir tidak pernah ada bug semacam ini yang benar-benar terjadi
    Uber pernah merangkum secara detail bug yang terjadi di kode Go mereka, dan tulisan ini merapikan dalam tabel seberapa sering masalah itu benar-benar terjadi
    Di Go, kebanyakan masalah akses concurrent ke map atau slice terjadi pada slice yang sama, dan perlu ada fenomena “torn read”, jadi dalam praktiknya tidak umum
    Meski begitu, alasan orang biasanya berhasil menghindari masalah seperti ini mungkin karena mereka cukup berhati-hati dan sangat sadar akan bahaya melakukan reassignment variabel dalam situasi akses concurrent
    Bahasa ini sendiri punya atomics, channel, dan mutex, jadi dalam praktiknya kasus salah pakai dalam situasi concurrent jarang terjadi, dan ada juga race detector sehingga masalah seperti ini cepat ditemukan
    Walau ada penurunan performa, saya menganggap masalah torn read hanyalah isu yang bisa diperbaiki, dan di kode Go produksi ini bukan masalah besar
    Video terkait
  • Saya pernah mengalami bug data race di Go yang butuh waktu berbulan-bulan untuk ditemukan
    Race detector juga tidak menemukan apa pun, dan tak seorang pun paham apa yang sedang terjadi
    Ternyata loop counter mengalami overflow, sehingga perhitungan yang sama diulang sangat banyak, dan request yang kadang seharusnya 100ms malah memakan waktu 3 menit
    Di production saya mengetahui masalah ini secara tidak langsung lewat perf, dan sebagai platform engineer, pengalaman debugging saya sangat membantu tim
    Karena begitu sering terpapar berbagai situasi race di Go, secara pribadi saya berharap Rust diadopsi di mana-mana
  • Para maintainer Rust juga mengakui soundness hole sebagai bug
    Misalnya issue ini membutuhkan refactor besar pada compiler sehingga lama terselesaikan
  • Uber mengatakan program Go “mengekspos concurrency 8 kali lebih banyak” dibanding microservice Java, dan saya penasaran apa maksud penggunaan kata concurrency di sini seolah-olah sebagai kata benda yang bisa dihitung
  • Zig juga mengklaim memory safety, tetapi tidak punya konsep seperti tipe Send/Sync di Rust
    Dalam praktiknya, sampai sekarang kode Zig concurrent masih sedikit sehingga masalahnya belum benar-benar mencuat, tetapi saya pikir ketika fitur async makin luas dipakai, banyak masalah bisa meledak sekaligus
  • Bahkan program Zig single-thread yang dibangun dengan ReleaseSafe pun, misalnya saat melakukan dereference pointer yang masa hidup variabel lokalnya sudah berakhir, tidak sepenuhnya bebas dari risiko memory corruption di semua mode optimisasi
  • Klaim memory safety pada Zig nyaris seperti lelucon
    Tentu bug-nya lebih sedikit daripada C, tetapi itu juga berlaku untuk C++, dan tidak ada yang menyebut C++ memory-safe
  • Dalam kode nyata, selama tidak dirancang secara jahat, saya belum pernah melihat kode Go yang memiliki kerentanan karena data race
    Tentu ini bukan berarti risikonya benar-benar nol, tetapi mengisyaratkan bahwa dari sisi keamanan aplikasi Go, ini kemungkinan bukan isu prioritas
    Sebaliknya, pada kode C/C++, 60~75% kerentanan nyata berasal dari masalah memory safety
    Memory safety juga berada pada sebuah spektrum, dan saya pikir setelah tingkat tertentu manfaat tambahannya mulai menurun
  • Saya memang pernah melihat kode Go yang rentan karena data race
  • Saat ini saya merasa penderitaan maintenance jauh lebih besar daripada CVE
    Bug yang tidak bisa dieksploitasi pun tetap bug yang pada akhirnya harus diperbaiki
    Karena waktu yang dihabiskan untuk maintenance jauh lebih besar daripada pengembangan awal, kalau maintenance bisa dikurangi maka meskipun peluncuran awal sedikit tertunda, saya rasa itu tetap sepadan
  • Alasan memory safety penting adalah karena sebagian besar CVE pada program C berasal dari bug memory safety
    Sebaliknya, di Go, thread safety bukan penyebab utama CVE
    Secara teoretis ada dasarnya, tetapi dalam kenyataan tidak terlalu menonjol
  • Yang penting adalah apa yang bisa dilakukan di dalam thread
    Saat memori dibagi, jika struktur data dirusak maka thread lain bisa berperilaku tidak aman atau salah
    Misalnya ketika satu thread mengubah ukuran vector sementara thread lain mengaksesnya, operasi yang aman dalam eksekusi berurutan menjadi berbahaya dalam concurrency
    Go juga tidak bisa bebas dari hal ini
  • Isu memory safety yang khas di C punya kemungkinan tinggi berujung pada RCE (remote code execution)
    Sebaliknya, jika isu thread safety hanya berakhir sebagai segfault, itu mungkin cuma serangan DoS (denial of service) biasa
    Race condition bisa berujung pada serangan yang lebih kuat, tetapi jauh lebih sulit dipicu
  • Meski CVE lebih fatal, data corruption/crash akibat bug threading tetaplah bug yang pada akhirnya harus ditriage, dianalisis, dan diperbaiki oleh seseorang
  • Realitas yang menyedihkan adalah kebanyakan bahasa yang memakai thread memberikan global variable dan akses shared memory tanpa batas sebagai default
    Inilah penyebab utama data corruption dan race
    Dalam berbagai situasi, model berbasis proses lebih baik daripada thread untuk concurrency, tetapi kelemahannya terlalu berat
    Kalau sejak awal default-nya adalah mengirim semua data yang diperlukan tiap thread lewat message passing, saya rasa sebagian besar masalah ini akan hilang
    Bagaimanapun, di platform ini kita punya kebebasan memakai global variable dan shared memory, jadi tinggal jangan dipakai
  • Rust adalah contoh bahasa modern yang bisa menanamkan thread safety ke dalam sistem tipenya
    Tujuan awal Rust bukan sistem language yang memory-safe, melainkan sistem language yang thread-safe, dan memory safety datang sebagai konsekuensi alaminya
    Di Rust, structured concurrency bisa dipakai lewat thread::scope dan sejenisnya, sehingga pekerjaan terkait thread sangat nyaman
  • Message passing juga bisa lebih banyak memicu masalah logis (race condition/deadlock dan sebagainya) daripada berbagi memori, jadi itu bukan solusi universal
  • Di Go, ada kecenderungan lebih menekankan komunikasi antar-goroutine (channel dan sebagainya) daripada berbagi memori secara langsung
    Lihat dokumen ini
  • Meski objek dikirim antar-goroutine lewat channel, Go tidak punya konsep seperti tipe sendable, ownership, atau referensi read-only, sehingga tidak mudah menggunakannya dengan aman
    Contoh nyata:
    func processData(lines <-chan []byte) {
     for line := range lines {
      fmt.Printf("processing line: %v\n", line)
     }
    }
    
    func main() {
     lines := make(chan []byte)
     go processData(lines)
    
     var buf bytes.Buffer
     for range 3 {
      buf.WriteString("mock data, assume this got read into the buffer from a file or something")
      lines <- buf.Bytes()
      buf.Reset()
     }
    }
    
    Pada kode di atas, buf.Bytes() mengoper referensi langsung ke memori internal, dan dengan pemanggilan Reset(), backing memory digunakan ulang sehingga processData dan main sama-sama mengakses memori yang sama secara bersamaan dan terjadilah data race
    Di Rust, kode seperti ini bahkan tidak akan lolos kompilasi karena merupakan dua mutable reference, dan akan dipaksa memindahkan ownership atau melakukan copy
    Di Go ini mudah membingungkan; bytes.Buffer.ReadBytes("\n") atau .String() mengembalikan salinan sehingga aman, tetapi .Bytes() berbahaya seperti contoh di atas
    Channel di Rust secara mendasar mencegah masalah ini lewat konsep ownership/transfer, tetapi Go tidak punya pengaman seperti itu
    Akibatnya, ini terasa lebih lambat daripada mutex, dan bagi pemula Go justru memberi pengalaman yang lebih sulit untuk digunakan dengan benar
  • Dalam program golang nyata, pola “berkomunikasi lewat berbagi” justru menimbulkan banyak masalah logis, dan pada akhirnya berbagi memori menjadi hal yang umum
    Artinya, race yang “aman” atau deadlock yang “aman” justru jadi lebih sering
  • Diskusi soal bug concurrency sering cenderung mengabaikan fakta bahwa pada kebanyakan aplikasi, sebagian besar bug yang benar-benar penting justru muncul karena lock, transaction, atau isolation transaksi di dalam DB diterapkan secara salah
    Dalam teori PL, pendekatan race freedom ala Rust mungkin menarik, tetapi dalam aplikasi nyata data penting bagaimanapun ada di RDBMS, dan misalnya kalau tidak memakai FOR UPDATE pada SELECT, race tetap bisa terjadi kapan saja
    Meski aplikasi Rust sama sekali tidak memakai unsafe, race tetap ada tergantung DB-nya
  • Istilah “memory safety” awalnya muncul untuk menjelaskan konsep yang rumit, tetapi seiring waktu maknanya meluas atau menyempit
    Kita tahu Go dirancang untuk hampir tidak mengizinkan bug memory corruption, terlihat dari tidak adanya eksploit nyata
    Jika mengikuti argumen tulisan ini, maka kebanyakan bahasa level tinggi (dalam tulisan ini Java saja yang dikecualikan) juga jadi tidak memory-safe
    Rust mungkin “lebih” aman daripada Go, tetapi “memory safety” bukanlah spektrum kontinu, melainkan konsep lulus/gagal
    Jika ingin mengklaim sebuah bahasa memory-unsafe, POC wajib ditunjukkan
  • Jika bagian penting dari istilah memory safety adalah “type confusion”, maka Go juga bukan pengecualian
    Contoh dalam tulisan menunjukkan bahwa memory corruption bisa dengan mudah terjadi karena int keliru dianggap sebagai pointer
    Dalam demo sengaja dipakai 42 sehingga muncul segfault, tetapi jika yang dipakai adalah alamat nyata, corruption sungguhan akan terjadi
  • Data race membuat program bisa masuk ke keadaan yang tidak dikenali spesifikasi bahasa (misalnya dipaksa berhenti dengan SIGSEGV), sehingga itu merupakan pelanggaran memory safety
    Karena itu, bahasa yang memungkinkan terjadinya data race tidak bisa disebut memory-safe
  • Seperti contoh dalam tulisan, torn read pada fat pointer akibat type confusion, atau out-of-bounds write akibat torn read pada slice, memang bisa direalisasikan
    Sulit rasanya menyebut kasus seperti ini sebagai memory-safe
  • Istilah berkembang dan berganti makna adalah hal yang sering terjadi juga di matematika dan fisika
    Untuk menghindari masalah seperti ini, kadang dipakai nama orang seperti “Gaussian Curvature” atau “Riemann Integrals”
    Ada juga kasus “makna awal tetap dipertahankan secara sempit, lalu diperluas ke makna yang lebih luas” seperti pada “Galois Group”
    Demikian pula memory safety bukan pengecualian
  • Dengan definisi penulis, saya ingin tahu dasar menyebut Java tidak memory-safe
    Saya meminta contoh konkretnya
  • Bahkan Go sendiri secara resmi juga tidak jelas dalam mendefinisikan memory safety
    Di FAQ dan sebagainya ada penyebutan memory safety atau jawaban terkait unions yang mengisyaratkan bahwa Go memory-safe, tetapi maknanya sebenarnya tidak jelas
    Dalam presentasi tahun 2012 oleh Rob Pike, ia mengatakan “Not purely memory safe”, tetapi bahkan arti 'purely' pun tidak didefinisikan
    Dokumentasi race detector Go juga tidak jelas soal definisi “safe” (contoh dokumen)
    Dari luar, justru sering ada klaim kuat yang menyebut Go sebagai “memory-safe programming language”
    Contohnya dokumen keamanan milik fly.io, di sini, atau dokumen memorysafety.org yang mengklasifikasikan Go sebagai memory safe
    Namun di dokumen yang sama, “Out of Bounds Reads and Writes” juga dijelaskan sebagai masalah memory safety, padahal error Go yang ditunjukkan dalam pos tersebut termasuk dalam kondisi itu
    Setidaknya, saya rasa Go dan komunitasnya perlu memperjelas makna persis dari “memory safety”
    Selama kasus seperti ini ada, lebih baik tidak menyebut Go sebagai bahasa memory-safe tanpa penjelasan
  • Definisi memory safety juga sedikit berubah mengikuti zaman
    Pada masa Go dibuat, pandangan dominan adalah “kalau ada garbage collector berarti memory-safe”, dan dibanding C/C++, itu memang jauh lebih aman