Artikel yang disebutkan oleh sgbj dalam komentar yang ditulis oleh Paul Turner dari Google menjelaskan hal-hal berikut ini dengan lebih detail, tetapi saya akan mencobanya:
Sejauh yang saya bisa menyatukan ini dari informasi terbatas saat ini, retpoline adalah trampolin balik yang menggunakan infinite loop yang tidak pernah dijalankan untuk mencegah CPU berspekulasi pada target lompatan tidak langsung.
Pendekatan dasar dapat dilihat pada cabang kernel Andi Kleen yang membahas masalah ini:
Ini memperkenalkan __x86.indirect_thunk
panggilan baru yang memuat target panggilan yang alamat memorinya (yang akan saya panggil ADDR
) disimpan di atas tumpukan dan menjalankan lompatan menggunakan RET
instruksi. Thunk itu sendiri kemudian dipanggil menggunakan NOSPEC_JMP / CALL makro, yang digunakan untuk mengganti banyak (dan tidak semua) panggilan tidak langsung dan lompatan. Makro hanya menempatkan target panggilan pada tumpukan dan menetapkan alamat kembali dengan benar, jika perlu (perhatikan aliran kontrol non-linear):
.macro NOSPEC_CALL target
jmp 1221f /* jumps to the end of the macro */
1222:
push \target /* pushes ADDR to the stack */
jmp __x86.indirect_thunk /* executes the indirect jump */
1221:
call 1222b /* pushes the return address to the stack */
.endm
Penempatan call
pada akhirnya diperlukan sehingga ketika panggilan tidak langsung selesai, aliran kontrol berlanjut di belakang penggunaan NOSPEC_CALL
makro, sehingga dapat digunakan di tempat biasacall
Ketukan itu sendiri terlihat sebagai berikut:
call retpoline_call_target
2:
lfence /* stop speculation */
jmp 2b
retpoline_call_target:
lea 8(%rsp), %rsp
ret
Aliran kontrol bisa sedikit membingungkan di sini, jadi izinkan saya mengklarifikasi:
call
mendorong penunjuk instruksi saat ini (label 2) ke tumpukan.
lea
menambahkan 8 ke penunjuk tumpukan , secara efektif membuang kata kunci yang paling baru didorong, yang merupakan alamat pengembalian terakhir (ke label 2). Setelah ini, bagian atas tumpukan menunjuk kembali ADDR alamat asli.
ret
melompat ke *ADDR
dan mengatur ulang penunjuk tumpukan ke awal tumpukan panggilan.
Pada akhirnya, seluruh perilaku ini praktis setara dengan melompat langsung ke *ADDR
. Satu manfaat yang kita dapatkan adalah bahwa prediktor cabang yang digunakan untuk pernyataan pengembalian (Return Stack Buffer, RSB), ketika mengeksekusi call
instruksi, mengasumsikan bahwa ret
pernyataan yang sesuai akan melompat ke label 2.
Bagian setelah label 2 sebenarnya tidak pernah dieksekusi, itu hanyalah sebuah loop tak terbatas yang secara teori akan mengisi pipa JMP
instruksi dengan instruksi. Dengan menggunakan LFENCE
, PAUSE
atau lebih umum suatu instruksi yang menyebabkan pipa instruksi menjadi macet menghentikan CPU dari membuang daya dan waktu pada eksekusi spekulatif ini. Ini karena jika panggilan ke retpoline_call_target akan kembali secara normal, itu LFENCE
akan menjadi instruksi berikutnya yang akan dieksekusi. Ini juga yang akan diprediksi oleh prediktor cabang berdasarkan alamat pengirim asli (label 2)
Mengutip dari manual arsitektur Intel:
Instruksi mengikuti LFENCE dapat diambil dari memori sebelum LFENCE, tetapi mereka tidak akan dijalankan sampai LFENCE selesai.
Namun perlu dicatat bahwa spesifikasi tidak pernah menyebutkan bahwa LFENCE dan PAUSE menyebabkan pipa macet, jadi saya membaca sedikit di antara kalimat di sini.
Sekarang kembali ke pertanyaan awal Anda: Pengungkapan informasi memori kernel dimungkinkan karena kombinasi dari dua ide:
Meskipun eksekusi spekulatif harus bebas efek samping ketika spekulasi salah, eksekusi spekulatif masih mempengaruhi hierarki cache . Ini berarti bahwa ketika beban memori dieksekusi secara spekulatif, mungkin masih menyebabkan garis cache digusur. Perubahan dalam hierarki cache ini dapat diidentifikasi dengan secara hati-hati mengukur waktu akses ke memori yang dipetakan ke dalam set cache yang sama.
Anda bahkan dapat membocorkan beberapa bit dari memori arbitrer ketika alamat sumber dari memory read itu sendiri dibaca dari memori kernel.
Prediktor cabang tidak langsung dari CPU Intel hanya menggunakan 12 bit yang paling rendah dari instruksi sumber, sehingga mudah meracuni ke-2 kemungkinan riwayat prediksi dengan alamat memori yang dikontrol pengguna. Ini kemudian, ketika lompatan tidak langsung diprediksi di dalam kernel, dieksekusi secara spekulatif dengan hak istimewa kernel. Dengan menggunakan saluran samping waktu-cache, Anda dapat membocorkan memori kernel sewenang-wenang.
UPDATE: Pada mailing list kernel , ada diskusi yang sedang berlangsung yang membuat saya percaya bahwa retpoline tidak sepenuhnya mengurangi masalah prediksi cabang, seperti ketika Return Stack Buffer (RSB) kosong, arsitektur Intel yang lebih baru (Skylake +) mundur untuk Branch Target Buffer (BTB) yang rentan:
Retpoline sebagai strategi mitigasi menukar cabang tidak langsung untuk pengembalian, untuk menghindari menggunakan prediksi yang berasal dari BTB, karena mereka dapat diracuni oleh penyerang. Masalah dengan Skylake + adalah bahwa RSB underflow kembali menggunakan prediksi BTB, yang memungkinkan penyerang mengendalikan spekulasi.