Artikel mendalam tentang teknologi yang membentuk masa depan.

Spinlock eBPF dan Seni Debugging Kernel

Bagaimana bug spinlock eBPF muncul di kernel Linux, mengapa sulit dilacak, dan apa yang diungkap proses debugging tentang model keamanan eBPF.

Gembok bercahaya berputar dan memercikkan bunga api di tengah lanskap papan sirkuit yang membeku

Bug spinlock di eBPF tidak memberi tahu kamu lewat stack trace atau pesan error. Ia menunjukkan gejalanya lewat mesin yang berhenti merespons. Tidak ada log, tidak ada peringatan, tidak ada degradasi yang anggun, hanya satu core CPU yang berputar selamanya menunggu lock yang tidak akan pernah dilepas. Kalau beruntung, watchdog timer menyala dan kamu mendapat kernel panic beserta trace-nya. Kalau sial, mesinnya cuma beku dan kamu harus mematikan lalu menyalakannya lagi secara paksa.

eBPF seharusnya aman. Ia berjalan di dalam kernel, tapi melewati verifier yang memastikan program berhenti, tidak mengakses memori yang tidak valid, dan tidak merusak state kernel. Jadi bagaimana bug spinlock bisa lolos? Karena verifier memeriksa program satu per satu, sedangkan bug spinlock muncul dari interaksi antara program, map, dan keputusan penjadwalan kernel, hal-hal yang tidak bisa diprediksi sepenuhnya oleh verifier statis.

Untuk Apa Spinlock di eBPF

Program eBPF berjalan dalam konteks kernel, sering kali di banyak CPU sekaligus. Ketika dua program eBPF perlu memperbarui nilai map yang sama, entah itu counter, struktur data, atau state machine, mereka butuh sinkronisasi. eBPF menyediakan bpf_spin_lock dan bpf_spin_unlock untuk keperluan ini.

Spinlock adalah lock paling sederhana: coba ambil, dan kalau sedang dipegang, berputar dalam loop ketat sampai dilepas. Tanpa tidur, tanpa menyerahkan CPU, tanpa antrean. Ini cocok untuk eBPF karena program eBPF berjalan di konteks yang dilarang tidur, seperti interrupt handler, softirq, dan bagian dengan preemption dinonaktifkan. Mutex yang membuat pemanggilnya tidur bisa membuat kernel crash. Spinlock cuma membakar siklus CPU sampai lock tersedia.

struct map_value {
struct bpf_spin_lock lock;
__u64 counter;
__u32 last_pid;
};
SEC("tp/sched/sched_switch")
int handle_sched_switch(struct trace_event_raw_sched_switch *ctx) {
__u32 key = 0;
struct map_value *val;
val = bpf_map_lookup_elem(&my_map, &key);
if (!val)
return 0;
bpf_spin_lock(&val->lock);
val->counter++;
val->last_pid = ctx->next_pid;
bpf_spin_unlock(&val->lock);
return 0;
}

Kelihatannya sederhana. Dan untuk kasus sederhana seperti ini, yaitu kunci, ubah nilai, lalu lepas, memang berjalan dengan baik. Masalah mulai muncul ketika pola locking makin rumit, atau ketika interaksi antara program eBPF dan lock milik kernel menciptakan dependensi yang tidak terduga.

Bagaimana Bug Spinlock Muncul

Bug spinlock yang paling klasik adalah deadlock: program A memegang lock 1 dan mencoba mengambil lock 2, sementara program B memegang lock 2 dan mencoba mengambil lock 1. Keduanya berputar selamanya menunggu satu sama lain. Dalam konteks kernel, artinya core CPU tersebut tamat dan tidak akan pernah menjalankan apa pun lagi.

Verifier eBPF mencegah sebagian kasus ini. Ia menolak program yang memegang lebih dari satu spinlock sekaligus, sehingga deadlock AB-BA klasik bisa dihindari. Tapi ia tidak bisa mencegah semua skenario deadlock, karena sebagian muncul dari hubungan antara spinlock eBPF dan lock milik kernel sendiri.

Bayangkan skenario ini: program eBPF yang terpasang di tracepoint mengambil spinlock pada sebuah map. Saat lock sedang dipegang, interrupt perangkat keras datang di CPU yang sama. Interrupt handler menjalankan program eBPF lain yang mencoba mengambil spinlock yang sama. Terjadi deadlock. Interrupt tidak bisa selesai sebelum mendapatkan lock, tapi lock tidak bisa dilepas sampai program yang terinterupsi berjalan lagi, dan itu tidak mungkin terjadi sebelum interrupt selesai.

Deadlock scenario (same CPU):
CPU 0 running eBPF program A
→ acquires spinlock on map_value
→ hardware interrupt fires
→ CPU 0 runs interrupt handler
→ interrupt handler runs eBPF program B
→ program B tries to acquire same spinlock
→ spinlock is held by program A
→ program B spins... forever
→ interrupt handler never returns
→ program A never resumes
→ program A never releases lock
→ CPU 0 is permanently stuck

Lock spinlock milik kernel menangani ini dengan menonaktifkan interrupt selama lock dipegang (spin_lock_irqsave). Spinlock eBPF juga melakukan hal serupa: bpf_spin_lock menonaktifkan preemption dan, pada kernel 5.1+, menonaktifkan softirq. Tapi tidak semua konteks interrupt tercakup, dan tidak semua versi kernel menanganinya dengan cara yang sama. Di sinilah bug-bug halus bersarang.

Masalah Debugging

Debugging masalah spinlock di eBPF itu sulit, dengan alasan-alasan spesifik.

Reproduksinya tidak deterministik. Bug spinlock bergantung pada timing: CPU mana yang menjalankan program mana, kapan interrupt datang, dan berapa lama critical section berjalan. Tes yang lolos 999 kali bisa saja deadlock di percobaan ke-1000. Kamu tidak bisa mereproduksinya dengan andal di lingkungan development karena topologi CPU, frekuensi interrupt, dan pola beban di mesin development berbeda dengan produksi.

Mode kegagalannya menghancurkan bukti. Saat deadlock spinlock terjadi, CPU yang terdampak berhenti menjalankan apa pun, termasuk infrastruktur logging dan monitoring yang seharusnya memberi tahu apa yang terjadi. Jika deadlock hanya di satu CPU, mesin mungkin masih pincang tapi tetap hidup. Jika beberapa CPU deadlock, atau CPU yang terkunci memegang resource yang dibutuhkan CPU lain, seluruh mesin akan hang.

Tool debugging tradisional tidak banyak membantu. Kamu tidak bisa begitu saja memasang debugger ke kernel untuk memeriksa kondisi deadlock (well, bisa dengan KGDB, tapi koneksi serial harus sudah disiapkan sebelum deadlock terjadi). dmesg tidak berguna karena tidak ada yang menulis ke sana. Satu-satunya informasi yang andal biasanya berasal dari output lockup detector, itu pun kalau ia sempat menyala sebelum mesin benar-benar mati.

Strategi yang Benar-Benar Berhasil

Meski begitu, orang tetap bisa menemukan dan memperbaiki bug spinlock eBPF. Ini caranya.

Analisis urutan lock. Sebelum menjalankan apa pun, analisis perilaku locking setiap program eBPF. Map mana yang diakses tiap program? Lock mana yang diambil? Apakah dua program yang menyentuh lock yang sama bisa berjalan di CPU yang sama, misalnya tracepoint yang sama atau tracepoint vs interrupt? Ini analisis manual yang membosankan, tapi mampu menangkap pola deadlock paling umum sebelum terjadi.

Pantau waktu tahan lock. Instrumentasikan program eBPF untuk mengukur berapa lama spinlock dipegang. Spinlock yang dipegang lebih dari beberapa mikrodetik adalah tanda bahaya, karena ia memperlebar celah untuk deadlock akibat interrupt dan membuat CPU lain membuang siklus untuk berputar. Critical section yang pendek itu lebih cepat sekaligus lebih aman.

// Monitoring lock hold time in eBPF
SEC("tp/sched/sched_switch")
int handle_switch(void *ctx) {
struct map_value *val;
__u64 start, elapsed;
__u32 key = 0;
val = bpf_map_lookup_elem(&my_map, &key);
if (!val)
return 0;
start = bpf_ktime_get_ns();
bpf_spin_lock(&val->lock);
// Critical section — keep this minimal
val->counter++;
bpf_spin_unlock(&val->lock);
elapsed = bpf_ktime_get_ns() - start;
// Report if lock was held too long
if (elapsed > 1000) {  // > 1 microsecond
bpf_printk("lock held for %llu ns", elapsed);
}
return 0;
}

Struktur data per-CPU. Bug spinlock terbaik adalah bug yang tidak mungkin terjadi. Jika setiap CPU bekerja pada salinan datanya sendiri dan agregasinya dilakukan belakangan, tidak ada kontensi dan tidak perlu lock. BPF_MAP_TYPE_PERCPU_HASH dan BPF_MAP_TYPE_PERCPU_ARRAY dirancang tepat untuk ini. Counter, histogram, dan akumulator hampir tidak pernah butuh spinlock untuk state bersama.

Operasi atomik sebagai pengganti lock. Untuk operasi sederhana seperti menaikkan counter atau membandingkan dan menukar nilai, __sync_fetch_and_add dan atomik sejenis lebih cepat dan bebas deadlock. Kamu tidak butuh spinlock hanya untuk menaikkan counter. Simpan spinlock untuk operasi yang memang perlu memperbarui beberapa field secara atomik sekaligus.

Apa yang Diungkap tentang Model Keamanan eBPF

Cerita keamanan eBPF itu mengesankan tapi tidak sesederhana itu. Verifier menjamin bahwa program per program itu aman: mereka berhenti, tidak mengakses memori tidak valid, dan tidak merusak state kernel. Tapi program yang 'aman' bisa membentuk sistem yang tidak aman ketika interaksi mereka menciptakan dependensi waktu yang tidak bisa dianalisis verifier.

Ini keterbatasan mendasar dari analisis statis. Verifier melihat setiap program secara terisolasi. Ia tidak tahu program lain apa yang sudah dimuat, map mana yang mereka pakai bersama, atau konteks kernel apa yang mereka jalankan. Program yang mengambil spinlock itu 'aman' karena ia mengambil dan melepas dengan benar. Tapi apakah program itu aman dikombinasikan dengan setiap program eBPF lain yang dimuat bergantung pada kondisi runtime.

Komunitas kernel sudah menanganinya secara bertahap. Patch terbaru menambahkan batasan tentang jenis program eBPF mana yang boleh memakai spinlock, berapa lama critical section boleh berjalan, dan operasi apa yang dilarang selama spinlock dipegang. Setiap batasan mempersempit celah bug, tapi juga membatasi apa yang bisa dilakukan program eBPF.

Aturan Praktis untuk Locking di eBPF

Setelah cukup banyak men-debug masalah spinlock, pola-pola untuk menghindarinya mulai terlihat.

  1. Utamakan map per-CPU. Jika datamu bisa dipartisi per CPU lalu diagregasi di user space, lakukan itu. Tanpa lock, tanpa kontensi, tanpa deadlock. Ini menangani 80% kasus, seperti counter, log event, dan histogram.
  2. Utamakan atomik daripada spinlock. Jika kamu butuh counter bersama atau compare-and-swap, gunakan operasi atomik. Operasi ini lock-free dan tidak bisa deadlock.
  3. Kalau terpaksa pakai spinlock, jaga critical section tetap kecil. Naikkan counter, perbarui timestamp, tukar pointer. Tidak lebih. Jangan pernah memanggil helper function saat memegang lock, karena kamu tidak tahu lock apa yang mereka ambil di dalamnya.
  4. Jangan pernah memegang lock yang sama dari dua jenis program eBPF. Jika program kprobe dan program tracepoint sama-sama mengunci nilai map yang sama, kamu hanya selangkah dari deadlock akibat interrupt. Gunakan map terpisah atau data per-CPU.
  5. Tes di bawah beban. Bug spinlock bergantung pada timing. Mereka muncul saat ada kontensi, seperti utilisasi CPU tinggi, interrupt yang sering, dan banyak program eBPF berjalan bersamaan. Tes dengan beban produksi yang realistis, bukan mesin development yang idle.

eBPF telah secara fundamental mengubah cara kita berinteraksi dengan kernel Linux, memberi program user-space akses yang aman ke observability dan networking tingkat kernel. Tapi 'aman' punya batasan. Verifier membuat eBPF jauh lebih aman dibanding menulis kernel module mentah, dan selisihnya sangat besar. Namun itu tidak membuatnya aman dengan cara yang sama seperti pemrograman user-space. Memahami di mana batas-batas itu berada, terutama terkait state bersama dan locking, adalah yang membedakan program eBPF yang berjalan di produksi dari yang hanya berjalan saat testing.