Mainkan "osu!", But Watch Out for Bugs

Gambar 1


Hai, kalian semua pengoleksi serangga eksotis dan polos! Kami punya spesimen langka di bangku tes PVS-Studio kami hari ini - permainan yang disebut "osu!", Ditulis dalam C #. Seperti biasa, kami akan mencari bug, menganalisisnya, dan bermain.

Permainan


Osu! adalah game rhythm open-source. Menurut situs web gim, ini cukup populer, dengan lebih dari 15 juta akun pemain. Proyek ini menampilkan permainan gratis, desain penuh warna, kustomisasi peta, sistem peringkat pemain online tingkat lanjut, mode multi-pemain, dan sekumpulan karya musik yang kaya. Tidak ada gunanya menjabarkan lebih lanjut tentang game; Anda dapat membaca semua tentang itu di Internet. Mulai dengan halaman ini .

Saya lebih tertarik pada kode sumber proyek, yang tersedia di GitHub . Satu hal yang segera menarik perhatian Anda adalah banyaknya jumlah tempat penyimpanan (lebih dari 24 ribu), yang merupakan pertanda perkembangan yang intens dan berkelanjutan (permainan ini pertama kali dirilis pada 2007, tetapi pekerjaan itu harus sudah dimulai lebih awal). Proyek ini tidak besar: hanya 1813 file .cs dengan total 135 ribu LOC tidak kosong. Jumlah ini juga termasuk tes, yang biasanya tidak saya perhitungkan saat menjalankan cek. Tes terdiri dari 306 file .cs dengan 25 ribu LOC. Proyek ini memang kecil: misalnya, inti C # dari PVS-Studio adalah sekitar 300 ribu LOC.

Menyisakan file uji, saya memeriksa 1507 file, 110 ribu LOC. Cek itu mengungkapkan beberapa bug menarik, yang saya ingin tunjukkan kepada Anda.

Bug


V3001 Ada hasil sub-ekspresi yang identik '== HitResult.Perfect' ke kiri dan ke kanan '||' operator. DrawableHoldNote.cs 266

protected override void CheckForResult(....) { .... ApplyResult(r => { if (holdNote.hasBroken && (result == HitResult.Perfect || result == HitResult.Perfect)) result = HitResult.Good; .... }); } 

Ini adalah contoh yang bagus dari pemrograman berorientasi copy-paste, yang merupakan istilah lucu yang baru-baru ini digunakan oleh rekan kerja saya Valeriy Komarov dalam artikelnya " Top 10 Bug yang Ditemukan di Proyek Java pada 2019 ".

Bagaimanapun, dua pemeriksaan identik dieksekusi berturut-turut. Salah satunya mungkin dimaksudkan untuk memeriksa beberapa konstanta lain dari penghitungan HitResult :

 public enum HitResult { None, Miss, Meh, Ok, Good, Great, Perfect, } 

Konstanta mana yang dimaksudkan untuk diperiksa? Atau mungkin cek kedua tidak seharusnya ada di sana? Ini adalah pertanyaan yang hanya bisa dijawab oleh penulis. Bagaimanapun, ini adalah kesalahan yang mengubah logika eksekusi program.

V3001 Ada sub-ekspresi identik 'family! = GetFamilyString (TournamentTypeface.Aquatico)' di sebelah kiri dan di sebelah kanan operator '&&'. TournamentFont.cs 64

 public static string GetWeightString(string family, FontWeight weight) { .... if (weight == FontWeight.Regular && family != GetFamilyString(TournamentTypeface.Aquatico) && family != GetFamilyString(TournamentTypeface.Aquatico)) weightString = string.Empty; .... } 

Copy-paste lagi. Saya refactored kodenya sehingga kesalahan mudah dilihat sekarang tetapi awalnya telah ditulis dalam satu baris. Sama seperti pada contoh sebelumnya, saya tidak bisa mengatakan dengan pasti bagaimana tepatnya yang ini harus diperbaiki. Enumerasi TournamentTypeface hanya berisi satu konstanta:

 public enum TournamentTypeface { Aquatico } 

Mungkin kesalahannya adalah tentang memeriksa variabel keluarga dua kali, tetapi saya mungkin salah.

V3009 [CWE-393] Aneh bahwa metode ini selalu mengembalikan satu dan nilai 'false' yang sama. KeyCounterAction.cs 19

 public bool OnPressed(T action, bool forwards) { if (!EqualityComparer<T>.Default.Equals(action, Action)) return false; IsLit = true; if (forwards) Increment(); return false; } 

Metode ini mengembalikan false setiap kali. Dalam kasus seperti ini, saya biasanya akan memeriksa pemanggilan fungsi, karena Anda mungkin sering menemukan bahwa pemanggil tidak menggunakan nilai balik, yang berarti tidak ada masalah (selain gaya buruk). Seperti inilah panggilan dalam kasus ini:

 public bool OnPressed(T action) => Target.Children .OfType<KeyCounterAction<T>>() .Any(c => c.OnPressed(action, Clock.Rate >= 0)); 

Seperti yang Anda lihat, pemanggil tidak menggunakan nilai yang dikembalikan oleh metode OnPressed . Karena nilai itu selalu salah , penelepon itu sendiri selalu mengembalikan false juga. Kode ini sangat mungkin mengandung kesalahan dan harus direvisi.

Bug serupa lainnya:

  • V3009 [CWE-393] Aneh bahwa metode ini selalu mengembalikan satu dan nilai 'false' yang sama. KeyCounterAction.cs 30

V3042 [CWE-476] Kemungkinan NullReferenceException. '?.' dan '.' operator digunakan untuk mengakses anggota objek 'val.NewValue' TournamentTeam.cs 41

 public TournamentTeam() { Acronym.ValueChanged += val => { if (....) FlagName.Value = val.NewValue.Length >= 2 // <= ? val.NewValue?.Substring(0, 2).ToUpper() : string.Empty; }; .... } 

Variabel val.NewValue ditangani dengan cara yang berbahaya dalam kondisi ?: Operator. Apa yang membuat penganalisa berpikir demikian adalah kenyataan bahwa kemudian di cabang kemudian , variabel yang sama ditangani dengan cara yang aman menggunakan operator akses bersyarat: val.NewValue? .Substring (....) .

Bug serupa lainnya:

  • V3042 [CWE-476] Kemungkinan NullReferenceException. '?.' dan '.' operator digunakan untuk mengakses anggota objek 'val.NewValue' TournamentTeam.cs 48

V3042 [CWE-476] Kemungkinan NullReferenceException. '?.' dan '.' operator digunakan untuk mengakses anggota objek 'api' SetupScreen.cs 77

 private void reload() { .... new ActionableInfo { Label = "Current User", ButtonText = "Change Login", Action = () => { api.Logout(); // <= .... }, Value = api?.LocalUser.Value.Username, .... }, .... } private class ActionableInfo : LabelledDrawable<Drawable> { .... public Action Action; .... } 

Yang ini lebih ambigu, tapi saya percaya itu bug juga. Programmer menciptakan objek bertipe ActionableInfo . Bidang Tindakan diinisialisasi dengan menggunakan fungsi lambda, yang menangani api referensi yang berpotensi nol dengan cara yang berbahaya. Penganalisa menganggap pola ini sebagai kesalahan karena variabel api ditangani dengan cara yang aman nanti, ketika menginisialisasi parameter Nilai . Saya menyebut kasus ini ambigu karena kode pada fungsi lambda menyiratkan eksekusi yang tertunda, pada saat itu pengembang mungkin dapat menjamin bahwa referensi api tidak akan nol. Tapi saya tidak yakin tentang itu karena tubuh fungsi lambda tampaknya tidak menggunakan penanganan referensi yang aman seperti cek sebelumnya.

V3066 [CWE-683] Kemungkinan urutan argumen yang salah diteruskan ke metode 'Atan2': 'diff.X' dan 'diff.Y'. SliderBall.cs 182

 public void UpdateProgress(double completionProgress) { .... Rotation = -90 + (float)(-Math.Atan2(diff.X, diff.Y) * 180 / Math.PI); .... } 

Penganalisa menduga bahwa argumen metode Atan2 dilewatkan dalam urutan yang salah. Ini adalah deklarasi metode:

 // Parameters: // y: // The y coordinate of a point. // // x: // The x coordinate of a point. public static double Atan2(double y, double x); 

Nilai diteruskan dalam urutan terbalik. Saya tidak yakin apakah ini bug karena metode UpdateProgress mengandung cukup banyak perhitungan nontrivial; Saya hanya menyebut itu sebagai bug yang mungkin.

V3080 [CWE-476] Kemungkinan null dereference. Pertimbangkan untuk memeriksa 'Beatmap'. WorkingBeatmap.cs 57

 protected virtual Track GetVirtualTrack() { .... var lastObject = Beatmap.HitObjects.LastOrDefault(); .... } 

Penganalisa menunjukkan kemungkinan null dereference dari Beatmap :

 public IBeatmap Beatmap { get { try { return LoadBeatmapAsync().Result; } catch (TaskCanceledException) { return null; } } } 

Nah, alat analisa itu benar.

Untuk mempelajari lebih lanjut tentang bagaimana PVS-Studio mendeteksi bug seperti ini, dan tentang fitur baru yang ditambahkan dalam C # 8.0 yang ada hubungannya dengan penanganan referensi yang berpotensi nol, lihat artikel " Jenis referensi yang dapat dihapus dalam C # 8.0 dan analisis statis ".

V3083 [CWE-367] Doa yang tidak aman dari acara 'ObjectConverted', NullReferenceException dimungkinkan. Pertimbangkan menugaskan acara ke variabel lokal sebelum menjalankannya. BeatmapConverter.cs 82

 private List<T> convertHitObjects(....) { .... if (ObjectConverted != null) { converted = converted.ToList(); ObjectConverted.Invoke(obj, converted); } .... } 

Ini adalah kesalahan kecil dan cukup umum. Pelanggan dapat berhenti berlangganan dari acara antara cek nol dan permintaan acara, yang mengakibatkan crash. Ini adalah salah satu cara untuk memperbaiki bug:

 private List<T> convertHitObjects(....) { .... converted = converted.ToList(); ObjectConverted?.Invoke(obj, converted); .... } 

V3095 [CWE-476] Objek 'kolom' digunakan sebelum diverifikasi terhadap nol. Periksa baris: 141, 142. SquareGraph.cs 141

 private void redrawProgress() { for (int i = 0; i < ColumnCount; i++) columns[i].State = i <= progress ? ColumnState.Lit : ColumnState.Dimmed; columns?.ForceRedraw(); } 

Iterasi atas koleksi kolom dilakukan dengan cara yang berbahaya. Pengembang mengasumsikan bahwa referensi kolom bisa nol, yang ditunjukkan dengan penggunaan operator akses bersyarat untuk mengakses koleksi lebih lanjut dalam kode.

V3119 Memanggil acara yang diganti 'OnNewResult' dapat menyebabkan perilaku yang tidak dapat diprediksi. Pertimbangkan untuk menerapkan pengakses acara secara eksplisit atau gunakan kata kunci 'tersegel'. DrawableRuleset.cs 256

 private void addHitObject(TObject hitObject) { .... drawableObject.OnNewResult += (_, r) => OnNewResult?.Invoke(r); .... } public override event Action<JudgementResult> OnNewResult; 

Penganalisa mengatakan berbahaya untuk menggunakan acara yang ditimpa atau virtual. Lihat deskripsi diagnostik untuk penjelasan. Saya juga menulis artikel tentang topik ini: " Peristiwa virtual di C #: ada yang salah ".

Berikut ini konstruksi serupa lainnya yang tidak aman:

  • V3119 Memanggil acara yang diganti dapat menyebabkan perilaku yang tidak terduga. Pertimbangkan untuk menerapkan pengakses acara secara eksplisit atau gunakan kata kunci 'tersegel'. DrawableRuleset.cs 257

V3123 [CWE-783] Mungkin '??' Operator bekerja dengan cara yang berbeda dari yang diharapkan. Prioritasnya lebih rendah daripada prioritas operator lain di bagian kirinya. OsuScreenStack.cs 45

 private void onScreenChange(IScreen prev, IScreen next) { parallaxContainer.ParallaxAmount = ParallaxContainer.DEFAULT_PARALLAX_AMOUNT * ((IOsuScreen)next)?.BackgroundParallaxAmount ?? 1.0f; } 

Untuk pemahaman yang lebih baik, berikut adalah contoh sintetis yang menunjukkan logika asli kode ini:

 x = (c * a) ?? b; 

Bug berasal dari fakta bahwa prioritas operator "*" lebih tinggi daripada "??" operator. Ini adalah kode tetap yang seharusnya (dengan tanda kurung ditambahkan):

 private void onScreenChange(IScreen prev, IScreen next) { parallaxContainer.ParallaxAmount = ParallaxContainer.DEFAULT_PARALLAX_AMOUNT * (((IOsuScreen)next)?.BackgroundParallaxAmount ?? 1.0f); } 

Bug serupa lainnya:

V3123 [CWE-783] Mungkin '??' Operator bekerja dengan cara yang berbeda dari yang diharapkan. Prioritasnya lebih rendah daripada prioritas operator lain di bagian kirinya. FramedReplayInputHandler.cs 103

 private bool inImportantSection { get { .... return IsImportant(frame) && Math.Abs(CurrentTime - NextFrame?.Time ?? 0) <= AllowedImportantTimeSpan; } } 

Seperti dalam kasus sebelumnya, programmer memiliki asumsi yang salah tentang prioritas operator. Ekspresi asli yang diteruskan ke metode Math.Abs mengevaluasi sebagai berikut:

 (a – b) ?? 0 

Inilah cara memperbaikinya:

 private bool inImportantSection { get { .... return IsImportant(frame) && Math.Abs(CurrentTime – (NextFrame?.Time ?? 0)) <= AllowedImportantTimeSpan; } } 

V3142 [CWE-561] Kode tidak terjangkau terdeteksi. Mungkin saja ada kesalahan. DrawableHoldNote.cs 214

 public override bool OnPressed(ManiaAction action) { if (!base.OnPressed(action)) return false; if (Result.Type == HitResult.Miss) // <= holdNote.hasBroken = true; .... } 

Penganalisa percaya kode penangan OnPressed tidak dapat dijangkau dimulai dengan pernyataan if kedua. Ini mengikuti dari fakta bahwa kondisi pertama selalu benar, yaitu bahwa metode base.OnPressed akan selalu mengembalikan false . Mari kita lihat basisnya. Metode yang ditekan :

 public virtual bool OnPressed(ManiaAction action) { if (action != Action.Value) return false; return UpdateResult(true); } 

Dan sekarang di metode UpdateResult :
 protected bool UpdateResult(bool userTriggered) { if (Time.Elapsed < 0) return false; if (Judged) return false; .... return Judged; } 

Perhatikan bahwa implementasi properti yang dinilai tidak masalah di sini karena logika metode UpdateResult menyiratkan bahwa pernyataan pengembalian terakhir setara dengan yang berikut ini:

 return false; 

Ini berarti metode UpdateResult akan mengembalikan false setiap saat, sehingga mengarah ke masalah kode yang tidak dapat dijangkau sebelumnya di stack.

V3146 [CWE-476] Kemungkinan null dereference dari 'ruleset'. 'FirstOrDefault' dapat mengembalikan nilai nol default. APILegacyScoreInfo.cs 24

 public ScoreInfo CreateScoreInfo(RulesetStore rulesets) { var ruleset = rulesets.GetRuleset(OnlineRulesetID); var mods = Mods != null ? ruleset.CreateInstance() // <= .GetAllMods().Where(....) .ToArray() : Array.Empty<Mod>(); .... } 

Penganalisa percaya panggilan ruleset.CreateInstance () menjadi tidak aman. Sebelum panggilan ini, variabel ruleset diberi nilai sebagai hasil memanggil metode GetRuleset :

 public RulesetInfo GetRuleset(int id) => AvailableRulesets.FirstOrDefault(....); 

Seperti yang Anda lihat, peringatan itu valid karena urutan panggilan menyertakan metode FirstOrDefault , yang dapat mengembalikan nol .

Kesimpulan


Tidak ada banyak bug dalam kode "osu!", Dan itu bagus. Tetapi saya masih merekomendasikan agar penulis memeriksa masalah yang dilaporkan oleh penganalisa. Saya harap ini akan membantu mempertahankan kualitas tinggi dan permainan akan terus membawa sukacita bagi para pemain.

Sebagai pengingat, PVS-Studio adalah pilihan yang baik jika Anda suka mengutak-atik kode sumber. Alat analisis tersedia untuk diunduh di situs web resmi. Hal lain yang saya ingin Anda ingat adalah bahwa pemeriksaan satu kali seperti ini tidak ada hubungannya dengan penggunaan normal analisis statis dalam proses pengembangan nyata. Ini paling efektif hanya jika digunakan secara teratur baik pada server build maupun pada komputer pengembang (ini disebut analisis tambahan). Tujuan utama Anda adalah mencegah bug masuk ke sistem kontrol versi dengan menangkapnya pada tahap pengkodean.

Semoga berhasil, dan tetap kreatif!

Referensi


Ini adalah artikel pertama kami di tahun 2020. Sementara kami membahasnya, berikut adalah tautan ke cek proyek C # yang dilakukan selama setahun terakhir:

Source: https://habr.com/ru/post/id483436/


All Articles