Fuzzing Günlüğü #2: V8, TurboFan ve Gizli Komutlar
Fuzzing ile adım adım ilerleme devam ediyor. İlk yazıdan hatırlanacağı üzere Fuzzing için ortam kurulumu ve eldeki script’imi daha kullanışlı/akıllı hale getirmiştim. Bugünkü kısımda seviyemi biraz daha yukarı çekip (kendimce tabii) gerçek V8 motorunu derledim ve JavaScript’in nasıl makine koduna dönüştüğünü fark ettim diyebilirim.
Chromium yerine V8 motorunu —en azından bu seviyede— tercih etme sebebim şu: JavaScript motoru açıklarının neredeyse tamamı, Chromium’un dev gövdesine ihtiyaç duymadan, V8’in tek başına çalışan kabuğu d8 içinde tetiklenebiliyor. İncelediğim Project Zero yazıları olsun, benzeri diğer kaynaklar olsun; hepsi bütün tarayıcıyı değil, bu d8’i hedefliyordu.
Bu kapsamda macOS (Silicon)‘da linux/amd64 Docker içinde depot_tools ile V8 15.2.0’ı çekip yalın bir d8 derledim. Burada karşılaştığım tek sorun şuydu: 8 GB RAM’de -O3 paralel derleme belleği tüketip OOM’a giriyordu; paralelliği -j4’e düşürünce problem kalmadı.
İlk Deneme Aşaması
V8’in geliştiricileri, motoru test edebilmek için içeriye gizli bir servis paneli koymuşlar. Bunlara “intrinsic” deniyor: % işaretiyle başlayan, normal JavaScript’te olmayan özel komutlar. Bir nevi hile kodları kullanıp oyun oynamak denilebilir: normalde oyunu o şekilde oynayamazsın çünkü varsayılan olarak kapalıdır. Ama bir kod kombinasyonuyla “tüm haritayı göster” gibi komutlar kullanılarak açılabilir.
Biz JIT’i incelemek istiyoruz ve normal şartlarda bir fonksiyonun ne zaman optimize edileceği belirsiz olabiliyor. Araştırma ya da pratik yaptığımız zamanlarda bu belirsizliği istemeyeceğimiz için “intrinsic”leri kullanacağız; bu sayede optimizasyonu istediğimiz anda, istediğimiz fonksiyonda tetikleyerek sonucu kendi lab ortamımızda gözlemleyebileceğiz.
D8’de de benzer bir mantık var: bu gizli paneli açabilmek için --allow-natives-syntax komutuyla başlatmak gerekiyor. Bu komut “ben bir araştırmacıyım, servis panelini açıyorum” anlamına geliyor diyebiliriz. Panele eriştikten sonra elimizde şu üç kod oluyor:
%PrepareFunctionForOptimization(add)— “Bu fonksiyonu birazdan optimize edeceğim, hazırlan.” Motora fişi takmak gibi.%OptimizeFunctionOnNextCall(add)— “Bir sonraki çağrıda bu fonksiyonu JIT ile optimize et.” Normalde motor bu kararı kendi verir (bir fonksiyon yeterince sık çağrılınca); biz bu kararı elle ve anında tetikliyoruz ki beklemek zorunda kalmayalım.%GetOptimizationStatus(add)— “Şu an bu fonksiyon hangi durumda? Yorumlanıyor mu, optimize mi edildi, hangi katta?” Motorun gösterge panelini okumak gibi.
Kodlar:
function add(a, b) { return a + b; }
%PrepareFunctionForOptimization(add);
add(1, 2); add(3, 4); // yorumlayıcı feedback toplasın
%OptimizeFunctionOnNextCall(add); // bir sonraki çağrıda optimize et
add(5, 6); // artık JIT edilmiş kod çalışır
print(%GetOptimizationStatus(add));
Bu kodu çalıştırdığımız zaman, %GetOptimizationStatus(add) bize çıktıyı 41 verdi. Bu çıktı bir sayıdan ziyade bir kontrol listesi: motorun sana cevaplamak istediği bir sürü evet/hayır sorusu var. “Bu bir fonksiyon mu?”, “Optimize edildi mi?”, “TurboFan mı kullandı?”, “Maglev mi kullandı?”… Motor bu cevapların hepsini tek tek yazmak yerine, hepsini tek bir sayıya paketliyor. Buna programcılıkta bit-maskesi deniyor.
Bunu bir sıralı ışık düğmesi gibi hayal edebilirsiniz. Her düğme bir soruya karşılık geliyor, ya açık (evet) ya kapalı (hayır):
… TurboFan?(AÇIK) Maglev?(kapalı) Optimize?(AÇIK) … Fonksiyon?(AÇIK)
Bu açık/kapalı dizisini tek bir sayıya çevirdiğinde 41 çıkıyor. Tersine, 41’i tekrar düğmelere ayırdığında hangi soruların “evet” olduğunu geri okuyabiliyorsun. Çözünce şu üç düğmenin açık olduğunu gördüm:
41 = kIsFunction + kOptimized + kTurboFanned → “Evet bir fonksiyon, evet optimize edildi, ve evet bunu TurboFan yaptı.”
İlk yanılmam: Ezbere iş yapmamak lazım
Hangi düğmenin hangi soru olduğunu ezbere yaptım ve haliyle yanlış oldu. “Optimize edildi mi?” düğmesi sandığım yer aslında “Maglev mi?” düğmesiymiş. Bu yüzden ilk denememde kod bana “optimize edilmedi” gibi yanlış bir cevap verdi; oysa fonksiyon gayet optimize edilmişti.
Peki nasıl “ezbere” yaptım?
Aslında değeri havadan uydurmadım — kafamda genel bir resim vardı: “optimize edildi” biti şuralarda bir yerde olmalı. Ama iki şeyi hesaba katmadım. Birincisi, kOptimized ve kMaglevved bitleri komşu (biri 3, diğeri 4); insan hangisinin hangisi olduğunu kolayca karıştırıyor.

İkincisi ve daha önemlisi: bu bitler sabit değil. Maglev, V8’e sonradan eklenen bir katman ve enum’a girdiğinde altındaki bütün bayrakları birer basamak öteledi. Yani benim “bildiğim” değer, Maglev’in henüz doğmadığı bir V8 dünyasına aitti. Motor değişmişti, benim ezberim değişmemişti. İşte “kaynaktan doğrula” kuralı buradan doğdu.
Doğru yolu bulmak için motorun kendi kaynak kodunda, d8’in derlendiği V8 sürümünün içindeki projedosyası/v8-dev/src/v8/src/runtime/runtime.h dosyasını inceledim. Burada hangi düğmenin hangi soru olduğu satır satır listelenmiş. Çünkü bu düğmelerin sırası V8 sürümünden sürüme değişebiliyor; bir blogda okuduğun “doğru” değer, senin derlediğin sürümde kaymış olabiliyor.
1 << 3 (=8) Gerçek Değer
1 << 4 (=16) Benim ezbere sandığım değer
Optimizasyon bir merdivendir
Bir yan not: 41 bize sadece “optimize edildi” demiyor, hangi kat tarafından edildiğini de söylüyor. Çünkü modern V8 kodu tek hamlede makine koduna çevirmez; bir merdivenden çıkarır.

V8, bir kod parçasını tek hamlede en hızlı makine koduna çevirmez — bu israf olurdu, çünkü kodun çoğu sadece bir-iki kez çalışır. Onun yerine bir merdiven kullanır: kod ne kadar sık çalışırsa (yani “ısındıkça”) o kadar üst basamağa terfi eder ve motor onu optimize etmeye o kadar çok emek harcar.
En alttaki Ignition anında başlar ama yavaş çalışır; en üstteki TurboFan üretilmesi pahalıdır ama en hızlı kodu verir. Bizim add() fonksiyonumuzu %OptimizeFunctionOnNextCall ile elimizle en üst basamağa, TurboFan’a zorladık — normalde oraya ancak binlerce kez çağrılsa kendiliğinden çıkardı.
Ve saldırganların iştahını kabartan basamak tam da bu en üsttekidir: TurboFan en agresif varsayımları yapan kattır, dolayısıyla bir varsayımı yanlış çıktığında en tehlikeli hataların doğduğu yer de burasıdır.
TurboFan’ın ürettiği kodu görmek
Bunu görmek için --print-opt-code ile TurboFan’ın ürettiği gerçek x86-64’e baktım. a + b gibi masum bir ifade, şuna dönüşmüştü:
testb rcx,0x1 ; a bir Smi mi?
jnz <deopt> ; değilse iptal
...
addl r9,rdi
jo <deopt> ; taşma olduysa iptal
Bu guard’lar doğruyken her şey güvenli. Ama derleyici, bir “yan etki” yüzünden bu kontrollerden birini gereksiz sanıp kaldırırsa, saldırgan artık motora yalan söyleyebilir — “uzunluğu 5” dediği diziye 100. indeksten yazabilir.