1 poin oleh GN⁺ 2025-04-24 | 1 komentar | Bagikan ke WhatsApp
  • Di Windows 11 24H2, masalah pesawat amfibi Skimmer yang menghilang atau pemain terpental sangat tinggi ke langit segera setelah spawn berhasil direproduksi, dan penyebabnya bukan OS melainkan bug lama pada pemrosesan data internal game
  • Baris vehicles.ide milik Skimmer kekurangan dua nilai skala roda yang dibutuhkan pesawat, tetapi CFileLoader::LoadVehicleObject tidak memeriksa nilai balik sscanf sehingga langsung memakai variabel lokal yang belum diinisialisasi
  • Di lingkungan Windows lama, nilai skala roda 0.7 milik kendaraan sebelumnya, TopFun, kebetulan tertinggal di stack sehingga Skimmer tampak normal, tetapi di Windows 11 24H2 penggunaan stack oleh LeaveCriticalSection berubah dan kebetulan itu pun hilang
  • Skala roda yang salah merusak perhitungan suspensi dan koordinat Z collision box, lalu menyebar sampai ke tinggi spawn dan perhitungan kecepatan baling-baling, memicu posisi kamera aneh, efek burn-in, dan loop macet di lingkungan SilentPatch
  • Solusinya adalah menambahkan -1, 0.7, 0.7, -1 ke baris Skimmer di vehicles.ide atau menerapkan hotfix SilentPatch berikutnya, dan validasi data input serta pengelolaan peringatan kompilasi berpengaruh langsung pada kompatibilitas jangka panjang

Gejala Skimmer yang muncul di Windows 11 24H2

  • Di issue tracker SilentPatch, muncul laporan bahwa setelah pembaruan Windows 11 24H2, pesawat Skimmer benar-benar hilang dari game
    • Tidak bisa di-spawn bahkan dengan trainer, dan juga tidak ditemukan di lokasi spawn aslinya
    • Masalah ini bisa direproduksi baik pada game yang memakai mod maupun pada salinan vanilla yang hanya dipasangi SilentPatch
  • Di GTAForums, masalah yang sama juga dilaporkan sejak November 2024, dan meski sebagian pengguna mencurigai SilentPatch, gejala yang sama muncul bahkan pada game tanpa mod sama sekali
  • Di Windows 10 22H2 dan Windows 11 23H2, Skimmer spawn normal, sedangkan pengguna Windows 11 24H2 mengalami bug yang sama
  • Hasil remote debugging pada VM 24H2 menunjukkan pesawat dan kapal lain normal, dan hanya Skimmer yang menghilang

Ketinggian abnormal dan loop baling-baling yang tak pernah selesai

  • Saat Skimmer dipaksa dibuat lewat skrip dan CJ dinaikkan ke dalamnya, pemain terpental ke 1.0287648030984853e+0031 m, sekitar 10,3 noniliun meter ke atas
  • Jika SilentPatch terpasang, game masuk ke loop dan macet segera setelah pemain dilontarkan ke atas
  • Tanpa SilentPatch, game tidak macet, tetapi muncul efek burn-in terkenal yang terjadi ketika kamera berpindah ke posisi yang mendekati tak hingga
  • Titik macet berada di loop normalisasi sudut rotor dalam CPlane::PreRender
    • Nilai m_fBladeSpeed membesar hingga 3.73340132e+29
    • Walaupun 6.2831855 terus dikurangi berulang kali, nilainya tidak berubah dalam representasi floating-point sehingga loop tidak pernah selesai
  • Karena kecepatan baling-baling diturunkan dari nilai yang proporsional terhadap ketinggian pesawat, ini menjadi petunjuk bahwa Skimmer sejak awal dibuat pada posisi yang sangat tidak normal

Perhitungan suspensi yang merusak collision box

  • Fungsi pembuatan lewat skrip CCarCtrl::CreateCarForScript menambahkan hasil GetDistanceFromCentreOfMassToBaseOfModel ke koordinat Z yang diberikan
  • Saat collision box Skimmer diperiksa, bbox.sup.z ternyata sudah tercemar dengan nilai tidak masuk akal seperti -4.30747210e+33
  • Penelusuran dengan data breakpoint menunjukkan bahwa nilai collision box saat loading awal sebenarnya normal
    • Nilai awal bbox.sup.z adalah -2.21952772
    • Setelah kendaraan pertama kali spawn, SetupSuspensionLines memperbarui koordinat Z collision box dengan memperhitungkan tinggi suspensi
  • Salah satu nilai input yang masuk ke perhitungan garis suspensi itulah sumber masalahnya
    • Perhitungan memakai batas atas dan bawah suspensi dari handling.cfg serta skala roda dari vehicles.ide
    • Nilai Skimmer di handling.cfg tidak jauh berbeda dari pesawat lain

Baris vehicles.ide Skimmer yang terlalu pendek

  • Definisi Skimmer di vehicles.ide lebih pendek daripada pesawat lain, dan empat parameter terakhir hilang
  • Dua dari nilai yang hilang itu adalah skala roda depan dan belakang
  • Pada kapal, tidak adanya nilai ini tidak menimbulkan masalah, tetapi Skimmer adalah satu-satunya pesawat yang menghilangkan parameter tersebut
  • Skimmer tampaknya awalnya didefinisikan sebagai kapal di Vice City lalu diubah menjadi pesawat di San Andreas, namun parameter baru yang diperlukan tidak ikut ditambahkan
  • Jika parameter yang hilang dikembalikan, Skimmer kembali berfungsi normal

Loader yang tidak memeriksa nilai balik sscanf

  • CFileLoader::LoadVehicleObject mem-parse satu baris vehicles.ide dengan sscanf sambil berasumsi semua parameter selalu ada
  • Fungsi ini tidak memeriksa nilai balik sscanf, dan juga tidak memberi nilai default pada sebagian besar parameter terakhir
    • wheelModelID tidak diinisialisasi
    • frontWheelScale dan rearWheelScale juga tidak diinisialisasi
    • Hanya wheelUpgradeClass yang diinisialisasi ke -1
  • Pada baris yang kekurangan nilai seperti Skimmer, variabel skala roda tetap belum diinisialisasi dan nilainya lalu menyebar ke data kendaraan
  • Perbaikan SilentPatch membungkus pemanggilan sscanf dan memberi nilai default pada empat nilai terakhir
    • wheelModelID = -1
    • frontWheelSize = 0.7f
    • rearWheelSize = 0.7f
    • wheelUpgradeClass = -1
  • Commit perbaikannya tercermin di repositori SilentPatch

Mengapa bug ini tersembunyi selama 20 tahun

  • San Andreas memakai CRT yang dikompilasi statis, jadi bukan berarti hotfix tingkat CRT di Windows mengubah perilaku sscanf
  • Di Windows 10, nilai 0.7 masih tertinggal di lokasi variabel lokal tepat sebelum parsing Skimmer
    • Nilai ini cocok dengan skala roda TopFun yang didefinisikan tepat sebelum Skimmer
    • Baris TopFun berisi -1, 0.7, 0.7, -1
  • vehicles.ide dibaca berurutan, dan LoadVehicleObject dipanggil untuk setiap baris
  • Di Windows 10, posisi stack tersebut tidak tertimpa di antara panggilan LoadVehicleObject, sehingga Skimmer secara kebetulan mewarisi skala roda TopFun
  • Di Windows 11 24H2, LeaveCriticalSection di dalam fgets memakai ruang stack lebih banyak saat membaca baris berikutnya, sehingga nilai yang tersisa tadi tertimpa

Windows 11 24H2 hanyalah pemicunya

  • Cara fungsi internal WinAPI memakai stack bukan perilaku yang dijamin, dan bisa berubah tanpa pemberitahuan sebelumnya
  • Windows 11 24H2 hanya menghapus nilai sisa di stack yang selama ini secara kebetulan diandalkan game; akar masalah sebenarnya adalah undefined behavior di dalam game
  • Bahkan di Windows 10, variabel lokal tepat setelah skala roda sudah pernah tertimpa oleh LeaveCriticalSection, jadi game sebenarnya sudah lama berada dalam kondisi yang bisa memicu bug ini
  • Karena San Andreas juga mendukung Windows 98, bug ini setidaknya tidak sengaja tersembunyi di sekitar belasan versi Windows dan beberapa rilis Wine
  • Di patch resmi PC 1.01, bug ini tidak diperbaiki, tetapi pada rilis Xbox aslinya sudah ada perbaikan yang memberi nilai default 1.0
    • Steam 3.0, newsteam, dan RGL mewarisi perbaikan ini karena berbasis cabang kode Xbox
    • Rilis Android, X360, PS3, dan Definitive Edition dari War Drum Studios juga tidak terdampak

Mengapa SilentPatch memilih 0.7 sebagai nilai default

  • SilentPatch memakai skala roda default 0.7, bukan 1.0 seperti perbaikan Xbox dari Rockstar
  • Ada tiga alasan untuk pilihan ini
    • Di versi PC, selama ini Skimmer pada praktiknya memang berjalan dengan skala roda 0.7 milik TopFun
    • Sea Sparrow dan Vortex, dua kendaraan non-kapal lain yang mengapung di air, juga memakai skala roda 0.7
    • Banyak mobil lain di dalam game juga memakai skala roda 0.7

Cara memperbaikinya sendiri

  • Perbaikan kode akan dimasukkan ke hotfix SilentPatch berikutnya
  • Untuk memperbaikinya sekarang juga, buka data\vehicles.ide di direktori San Andreas dengan Notepad lalu ganti baris yang diawali 460, skimmer
  • Baris penggantinya adalah sebagai berikut
460, 	skimmer,	skimmer, 	plane,		SEAPLANE,	SKIMMER,	null,	ignore,		5,	0,	0,		-1, 0.7, 0.7,		-1

Pelajaran dari kompatibilitas game lama

  • Masalah ini hanyalah bug sederhana di San Andreas, dan fungsi tersebut sejak awal memang berupa kode yang tidak mungkin bekerja dengan benar
  • Perubahan layout stack pada implementasi internal pun dapat berubah menjadi masalah kompatibilitas jika aplikasi yang bug bergantung secara kebetulan pada perilaku tertentu
  • Contoh serupa adalah Bully: Scholarship Edition yang rusak di Windows 10 karena bergantung pada asumsi yang salah hingga masalahnya muncul saat OS berubah
  • Masalah mendasar San Andreas adalah tidak adanya validasi data input yang mampu menolak baris konfigurasi yang tidak lengkap
  • Kode ini kemungkinan besar sejak awal memunculkan peringatan kompilasi, dan jika peringatan diabaikan atau dinonaktifkan, bug yang lama tersembunyi dapat muncul sebagai masalah nyata bagi pengguna

1 komentar

 
GN⁺ 2025-04-24
Opini Hacker News
  • Tulisan seperti ini rasanya hanya bisa diharapkan dari Raymond Chen, dan itu pujian yang sangat besar
    Senang melihat penulisnya menggali lebih jauh sampai menemukan persis alasannya

  • Secara pribadi, menurut saya jika itu adalah perilaku yang tidak termasuk dalam kontrak, sebaiknya dibuat acak
    Misalnya, jika sebuah bahasa tidak menjamin urutan iterasi map, maka urutannya seharusnya sengaja diacak
    Kalau tidak, akan muncul kode rapuh yang “berjalan baik sampai suatu hari tiba-tiba rusak”

    • Ada berbagai opsi compiler seperti -ftrivial-auto-var-init yang menginisialisasi variabel yang belum diinisialisasi dengan nilai tertentu atau nilai acak
      Namun mengacak atau mengisi nol seluruh isi stack pada setiap pemanggilan fungsi akan menimbulkan penurunan performa yang mengerikan, jadi biasanya itu tidak dilakukan
    • Pengacakan pada level ini terlalu mahal biayanya
      Ada tool yang melakukan hal seperti ini untuk tujuan debugging, tetapi dalam mode itu program berjalan jauh lebih lambat
    • Dari sudut pandang kontrak, ada juga pelajaran dari tulisan aslinya: “Ini adalah pelajaran menarik tentang kompatibilitas. Jika sebuah aplikasi memiliki bug dan tanpa sengaja bergantung pada perilaku tertentu, bahkan perubahan tata letak stack dalam implementasi internal pun dapat berdampak pada kompatibilitas”
      Mungkin inilah alasan para maintainer kernel Linux bersikeras agar user space jangan pernah dirusak
    • Tidak. Kita perlu mengingat https://www.hyrumslaw.com/
      Jika pengguna API cukup banyak, apa yang dijanjikan dalam kontrak tidak lagi penting; akan selalu ada seseorang yang bergantung pada setiap perilaku sistem yang dapat diamati
      Jika Anda menjanjikan pengacakan, akan ada seseorang yang bergantung juga pada pengacakan itu
      Maka itu pun tidak akan pernah bisa dihapus
    • Salah satu keunggulan bahasa seperti C bisa dilihat sebagai: Anda hanya membayar biaya untuk fitur yang Anda pilih
      Anda tidak dipaksa membayar overhead yang tidak perlu seperti inisialisasi variabel yang tidak digunakan
  • Pada bagian “jangan abaikan peringatan kompilasi”, saya tidak tahu error compiler seperti apa yang bisa diharapkan di sini
    Mungkin sebatas tidak memeriksa apakah nilai balik scanf sesuai dengan jumlah argumen? Selain itu, ini tampak seperti kesalahan file data yang tidak bisa diketahui compiler

    • Jika dicoba dengan g++ 11.4, tidak ada peringatan default meski nilai balik sscanf tidak diperiksa
      Dalam contoh kecil, bahkan dengan g++ -Wall -Wextra -Wunused-result pun tidak muncul peringatan
    • Mengakses memori yang belum diinisialisasi adalah perilaku tak terdefinisi, jadi sanitizer seharusnya bisa menangkapnya
    • Poin yang bagus. Saat membacanya, saya samar-samar mengira peringatan “penggunaan memori yang belum diinisialisasi” akan menangkap ini
      Namun karena seluruh baris diparsing dengan satu pemanggilan sscanf, analisis statis compiler mau tidak mau harus mengasumsikan nilai-nilai itu kini sudah diinisialisasi
      Sepertinya tidak ada metode analisis statis umum untuk menangkap bug ini
      Namun mungkin bisa dibuat peringatan khusus untuk scanf yang memaksa nilai yang sudah diinisialisasi sebelumnya diteruskan, atau nilai baliknya diperiksa
  • Membaca tulisan analisis teknis mendalam seperti ini selalu menyenangkan
    Saya penasaran apakah tulisan seperti ini akan makin langka di era AI atau tidak

    • Sepertinya tidak akan makin langka. Akan selalu ada engineer papan atas yang menggali sangat dalam
      AI tidak akan menggantikan mereka, dan berbagai inovasi pengembangan software selama lebih dari 50 tahun terakhir pun tidak berhasil melakukannya
      Jutaan, mungkin puluhan juta developer bahasa pemrograman tingkat tinggi hanya mengenal perbedaan stack dan heap sebagai teori samar yang pernah dipelajari di sekolah, dan karena tidak perlu memikirkannya dalam pekerjaan sehari-hari, mereka juga tidak tertarik
    • Engineer software pada umumnya mungkin bergeser dari pengrajin ke arah pekerja teknis, tetapi tulisan seperti ini tampaknya lahir dari gaya keahlian ala pengrajin itu sendiri
  • Saya lebih penasaran apa yang berubah pada implementasi lock/unlock critical section di versi Windows ini

    • Sepertinya ukuran stack yang digunakan atau area pelindung stack bertambah
  • Apakah hanya saya yang terganggu oleh kode ini?
    while (this->m_fBladeAngle > 6.2831855) { this->m_fBladeAngle = this->m_fBladeAngle - 6.2831855; }
    Rasanya seperti memakai loop while yang bisa saja menjadi loop tak berujung hanya karena malas melakukan pembagian

    • Saya ingin percaya developer GTA melakukan hack seperti ini karena di lingkungan seperti PlayStation 2 itu lebih cepat daripada pembagian floating-point
      Namun melihat mereka pernah mem-parsing JSON dengan sscanf sampai waktu loading GTA5 bisa bertambah 5 menit, ekspektasi saya tidak terlalu tinggi
    • Menurut saya besar kemungkinan alasannya performa. Pengurangan lebih murah daripada pembagian floating-point
      Compiler juga mungkin punya teknik untuk mengoptimalkannya lebih jauh
      Sebenarnya nyaris tidak ada cara agar ini menjadi loop tak berujung. Underflow memang mungkin, tetapi untuk itu sudutnya harus sudah lebih kecil dari 2*pi, sehingga loop akan keluar
    • Kecil kemungkinannya, tetapi jika nilainya kecil, loop ini bisa saja lebih cepat daripada pembagian
    • Memang benar. Sepertinya penulisnya sama sekali tidak tahu fmod
  • Bagi yang bermasalah mengaksesnya, bisa memakai tautan ini
    https://web.archive.org/web/20250423144746/https://cookieplm...

  • Karena tahu C/C++, sejak awal blog saya sudah bisa menebak kira-kira apa yang terjadi, yaitu masalah variabel yang belum diinisialisasi
    Mengejutkan bahwa ada bahasa yang membiarkan variabel dibiarkan tanpa diinisialisasi. Hal ini telah menyebabkan bug yang tak terhitung jumlahnya, termasuk bug produksi yang pernah saya lihat sendiri, dan untuk menangkapnya sering kali harus bergantung pada flag compiler tambahan, tool analisis statis, Valgrind, dan sebagainya
    Bahasa yang lebih modern memilih solusi lain, seperti memakai nilai default 0 atau mewajibkan inisialisasi sebelum digunakan, tetapi orang-orang tetap kembali ke C/C++

  • Bagian “Semua temuan ini membuktikan bahwa bug tersebut bukan masalah Windows 11 24H2. Hal seperti cara fungsi WinAPI internal menggunakan stack bukanlah kontrak, dan dapat berubah kapan saja tanpa pemberitahuan sebelumnya” mengingatkan saya pada tulisan bagus yang pernah saya baca
    Intinya, pada API yang cukup sukses, tidak ada yang namanya API privat

    • Kalau bisa temukan tulisan itu dan bagikan tautannya. Saya penasaran dengan logikanya
    • Saya tahu ada komik XKCD yang terkait dengan hal ini