Yazılar · 21.05.26 · 5 dk

C# bellek güvenliği yeniden tanımlanıyor (C# 16 / .NET 11)

C#'ta bellek hatası neden tehlikeli, yeni unsafe kuralları ne anlama geliyor — junior dostu anlatım.

C# bellek güvenliği yeniden tanımlanıyor (C# 16 / .NET 11)

Kaynak (İngilizce orijinal): Improving C# Memory Safety — .NET Blog · Richard Lander · 21 Mayıs 2026

Microsoft, C#'ta bellek güvenliğini yeniden tanımlıyor. unsafe artık sadece "pointer yazabilirsin" demek değil; çağıran tarafa yüklenen bir sözleşme anlamına geliyor. Derleyici, bellek kurallarını bozabilecek her işlemi görünür kılıyor. AI ile üretilen kod arttıkça bu görünürlük code review için kritik hale geliyor.

C# bellek güvenliği Yeni model (C# 16) .NET 11'de preview, .NET 12'de production hedefleniyor. Başlangıçta isteğe bağlı (opt-in); nullable reference types gibi kademeli geçiş planlanıyor.

Güvenli C# zaten ne yapıyor?

Normal C# kodunda runtime ve derleyici birlikte çalışır:

  • Referanslar ya geçerli bir nesneyi, ya null'u, ya da artık geçersiz olanı gösterir.
  • Yeni nesneler varsayılan olarak sıfırlanır.
  • Dizi erişiminde sınır kontrolü yapılır; geçersiz indeks IndexOutOfRangeException fırlatır — yani off-by-one hatasında rastgele bellek okumak yerine kontrollü bir hata alırsınız.

Güvenli kod canlı bellek kuralını yapısal olarak korur: her erişim ayrılmış, başlatılmış ve kullanılabilir olmalıdır. unsafe kod — native kütüphane çağrısı veya performans için — bu kuralı geliştiriciye bırakır. Dilin rolü unsafe kod yazmanıza yardım etmek değil; sınırların nerede olduğunu ve güvenli koda nasıl dönüldüğünü netleştirmektir.

Karayolu benzetmesi

Sarı çizgi sürücü disiplinine dayanır; bariyer ise yapısal ayrım sağlar. Programlamada da "hız" arttıkça (gigabaytlarca bellek erişimi) risk yükselir. Undefined Behavior (UB), yani tanımsız davranış — programın sessizce yanlış sonuç üretmesi veya çökmesi — endüstri güvenlik açıklarının ana kaynağıdır.

C# 16 modeli: dört katman

Yarım uygulama yarım değer verir. Dört katman birlikte çalışınca çağrı grafiği boyunca denetlenebilir bir güvenlik hattı oluşur:

  1. İç unsafe { } bloğu: Pointer okuma, unsafe üye çağrısı gibi her riskli operasyon açıkça bu blok içinde olmalı.
  2. Yayılım (propagation): Metot imzasına unsafe eklerseniz, yükümlülük çağıranlara taşınır — tıpkı "bu metodu çağırmak için sen de unsafe bölgede olmalısın" demek gibi.
  3. /// <safety> dokümantasyonu: Metodun çağırandan ne beklediği yazılı olarak belgelenir; analyzer eksikliği işaretleyebilir.
  4. Sınırda bastırma (suppression): İçeride unsafe var ama imza unsafe değilse, metot güvenli bir API yüzeyi sunar — runtime kontrolü, statik akıl yürütme veya üst katmandaki invariant ile yükümlülük "boşaltılır".

C# 1.0'dan farklar

  • unsafe artık tipe değil, metot/özellik/field seviyesine iner.
  • Static constructor ve finalizer'da unsafe yasak.
  • new() constraint yalnızca güvenli parametresiz ctor kabul eder.
  • Yeni safe keyword: extern/LibraryImport için açık seçim zorunlu.
  • unsafe imza artık unsafe context kurmaz; çağrı noktasında iç blok şart.
  • Pointer imzada tek başına unsafety yaymaz; dereference unsafe'dir. Yeni kodda IntPtr yerine byte* tercih edin; opak handle için SafeHandle.

Örnek: iyi ve kötü unsafe API

Encoding.GetString(byte*, int) iyi örnek: pointer + uzunluk çifti net sözleşme; null/negatif guard'lar; dönüş yeni string — buffer yaşam süresi sorunu kalmaz. Çağıranın tek yükümlülüğü: byteCount kadar byte okunabilir olmalı.

Marshal.ReadByte(IntPtr, int) uyarıcı örnek: bugün güvenli koddan çağrılabiliyor; IntPtr yayılım kuralını atlıyor — "pointer gizlice taşıma". try/catch yalnızca exception tipini değiştirir; gerçek doğrulama yok.

Yeni modelde ReadByte imzası unsafe kalır; okuma tek satırda iç unsafe bloğuna alınır; /// <safety> okuma iznini yazar. İhlaller uyarı değil, derleme hatası olur.

Yayılım ve bastırma

C# 1.0'da unsafe void M() { } tüm metodu unsafe context yapıyordu; pointer imzada yoksa çağıran töreni görmezdi — convention'a dayalı, denetlenemez bir sistem.

C# 16'da iki net yol var:

  • Yayılım: Dış unsafe + iç unsafe { M(); } — yükümlülük yukarı taşınır.
  • Bastırma: Guard ile yükümlülük boşaltılır, imza güvenli kalır — örneğin null kontrolü yapıp sonra unsafe işlemi içeride yapmak.

Cross-assembly davranış proje opt-in'ine bağlı: yeni model açık çağıran + yeni callee → yeni kurallar; legacy callee → uyumluluk modu; legacy caller + yeni callee → yeni marker'lar zorlanmaz. Runtime kütüphaneleri opt-in olacak. Derleme zamanı only; runtime performans etkisi yok.

Proje seviyesi opt-in

İki bağımsız anahtar:

  1. Yeni güvenlik modeli property (.NET 11 preview ile isim netleşecek) — kapalıyken C# 1.0 kuralları.
  2. Mevcut AllowUnsafeBlocks — varsayılan false; tüm unsafe görünümünü kapılar.

En güvenli kombinasyon: yeni model açık, AllowUnsafeBlocks kapalıMarshal.ReadByte derleme aşamasında engellenir.

dotnet format fixer çağrı noktalarına unsafe { } sarmayı otomatik yapabilir; /// <safety> yazamaz. AllowUnsafeBlocks=false ile derleyici unsafe kodu reddeder — diff review'dan daha verimli.

Safety documentation

"Unsafe" kelimesi "güvenlikleri kapat" demek; derleyici doğrulayamaz, yükümlülük geliştiricide kalır.

  • /// <safety>: Caller contract — public API yüzeyinde, çağırandan ne beklendiğini yazar.
  • // SAFETY:: İç not — hangi varsayıma dayanıldığı; derleyici okumaz.

ReadByte için IntPtr durumları: Zero → NRE; unmapped → donanım hatası; yanlış sahiplik → sessizce rastgele byte (klasik UB); doğru pointer → çalışır. Sözleşme görünür olmadan çağıran koruyamaz.

Safety guards

Dokümantasyon yükümlülükleri adlandırır; guard'lar boşaltır. String.CopyTo örneği: her ThrowIf* bir invariant korur — ThrowIfNull, negatif count, indeks sınırı. Guard'lar birleşir; bir link değişirse akıl yürütme yeniden yapılmalı.

ThrowIf* = UB yerine kontrollü crash. Unsafe boundary method: imza güvenli, içeride unsafety bastırılmış — code review buradan başlar.

Unsafe fields

Field unsafe olmalı when declared type invariant'ı taşımıyor:

NativeBuffer mockup: unsafe byte* _ptr + sabit Length; ctor'da NativeMemory.Alloc; ReadAt bounds + disposed check; Dispose free + null. Field marker tüm yazma noktalarını gözden geçirilebilir kılar.

ArrayWrapper<T>: readonly unsafe Array _array — runtime her zaman T[] tutar; Unsafe.As<T[]> ile okuma. Motivasyon aynı: invariant tip sisteminde görünmüyor.

Kurallar: yazma birincil motivasyon; readonly unsafe = sözleşme + built-in guard; private serbest geçiş değil — member-to-member de sözleşme yüzeyi.

Migration walkthrough (özet)

Runtime kütüphanelerinde pattern: unsafe API → caller → inline discharge mi propagation mı?

NativeMemory.Allocsafe (pointer tutmak unsafe değil; OOM throw eder). NativeMemory.Freeunsafe + <safety>: Alloc'tan gelmiş, henüz free edilmemiş, alias yok.

FileVersionInfo constructor mockup: scratch buffer allocation inner unsafe'da; parse metodu propagation veya boundary kararı. Her caller potansiyel boundary.

(Model henüz finalize değil; örnekler yön gösterici.)

AI çağında neden önemli?

Bellek güvenliği endüstri ve kamu önceliği; AI kod üretimi review kapasitesini aşıyor. unsafe artık "uzman convention" değil — grep ile audit edilebilir, derleyici zorunlu sözleşme. Backend ekibi olarak interop path'lerinde PR checklist: /// <safety>, inner block, AllowUnsafeBlocks property, propagation vs suppression kararı.

Özet

C# 16, unsafe kodu görünür ve denetlenebilir kılar: iç blok, imza yayılımı, <safety> dokümantasyonu ve sınırda bastırma birlikte çalışır. Tam kod örnekleri ve migration PR listesi orijinal yazıda.

C# bellek güvenliği yeniden tanımlanıyor (C# 16 / .NET 11) — Aziz Osmanoğlu