Poznámka:
Přístup k této stránce vyžaduje autorizaci. Můžete se zkusit přihlásit nebo změnit adresáře.
Přístup k této stránce vyžaduje autorizaci. Můžete zkusit změnit adresáře.
Tento článek obsahuje podrobná doporučení pro konkrétní nebezpečné vzory, rizika, která zahrnují, a postup zmírnění těchto rizik. Tyto pokyny cílí na všechny vývojáře, kteří píší nebo kontrolují nebezpečný kód v jazyce C#. Jiné jazyky .NET, jako je F# a Visual Basic, jsou mimo rozsah tohoto článku, i když některá doporučení se můžou v těchto jazycích použít i.
Glosář
- AVE – Výjimka porušení přístupu
- Byref – spravovaný ukazatel (
ref T t), který je podobný nespravovanému ukazateli, ale sleduje GC. Obvykle ukazuje na libovolné části objektů nebo zásobníku. Odkaz je efektivně řízený ukazatel s posunem +0. - CVE – Veřejně zpřístupněná ohrožení zabezpečení kybernetické bezpečnosti
- JIT – kompilátor 'just-in-time' (RyuJIT v CoreCLR a NativeAOT).
- PGO – Optimalizace na základě profilu
- Nespravovaný ukazatel (nebo nezpracovaný ukazatel) – ukazatel (
T* p), který odkazuje na libovolné umístění paměti a není spravován nebo sledován GC.
Další termíny najdete v glosáři modulu runtime .NET.
Běžné nespolehlivé vzory
Jazyk C# poskytuje bezpečné prostředí, ve kterém se vývojáři nemusí starat o interní fungování modulu runtime a GC. Nebezpečný kód umožňuje obejít tyto bezpečnostní kontroly a potenciálně zavést nespolehlivé vzory, které můžou vést k poškození paměti. I když tyto vzory můžou být užitečné v určitých scénářích, měli byste je používat s opatrností a pouze v případě, že je to naprosto nezbytné. Nejen že jazyk C# a .NET neposkytují nástroje pro ověření zvuku nebezpečného kódu (jak mohou poskytovat různé sanitizátory C/C++), chování specifické pro GC může představovat další rizika v nebezpečném jazyce C# nad rámec těch, které můžou tradiční vývojáři C/C++ znát.
Nebezpečný kód pro spravované odkazy by měl být napsán s následujícími konzervativními předpoklady:
- GC může přerušit provádění jakékoli metody v libovolném okamžiku v libovolném okamžiku instrukce.
- GC může přesouvat objekty v paměti a aktualizovat všechny sledované odkazy.
- GC přesně ví, kdy už odkazy nepotřebujete.
Klasický příklad poškození haldy nastane, když GC ztratí stopu odkazu na objekt nebo považuje neplatné ukazatele za odkazy haldy. To často vede k ne deterministickým chybám nebo poškození paměti. Chyby poškození haldy jsou zvláště náročné diagnostikovat a reprodukovat, protože:
- Tyto problémy můžou zůstat skryté po dlouhou dobu a projeví se pouze po nesouvisející změně kódu nebo aktualizaci prostředí runtime.
- Často vyžadují přesné načasování pro reprodukci, jako je přerušení provádění GC v určitém umístění a počáteční komprimace haldy, což je vzácná a nedetermintická událost.
Následující části popisují běžné nebezpečné vzorce s doporučeními ✔️ Dělat a ❌ NEDĚLAT.
1. Nesledované spravované ukazatele (Unsafe.AsPointer a podobné)
Spravovaný (sledovaný) ukazatel není možné převést na nespravovaný (nesledovaný) ukazatel v bezpečném jazyce C#. Pokud taková potřeba nastane, může být lákavé použít Unsafe.AsPointer<T>(T) k ušetření režijních nákladů fixed oproti použití příkazu. I když existují platné případy použití, představuje riziko vytvoření nesledovaných ukazatelů na přesunoutelné objekty.
Příklad:
unsafe void UnreliableCode(ref int x)
{
int* nativePointer = (int*)Unsafe.AsPointer(ref x);
nativePointer[0] = 42;
}
Pokud GC přeruší provádění metody UnreliableCode hned po přečtení ukazatele (adresa odkazovaná x) a přemístí odkazovaný objekt, GC správně aktualizuje polohu uloženou v x, ale nativePointer nezohlední a neaktualizuje hodnotu, kterou obsahuje. V tomto okamžiku je zápis do nativePointer zápisem do libovolné paměti.
unsafe void UnreliableCode(ref int x)
{
int* nativePointer = (int*)Unsafe.AsPointer(ref x);
// <-- GC happens here between the two lines of code and updates `x` to point to a new location.
// However, `nativePointer` still points to the old location as it's not reported to the GC
nativePointer[0] = 42; // Potentially corrupting write, access violation, or other issue.
}
Jakmile GC obnoví provádění metody, zapíše 42 do starého umístění x, což může vést k neočekávané výjimce, rozsáhlému poškození globálního stavu nebo ukončení procesu kvůli porušení přístupu.
Doporučeným řešením je místo toho použít fixed klíčové slovo a & operátor adresace, aby se zajistilo, že GC nemůže po dobu trvání operace přemístit cílový odkaz.
unsafe void ReliableCode(ref int x)
{
fixed (int* nativePointer = &x) // `x` cannot be relocated for the duration of this block.
{
nativePointer[0] = 42;
}
}
Recommendations
-
❌ Nepoužívejte
ref Xargumenty s implicitním kontraktem, žeXje vždy alokovaný na zásobníku, připnutý nebo jinak nepřemístitelný pomocí GC. Totéž platí pro prosté objekty a Spans – nezavádět nezřejmé kontrakty založené na volajících o jejich životnosti ve signaturách metod. Zvažte místo toho použití argumentu referenční struktura nebo změňte argument na typ ukazatele „raw“ (X*). - ❌ Nepoužívejte ukazatel z Unsafe.AsPointer<T>(T), pokud může přežít původní objekt, na který ukazuje. Podle dokumentace rozhraní API je na volajícím Unsafe.AsPointer<T>(T) zajistit, že GC nemůže odkaz přemístit. Ujistěte se, že je pro code reviewery jasně viditelné, že volající splnil potřebné podmínky.
- ✔️ Použijte GCHandle nebo
fixedmísto Unsafe.AsPointer<T>(T) k definování explicitních oborů pro nespravované ukazatele a ujistěte se, že je objekt vždy připnutý. - ✔️ Pokud potřebujete zarovnat pole na určitou hranici, použijte nespravované ukazatele (s
fixed) místo byrefs. Tím zajistíte, že GC nepřemístí objekt a zneplatní všechny předpoklady zarovnání, na které by vaše logika mohla spoléhat.
2. Exponování ukazatelů mimo oblast fixed rozsahu
I když pevné klíčové slovo definuje obor pro ukazatel získaný z připnutého objektu, je stále možné, aby tento ukazatel utekl fixed oboru a zavedl chyby, protože jazyk C# neposkytuje žádnou ochranu vlastnictví a životního cyklu.
Typickým příkladem je následující fragment kódu:
unsafe int* GetPointerToArray(int[] array)
{
fixed (int* pArray = array)
{
_ptrField = pArray; // Bug!
Method(pArray); // Bug if `Method` allows `pArray` to escape,
// perhaps by assigning it to a field.
return pArray; // Bug!
// And other ways to escape the scope.
}
}
V tomto příkladu je pole správně připnuto pomocí klíčového slova fixed (aby GC nemohl přemístit pole v rámci bloku fixed), ale ukazatel je vystaven mimo fixed blok. Tím se vytvoří zpožděný ukazatel, jejichž dereference povede k nedefinovanému chování.
Recommendations
- ✔️ Ujistěte se, že ukazatele v
fixedblocích neopustují definovaný obor. - ✔️ Upřednostňujte bezpečné primitivy nízké úrovně s integrovanou analýzou úniku, jako je ref struktura jazyka C#. Další informace naleznete v tématu Vylepšení struktury nízké úrovně.
3. Podrobnosti interní implementace modulu runtime a knihoven
Při přístupu k interním podrobnostem implementace nebo spoléhání se na ně, což je obecně špatná praxe a není podporováno rozhraním .NET, je třeba zmínit konkrétní běžně pozorované případy. Nejedná se o vyčerpávající seznam všech možných věcí, které by se mohly pokazit, když kód nevhodně spoléhá na interní podrobnosti implementace.
Recommendations
❌ Neměňte ani nečtěte žádné části záhlaví objektu.
- Záhlaví objektů se můžou v různých runtime prostředích lišit.
- V CoreCLR nelze bezpečně přistupovat k hlavičce objektu bez toho, abyste nejprve připnuli objekt.
- Typ objektu nikdy nezměníte úpravou ukazatele MethodTable.
❌ Neukládejte žádná data do odsazení objektu. Nepředpokládejte, že obsah vyrovnání zůstane zachován, nebo že vyrovnání bude ve výchozím nastavení vždy vynulováno.
❌ Nedělejte předpoklady o velikostech a posunech čehokoli jiného než primitiv a struktur se sekvenčním nebo explicitním rozložením. Výjimky však existují, například když jsou zapojeny držáky GC.
❌ Nevyvolávejte neveřejné metody, nepřistupujte k neveřejným polím ani neměňte pole pouze pro čtení u typů BCL pomocí reflexe nebo nebezpečného kódu.
❌ Neberte jako dané, že jakýkoli neveřejný člen v rámci BCL bude vždy přítomen nebo bude mít specifickou podobu. Tým .NET občas upravuje nebo odebírá neveřejné rozhraní API v rámci servisních verzí.
❌ Neměňte
static readonlypole pomocí reflexe nebo nebezpečného kódu, protože se předpokládá, že jsou konstantní. Například RyuJIT je obvykle vylineuje jako explicitní konstanty.❌ Nepředpokládejte jednoduše, že odkaz nelze přemístit. Tyto pokyny platí pro řetězcové a UTF-8 (
"..."u8) literály, statická pole, pole RVA, objekty LOH atd.- Jedná se o podrobnosti implementace runtime, které mohou platit pro některé runtime, ale ne pro jiné.
- Nespravované ukazatele na takové objekty nemusí zastavit uvolnění sestavení, což způsobí, že se ukazatele změní na dangling. K zajištění správnosti použijte
fixedoblasti.
ReadOnlySpan<int> rva = [1, 2, 4, 4]; int* p = (int*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(rva)); // Bug! The assembly containing the RVA field might be unloaded at this point // and `p` becomes a dangling pointer. int value = p[0]; // Access violation or other issue.❌ Nezapisujte kód, který závisí na podrobnostech implementace konkrétního modulu runtime.
4. Neplatné spravované ukazatele (i když se nikdy nepřesouvají)
Některé kategorie kódu nakonec spoléhají na manipulaci s ukazateli a aritmetiku, a takový kód má často na výběr mezi použitím nespravovaných ukazatelů (T* p) a spravovaných ukazatelů (ref T p).
Tyto ukazatele lze manipulovat libovolně, například prostřednictvím operátorů na nespravovaných ukazatelích (p++) a prostřednictvím Unsafe metod spravovaných ukazatelů (p = ref Unsafe.Add(ref p, 1)). Oba se považují za nebezpečný kód a je možné vytvořit nespolehlivé vzory s oběma způsoby. U některých algoritmů ale může být při manipulaci se spravovanými ukazateli snazší neúmyslně vytvářet vzory GC-nebezpečné. Vzhledem k tomu, že nespravované ukazatele nejsou sledovány pomocí GC, je hodnota, kterou obsahují, relevantní pouze tehdy, když je dereferencována v kódu vývojáře. Naproti tomu je hodnota spravovaného ukazatele relevantní nejen tehdy, když je dereferencován kódem vývojáře, ale také když ji zkoumá GC. Vývojář proto může vytvořit neplatné nespravované ukazatele bez výsledku, pokud nejsou odvozeny, ale vytvoření jakéhokoli neplatného spravovaného ukazatele je chyba. Příklad:
unsafe void UnmanagedPointers(int[] array)
{
fixed (int* p = array)
{
int* invalidPtr = p - 1000;
// invalidPtr is pointing to an undefined location in memory
// it's ok as long as it's not dereferenced.
int* validPtr = invalidPtr + 1000; // Returning back to the original location
*validPtr = 42; // OK
}
}
Podobný kód používající byrefs (spravované ukazatele) je však neplatný.
void ManagedPointers_Incorrect(int[] array)
{
ref int invalidPtr = ref Unsafe.Add(ref array[0], -1000); // Already a bug!
ref int validPtr = ref Unsafe.Add(ref invalidPtr, 1000);
validPtr = 42; // possibly corrupting write
}
I když se zde spravovaná implementace vyhne menší režii připnutí, je to neuvážené, protože invalidPtrse může stát externím ukazatelem, zatímco skutečná adresa array[0] je aktualizována modulem Garbage Collector (GC).
Tyto chyby jsou drobné a dokonce i .NET na ně během vývoje narazil.
Recommendations
-
❌ Nevytvářejte neplatné spravované ukazatele, i když nejsou dereferencovány nebo jsou umístěny uvnitř nikdy nespouštěných větví kódu.
- Další informace o tom, co představuje platný spravovaný ukazatel, viz ECMA-335, s. II.14.4.2 Spravované ukazatele; a dodatek specifikace ROZHRANÍ příkazového řádku ECMA-335, s. II.14.4.2.
- ✔️ Pokud algoritmus vyžaduje takové operace, použijte připnuté nespravované ukazatele.
5. Reinterpretovat přetypování typu
I když jsou všechny druhy přetypování z struktury na třídu nebo z třídy na strukturu podle definice nedefinovaným chováním, je také možné narazit na nespolehlivé vzory při převodech z struktury na strukturu nebo z třídy na třídu. Typickým příkladem nespolehlivého vzoru je následující kód:
struct S1
{
string a;
nint b;
}
struct S2
{
string a;
string b;
}
S1 s1 = ...
S2 s2 = Unsafe.As<S1, S2>(ref s1); // Bug! A random nint value becomes a reference reported to the GC.
A i když je rozložení podobné, při odkazech GC (pole) byste měli být opatrní.
Recommendations
- ❌ Nepřetypovávejte struktury na třídy nebo naopak.
-
❌ Nepoužívejte
Unsafe.Aspro konverze struktura na strukturu nebo třída na třídu, pokud si nejste zcela jisti, že přetypování je legální. Další informace najdete v části Poznámky vUnsafe.Asdokumentaci k rozhraní API. - ✔️ Preferujte bezpečnější kopírování polí po polích, externí knihovny, jako jsou AutoMapper nebo Zdrojové generátory pro takové převody.
- ✔️ Preferujte
Unsafe.BitCastpředUnsafe.As, protožeBitCastposkytuje některé základní kontroly použití. Všimněte si, že tyto kontroly neposkytují záruky úplné správnosti, což znamená, žeBitCastse stále považuje za nebezpečné rozhraní API.
6. Obejití bariéry zápisu a neatomické operace s odkazy na GC
Za normálních okolností jsou všechny typy zápisů nebo čtení odkazů GC vždy atomické. Také všechny pokusy o přiřazení reference GC (nebo byref na strukturu s poli GC) k potenciálnímu umístění haldy procházejí bariérou zápisu, která zajišťuje, že GC ví o nových propojeních mezi objekty. Nebezpečný kód nám ale umožňuje tyto záruky obejít a zavést nespolehlivé vzory. Příklad:
unsafe void InvalidCode1(object[] arr1, object[] arr2)
{
fixed (object* p1 = arr1)
fixed (object* p2 = arr2)
{
nint* ptr1 = (nint*)p1;
nint* ptr2 = (nint*)p2;
// Bug! We're assigning a GC pointer to a heap location
// without going through the Write Barrier.
// Moreover, we also bypass array covariance checks.
*ptr1 = *ptr2;
}
}
Podobně je následující kód se spravovanými ukazateli také nespolehlivý:
struct StructWithGcFields
{
object a;
int b;
}
void InvalidCode2(ref StructWithGcFields dst, ref StructWithGcFields src)
{
// It's already a bad idea to cast a struct with GC fields to `ref byte`, etc.
ref byte dstBytes = ref Unsafe.As<StructWithGcFields, byte>(ref dst);
ref byte srcBytes = ref Unsafe.As<StructWithGcFields, byte>(ref src);
// Bug! Bypasses the Write Barrier. Also, non-atomic writes/reads for GC references.
Unsafe.CopyBlockUnaligned(
ref dstBytes, ref srcBytes, (uint)Unsafe.SizeOf<StructWithGcFields>());
// Bug! Same as above.
Vector128.LoadUnsafe(ref srcBytes).StoreUnsafe(ref dstBytes);
}
Recommendations
- ❌ Nepoužívejte ne atomické operace u odkazů na GC (například operace SIMD je často neposkytují).
- ❌ Nepoužívejte nespravované ukazatele k ukládání odkazů GC do umístění haldy (bez bariéry zápisu).
7. Předpoklady o životnosti objektů (finalizátory, GC.KeepAlive)
Vyhněte se vytváření předpokladů o životnosti objektů z hlediska GC. Konkrétně nepředpokládáte, že objekt je stále aktivní, když nemusí být. Životnost objektů se může lišit v různých modulech runtime nebo dokonce mezi různými vrstvami stejné metody (Vrstva0 a Vrstva1 v RyuJIT). Finalizátory jsou běžným scénářem, kdy mohou být podobné předpoklady nesprávné.
public class MyClassWithBadCode
{
public IntPtr _handle;
public void DoWork() => DoSomeWork(_handle); // A use-after-free bug!
~MyClassWithBadCode() => DestroyHandle(_handle);
}
// Example usage:
var obj = new MyClassWithBadCode()
obj.DoWork();
V tomto příkladu může být DestroyHandle zavolána ještě před tím, než DoWork dokončí nebo dokonce než začne.
Proto není nutné předpokládat, že objekty, například this, zůstanou aktivní až do konce metody.
void DoWork()
{
// A pseudo-code of what might happen under the hood:
IntPtr reg = this._handle;
// 'this' object is no longer alive at this point.
// <-- GC interrupts here, collects the 'this' object, and triggers its finalizer.
// DestroyHandle(_handle) is called.
// Bug! 'reg' is now a dangling pointer.
DoSomeWork(reg);
// You can resolve the issue and force 'this' to be kept alive (thus ensuring the
// finalizer will not run) by uncommenting the line below:
// GC.KeepAlive(this);
}
Proto se doporučuje explicitně prodloužit životnost objektů používajících GC.KeepAlive(Object) nebo SafeHandle.
Dalším klasickým výskytem tohoto problému je Marshal.GetFunctionPointerForDelegate<TDelegate>(TDelegate) rozhraní API:
var callback = new NativeCallback(OnCallback);
// Convert delegate to function pointer
IntPtr fnPtr = Marshal.GetFunctionPointerForDelegate(callback);
// Bug! The delegate might be collected by the GC here.
// It should be kept alive until the native code is done with it.
RegisterCallback(fnPtr);
Recommendations
-
❌ Nedělejte předpoklady o životnosti objektů. Nikdy například nepředpokládáme
this, že je vždy aktivní až do konce metody. - ✔️ Používejte SafeHandle ke správě nativních prostředků.
- ✔️ GC.KeepAlive(Object) Použijte k prodloužení životnosti objektů v případě potřeby.
8. Přístup mezi vlákny k místním proměnným
Přístup k místním proměnným z jiného vlákna se obecně považuje za chybný postup. Při zapojení spravovaných odkazů se však stane explicitně nedefinovaným chováním, jak je uvedeno v modelu paměti .NET.
Příklad: Struktura obsahující odkazy GC může být vynulována nebo přepsána nezabezpečeným vůči více vláknům způsobem v oblasti bez GC, zatímco jiné vlákno ji čte, což vede k nedefinovanému chování.
Recommendations
- ❌ Nepřistupujte k místním prostředím napříč vlákny (zejména pokud obsahují odkazy na GC).
- ✔️ Místo toho použijte haldu nebo nespravovanou paměť (například NativeMemory.Alloc).
9. Odebrání kontroly nebezpečných hranic
V jazyce C# všechny idiomatické přístupy k paměti standardně zahrnují kontroly hranic. Kompilátor JIT může tyto kontroly odebrat, pokud může prokázat, že jsou zbytečné, jako v následujícím příkladu.
int SumAllElements(int[] array)
{
int sum = 0;
for (int i = 0; i < array.Length; i++)
{
// The JIT knows that within this loop body, i >= 0 and i < array.Length.
// The JIT can reason that its own bounds check would be duplicative and
// unnecessary, so it opts not to emit the bounds check into the final
// generated code.
sum += array[i];
}
}
I když JIT neustále vylepšuje při rozpoznávání takových vzorů, stále existují scénáře, kdy ponechá kontroly na místě, což může mít vliv na výkon v kritickém kódu. V takových případech můžete být lákaví použít nebezpečný kód k ručnímu odebrání těchto kontrol, aniž byste plně porozuměli rizikům nebo přesně vyhodnocovali výhody výkonu.
Představte si například následující metodu.
int FetchAnElement(int[] array, int index)
{
return array[index];
}
Pokud JIT nemůže prokázat, že index je vždy právně v mezích array, přepíše metodu tak, aby vypadala přibližně takto.
int FetchAnElement_AsJitted(int[] array, int index)
{
if (index < 0 || index >= array.Length)
throw new IndexOutOfBoundsException();
return array.GetElementAt(index);
}
Pokud chcete snížit režijní náklady z této kontroly v horkém kódu, může být lákavé používat nebezpečná ekvivalentní rozhraní API (Unsafe a MemoryMarshal):
int FetchAnElement_Unsafe1(int[] array, int index)
{
// DANGER: The access below is not bounds-checked and could cause an access violation.
return Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(array), index);
}
Nebo použijte připínání a surové ukazatele:
unsafe int FetchAnElement_Unsafe2(int[] array, int index)
{
fixed (int* pArray = array)
{
// DANGER: The access below is not bounds-checked and could cause an access violation.
return pArray[index];
}
}
To může vést k náhodnému chybovému ukončení nebo poškození stavu, pokud index je mimo hranice array.
Takové nebezpečné transformace můžou mít u velmi horkých cest výhody výkonu, ale tyto výhody jsou často přechodné, protože každá vydaná verze .NET zlepšuje schopnost JIT eliminovat zbytečné kontroly hranic, pokud je to bezpečné.
Recommendations
- ✔️ Ověřte, jestli nejnovější verze rozhraní .NET stále nemůže odstranit kontrolu hranic. Pokud ano, přepište ho pomocí bezpečného kódu. V opačném případě nahlaste problém s RyuJIT. Tento problém se sledováním použijte jako dobrý výchozí bod.
- ✔️ Změřte dopad skutečného výkonu. Pokud je zvýšení výkonu zanedbatelné nebo není kód často spouštěn mimo triviální mikrobenchmark, přepište ho pomocí bezpečného kódu.
- ✔️ POSKYTUJTE další rady JIT, jako jsou ruční kontroly hranic před smyčkami a ukládání polí do lokálních proměnných, protože .NET Model paměti může konzervativně zabránit JIT v odebrání kontrol hranic v některých scénářích.
- ✔️ Ochrana kódu pomocí kontroly hranic
Debug.Assert, pokud je stále nutný nebezpečný kód. Podívejte se na následující příklad.
Debug.Assert(array is not null);
Debug.Assert((index >= 0) && (index < array.Length));
// Unsafe code here
Tyto kontroly můžete dokonce refaktorovat na opakovaně použitelné pomocné metody.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static T UnsafeGetElementAt<T>(this T[] array, int index)
{
Debug.Assert(array is not null);
Debug.Assert((index >= 0) && (index < array.Length));
return Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(array), index);
}
Debug.Assert Zahrnutí neposkytuje žádné kontroly správnosti pro produkční sestavení, ale může pomoci odhalit potenciální chyby v ladicích sestaveních.
10. Spojování přístupu k paměti
Možná budete chtít použít nebezpečný kód ke zkvalitnění přístupu k paměti za účelem zlepšení výkonu.
Klasický příklad je následující kód pro zápis "False" do pole znaků:
// Naive implementation
static void WriteToDestination_Safe(char[] dst)
{
if (dst.Length < 5) { throw new ArgumentException(); }
dst[0] = 'F';
dst[1] = 'a';
dst[2] = 'l';
dst[3] = 's';
dst[4] = 'e';
}
// Unsafe coalesced implementation
static void WriteToDestination_Unsafe(char[] destination)
{
Span<char> dstSpan = destination;
if (dstSpan.Length < 5) { throw new ArgumentException(); }
ulong fals_val = BitConverter.IsLittleEndian ? 0x0073006C00610046ul : 0x00460061006C0073ul;
MemoryMarshal.Write(MemoryMarshal.AsBytes(dstSpan.Slice(0, 4)), in fals_val); // Write "Fals" (4 chars)
dstSpan[4] = 'e'; // Write "e" (1 char)
}
V předchozích verzích .NET byla nebezpečná verze používající MemoryMarshal měřitelně rychlejší než jednoduchá bezpečná verze. Moderní verze .NET však obsahují mnohem vylepšenou JIT, která v obou případech vytváří ekvivalentní kódgen. Od verze .NET 10 je x64 codegen následující:
; WriteToDestination_Safe
cmp eax, 5
jl THROW_NEW_ARGUMENTEXCEPTION
mov rax, 0x73006C00610046
mov qword ptr [rdi+0x10], rax
mov word ptr [rdi+0x18], 101
; WriteToDestination_Unsafe
cmp edi, 5
jl THROW_NEW_ARGUMENTEXCEPTION
mov rdi, 0x73006C00610046
mov qword ptr [rax], rdi
mov word ptr [rax+0x08], 101
Existuje ještě jednodušší a čitelnější verze kódu:
"False".CopyTo(dst);
Od verze .NET 10 toto volání vytvoří identický kódgen, jak je uvedeno výše. Má dokonce další výhodu: naznačuje JITu, že přísné zápisy pro každý prvek nemusí být atomické. JIT může tuto nápovědu zkombinovat s dalšími kontextovými znalostmi, aby poskytovala ještě více optimalizací nad rámec toho, co jsme zde probírali.
Recommendations
- ✔️ Preferujte idiomatický bezpečný kód místo nebezpečného pro spojení při přístupu k paměti.
- Upřednostňujte
Span<T>.CopyToaSpan<T>.TryCopyTopro kopírování dat. - Preferujte použití
String.EqualsaSpan<T>.SequenceEqualk porovnávání dat (i když používáteStringComparer.OrdinalIgnoreCase). - Preferujte
Span<T>.Fillpro vyplňování dat aSpan<T>.Clearpro vymazávání dat. - Mějte na paměti, že JIT může automaticky sloučit zápisy a čtení na úrovni jednotlivých prvků nebo polí.
- Upřednostňujte
- ✔️ Pokud napíšete idiomaticický kód a zjistíte, že není optimalizovaný podle očekávání, zapište problém s dotnet/runtime .
- ❌ Neslučujte přístupy k paměti ručně, pokud si nejste jistí riziky nesprávně zarovnaného přístupu k paměti, zárukami atomicity nebo souvisejícími výhodami výkonu.
11. Nerovnaný přístup k paměti
Koalescence přístupu k paměti popsaná v koalescenci přístupu k paměti často vede k explicitnímu nebo implicitnímu nevyrovnanému čtení a zápisu. I když to obvykle nezpůsobuje vážné problémy (kromě potenciálních sankcí za výkon kvůli překročení mezipaměti a hranic stránek), stále představuje určitá skutečná rizika.
Představte si například scénář, ve kterém vymažete dva prvky pole najednou:
uint[] arr = _arr;
arr[i + 0] = 0;
arr[i + 1] = 0;
Řekněme, že předchozí hodnoty v těchto umístěních byly obě uint.MaxValue (0xFFFFFFFF).
Model paměti .NET zaručuje, že oba zápisy jsou atomické, takže všechna ostatní vlákna v procesu budou sledovat pouze novou hodnotu nebo starou hodnotu 00xFFFFFFFF, nikdy "roztrhané" hodnoty jako 0xFFFF0000.
Předpokládejme však, že následující nebezpečný kód se používá k obejití kontroly mezí a vynulování obou prvků jediným 64bitovým zápisem:
ref uint p = ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(arr), i);
Unsafe.WriteUnaligned<ulong>(ref Unsafe.As<uint, byte>(ref p), 0UL);
Tento kód má vedlejší účinek ztráty záruky atomicity. Roztržené hodnoty můžou být pozorovány jinými vlákny, což vede k nedefinovanému chování. Aby byl takový sloučený zápis atomický, musí být paměť zarovnaná s velikostí zápisu (v tomto případě 8 bajtů). Pokud se pokusíte ručně zarovnat paměť před operací, je nutné zvážit, že GC může kdykoli přemístit (a tím efektivně změnit zarovnání) pole, pokud není připnuto. Další podrobnosti najdete v dokumentaci k modelu paměti .NET .
Dalším rizikem nerovného přístupu k paměti je potenciál chybového ukončení aplikace v určitých scénářích. I když se některé moduly .NET runtime spoléhají na operační systém pro opravu nesprávně zarovnaných přístupů, stále existují určité scénáře na některých platformách, kde může chybně zarovnaný přístup vést k DataMisalignedException (nebo SEHException). Mezi příklady patří:
-
Interlockedoperace s chybně zarovnanou pamětí na některých platformách (například). - Nesprávně zarovnané operace s plovoucí řádovou čárkou na platformě ARM
- Přístup ke speciální paměti zařízení s určitými požadavky na zarovnání (není ve skutečnosti podporován rozhraním .NET).
Recommendations
- ❌ Nepoužívejte nerovné přístupy k paměti v algoritmech bez zámků a dalších scénářích, ve kterých je atomicita důležitá.
- ✔️ V případě potřeby data zarovnejte ručně, ale mějte na paměti, že GC může objekty kdykoli přemístit a efektivně měnit zarovnání dynamicky. To je zvlášť důležité pro různá
StoreAligned/LoadAlignedrozhraní API v SIMD. - ✔️ Používejte explicitní nerovná rozhraní API pro čtení a zápis, například Unsafe.ReadUnaligned/Unsafe.WriteUnaligned, místo zarovnaných rozhraní API, jako jsou Unsafe.Read<T>(Void*)/Unsafe.Write<T>(Void*, T) nebo Unsafe.As<TFrom,TTo>(TFrom), pokud by data mohla být nesprávně zarovnaná.
- ✔️ Mějte na paměti, že různá rozhraní API pro manipulaci s pamětí, jako Span<T>.CopyTo(Span<T>), také neposkytují záruky atomicity.
- ✔️ Další podrobnosti o zárukách atomicity najdete v dokumentaci k modelu paměti .NET (viz odkazy).
- ✔️ Změřte výkon napříč všemi vašimi cílovými platformami, protože na některých platformách může nevyrovnaný přístup k paměti výrazně snížit výkon. Na těchto platformách můžete zjistit, že kód naïve funguje lépe než chytrý kód.
- ✔️ Mějte na paměti, že existují scénáře a platformy, u kterých by nerovnaný přístup k paměti mohl vést k výjimce.
12. Binární (de)serializace struktur s vyplňováním nebo neblittovatelnými členy
Při kopírování nebo čtení struktur do nebo z polí bajtů buďte opatrní, když používáte různá rozhraní API podobná serializaci.
Pokud struktura obsahuje výplně nebo neprůhledné členy (například bool nebo pole GC), pak klasické nebezpečné operace s pamětí, jako Fill, CopyTo a SequenceEqual, mohou neúmyslně kopírovat citlivá data ze zásobníku do výplní nebo mohou při porovnání považovat za významná data, která vedou k zřídkakdy reprodukovatelným chybám. Běžný anti-vzor může vypadat takto:
T UnreliableDeserialization<TObject>(ReadOnlySpan<byte> data) where TObject : unmanaged
{
return MemoryMarshal.Read<TObject>(data); // or Unsafe.ReadUnaligned
// BUG! TObject : unmanaged doesn't guarantee that TObject is blittable and contains no paddings.
}
Jediným správným přístupem je použití načítání a ukládání polí specializovaných pro každý TObject vstup (nebo zobecněný pomocí reflexe, generátorů kódu nebo knihoven pro (de)serializaci).
Recommendations
-
❌ Nepoužívejte nezabezpečený kód ke kopírování, načítání a porovnávání struktur s vyplněním nebo neblittovatelnými členy. Zatížení z nedůvěryhodných vstupů jsou problematické i pro základní typy jako
boolnebodecimal. Současně můžou úložiště náhodně serializovat citlivé informace ze zásobníku v mezerách nebo odsazeních struktury. -
❌ Nespoléhejte na
T : unmanagedomezení, ani podobná rozhraní API, abyste zajistili,RuntimeHelpers.IsReferenceOrContainsReferencesže obecný typ je bezpečný pro provádění bitových operací. V době psaní těchto pokynů neexistuje žádný spolehlivý programový způsob, jak určit, zda je legální provádět libovolné bitové operace u daného typu.- Pokud je nutné provést takovou bitovou manipulaci, udělejte to pouze proti tomuto pevně zakódovanému seznamu typů a mějte na paměti endianitu aktuálního počítače:
- Primitivní celočíselné typy
Byte,SByte,Int16,UInt16,Int32,UInt32,Int64aUInt64. - Podporovaný jedním z výše uvedených primitivních integrálních typů
Enum. -
Char,Int128,UInt128, ,HalfSingle,Double,IntPtr, ,UIntPtr.
- Primitivní celočíselné typy
- Pokud je nutné provést takovou bitovou manipulaci, udělejte to pouze proti tomuto pevně zakódovanému seznamu typů a mějte na paměti endianitu aktuálního počítače:
- ✔️ Místo toho použijte načtení/úložiště (de)serializace pole po polích. Zvažte použití oblíbených a bezpečných knihoven pro (de)serializaci.
13. Spravované ukazatele s hodnotou null
Obecně platí, že byrefs (spravované ukazatele) jsou zřídka null a jediný bezpečný způsob, jak v současnosti vytvořit null hodnotu byref, je inicializovat ref struct s default. Všechna jeho pole jsou všechna spravovaná ref ukazateli null:
RefStructWithRefField s = default;
ref byte nullRef = ref s.refFld;
Existuje však několik nebezpečných způsobů, jak vytvořit byrefs null. Mezi příklady patří:
// Null byref by calling Unsafe.NullRef directly:
ref object obj = ref Unsafe.NullRef<object>();
// Null byref by turning a null unmanaged pointer into a null managed pointer:
ref object obj = ref Unsafe.AsRef<object>((void*)0);
Riziko zavedení problémů s bezpečností paměti je nízké a jakýkoli pokus o dereference hodnoty null byref povede k dobře definované výjimce NullReferenceException. Kompilátor jazyka C# však předpokládá , že dereferencování byref vždy proběhne úspěšně a nevygeneruje pozorovatelný vedlejší účinek. Proto je to legální optimalizace, která vylučuje jakékoli dereference, jejichž výsledná hodnota je okamžitě vyhozena. Podívejte se na dotnet/runtime#98681 (a tento související komentář) pro příklad nyní opravené chyby v rozhraní .NET, kde kód knihovny nesprávně spoléhal na dereferenci vyvolávající vedlejší efekt, aniž by si uvědomil, že kompilátor jazyka C# efektivně obešel zamýšlenou logiku.
Recommendations
- ❌ Pokud není nutné, nevytvávejte v jazyce C# hodnoty null byrefs. Zvažte použití běžných spravovaných odkazů, Vzor objektu Null nebo prázdných úseků.
- ❌ Nezahoďte výsledek funkce byref dereference, protože může být optimalizovaný a vést k potenciálním chybám.
14. stackalloc
stackalloc byl historicky použit k vytváření malých, neekkapace polí na zásobníku, což snižuje tlak GC. V budoucnu může analýza úniku JIT začít optimalizovat alokace polí GC, které neunikají, na objekty na zásobníku, což by mohlo stackalloc učinit nadbytečným. Do té doby stackalloc zůstává užitečné pro přidělování malých bufferů na zásobníku. U větších nebo zapouzdřených vyrovnávacích pamětí se často kombinuje s ArrayPool<T>.
Recommendations
✔️ Vždy spotřebovávejte
stackallocdoReadOnlySpan<T>/Span<T>na levé strany výrazu, aby se zajistily kontroly hranic:// Good: Span<int> s = stackalloc int[10]; s[2] = 0; // Bounds check is eliminated by JIT for this write. s[42] = 0; // IndexOutOfRangeException is thrown // Bad: int* s = stackalloc int[10]; s[2] = 0; s[42] = 0; // Out of bounds write, undefined behavior.❌ Nepoužívejte
stackallocuvnitř smyček. Prostor zásobníku není uvolněný, dokud se metoda nevrátí, takže zahrnutístackallocuvnitř smyčky může vést k ukončení procesu kvůli přetečení zásobníku.❌ Nepoužívejte velké délky pro
stackalloc. Například 1024 bajtů lze považovat za rozumnou horní mez.✔️ Zkontrolujte rozsah proměnných, které se používají jako
stackallocdélky.void ProblematicCode(int length) { Span<int> s = stackalloc int[length]; // Bad practice: check the range of `length`! Consume(s); }Opravená verze:
void BetterCode(int length) { // The "throw if length < 0" check below is important, as attempting to stackalloc a negative // length will result in process termination. ArgumentOutOfRangeException.ThrowIfLessThan(length, 0, nameof(length)); Span<int> s = length <= 256 ? stackalloc int[length] : new int[length]; // Or: // Span<int> s = length <= 256 ? stackalloc int[256] : new int[length]; // Which performs a faster zeroing of the stackalloc, but potentially consumes more stack space. Consume(s); }✔️ Používejte moderní funkce jazyka C#, jako jsou literály kolekcí (
Span<int> s = [1, 2, 3];),params Span<T>, a vložená pole, abyste se vyhnuli ruční správě paměti, pokud je to možné.
15. Vyrovnávací paměti s pevnou velikostí
Pevné velikosti bufferů byly užitečné pro integrační scénáře se zdroji dat z jiných jazyků nebo platforem. Pak byly nahrazeny bezpečnějšími a praktičtějšími vloženými poli.
Příkladem vyrovnávací paměti s pevnou velikostí (vyžaduje unsafe kontext) je následující fragment kódu:
public struct MyStruct
{
public unsafe fixed byte data[8];
// Some other fields
}
MyStruct m = new();
ms.data[10] = 0; // Out-of-bounds write, undefined behavior.
Moderní a bezpečnější alternativou jsou vložená pole:
[System.Runtime.CompilerServices.InlineArray(8)]
public struct Buffer
{
private int _element0; // can be generic
}
public struct MyStruct
{
public Buffer buffer;
// Some other fields
}
MyStruct ms = new();
ms.buffer[i] = 0; // Runtime performs a bounds check on index 'i'; could throw IndexOutOfRangeException.
ms.buffer[7] = 0; // Bounds check elided; index is known to be in range.
ms.buffer[10] = 0; // Compiler knows this is out of range and produces compiler error CS9166.
Dalším důvodem, proč se vyhnout vyrovnávacím pamětím s pevnou velikostí ve prospěch vložených polí, které jsou ve výchozím nastavení vždy inicializovány nulou, je, že vyrovnávací paměti s pevnou velikostí mohou mít v určitých scénářích nenulový obsah.
Recommendations
- ✔️ Raději nahraďte vyrovnávací paměti s pevnou velikostí vloženými poli nebo atributy zařazování IL, pokud je to možné.
16. Předání sousledných dat jako ukazatelů + délky (nebo spoléhání na nulové ukončení)
Vyhněte se definování rozhraní API, která přijímají nespravované nebo spravované ukazatele na souvislá data. Místo toho použijte Span<T> nebo ReadOnlySpan<T>:
// Poor API designs:
void Consume(ref byte data, int length);
void Consume(byte* data, int length);
void Consume(byte* data); // zero-terminated
void Consume(ref byte data); // zero-terminated
// Better API designs:
void Consume(Span<byte> data);
void Consume(Memory<byte> data);
void Consume(byte[] data);
void Consume(byte[] data, int offset, int length);
Nulové ukončení je zvlášť rizikové, protože ne všechny vyrovnávací paměti jsou ukončeny nulou a čtení po jakémkoli nulovém ukončovacím modulu může vést k zpřístupnění informací, poškození dat nebo ukončení procesu prostřednictvím porušení přístupu.
Recommendations
❌ Nezpřístupňujte metody, jejichž argumenty jsou typy ukazatelů (nespravované ukazatele
T*nebo spravované ukazatele), pokud jsou tyto argumentyref Turčeny k reprezentaci vyrovnávacích pamětí. Používejte bezpečné typy vyrovnávací paměti jakoSpan<T>neboReadOnlySpan<T>místo toho.❌ Nepoužívejte implicitní kontrakty pro argumenty byref, jako je například vyžadování, aby všichni volající umístili vstup na zásobník. Pokud je taková smlouva nezbytná, zvažte místo toho použití ref struktury .
❌ Nepředpokládejte, že vyrovnávací paměti jsou ukončeny nulovým bajtem, pokud není výslovně uvedeno, že to je platný předpoklad. Například, i když .NET zaručuje, že
stringinstance a"..."u8literály jsou ukončeny hodnotou null, to samé neplatí pro jiné typy vyrovnávací paměti jakoReadOnlySpan<char>nebochar[].unsafe void NullTerminationExamples(string str, ReadOnlySpan<char> span, char[] array) { Debug.Assert(str is not null); Debug.Assert(array is not null); fixed (char* pStr = str) { // OK: Strings are always guaranteed to have a null terminator. // This will assign the value '\0' to the variable 'ch'. char ch = pStr[str.Length]; } fixed (char* pSpan = span) { // INCORRECT: Spans aren't guaranteed to be null-terminated. // This could throw, assign garbage data to 'ch', or cause an AV and crash. char ch = pSpan[span.Length]; } fixed (char* pArray = array) { // INCORRECT: Arrays aren't guaranteed to be null-terminated. // This could throw, assign garbage data to 'ch', or cause an AV and crash. char ch = pArray[array.Length]; } }❌ Nepředávejte připnuté
Span<char>neboReadOnlySpan<char>přes p/invoke hranice, pokud jste nepředali explicitní argument délky. V opačném případě může kód na druhé straně p/invoke rozhraní nesprávně předpokládat, že vyrovnávací paměť je ukončena nulovým znakem.
unsafe static extern void SomePInvokeMethod(char* pwszData);
unsafe void IncorrectPInvokeExample(ReadOnlySpan<char> data)
{
fixed (char* pData = data)
{
// INCORRECT: Since 'data' is a span and is not guaranteed to be null-terminated,
// the receiver might attempt to keep reading beyond the end of the buffer,
// resulting in undefined behavior.
SomePInvokeMethod(pData);
}
}
Pokud chcete tento problém vyřešit, použijte alternativní podpis p/invoke, který přijímá datový ukazatel i délku, pokud je to možné. Jinak, pokud příjemce nemá žádný způsob, jak přijmout samostatný argument délky, ujistěte se, že se původní data nejprve převedou na string, a teprve poté je připevněte a předejte přes hranice p/invoke.
unsafe static extern void SomePInvokeMethod(char* pwszData);
unsafe static extern void SomePInvokeMethodWhichTakesLength(char* pwszData, uint cchData);
unsafe void CorrectPInvokeExample(ReadOnlySpan<char> data)
{
fixed (char* pData = data)
{
// OK: Since the receiver accepts an explicit length argument, they're signaling
// to us that they don't expect the pointer to point to a null-terminated buffer.
SomePInvokeMethodWhichTakesLength(pData, (uint)data.Length);
}
// Alternatively, if the receiver doesn't accept an explicit length argument, use
// ReadOnlySpan<T>.ToString to convert the data to a null-terminated string before
// pinning it and sending it across the p/invoke boundary.
fixed (char* pStr = data.ToString())
{
// OK: Strings are guaranteed to be null-terminated.
SomePInvokeMethod(pStr);
}
}
17. Řetězcové mutaci
Řetězce v jazyce C# jsou neměnné podle návrhu a všechny pokusy o změny pomocí nebezpečného kódu mohou vést k nedefinovanému chování. Příklad:
string s = "Hello";
fixed (char* p = s)
{
p[0] = '_';
}
Console.WriteLine("Hello"); // prints "_ello" instead of "Hello"
Při úpravě interovaného řetězce (většina řetězcových literálů je) se změní hodnota pro všechna ostatní použití. I bez internování řetězců by zápis do nově vytvořeného řetězce měl být nahrazen bezpečnějším rozhraním API String.Create.
// Bad:
string s = new string('\n', 4); // non-interned string
fixed (char* p = s)
{
// Copy data into the newly created string
}
// Good:
string s = string.Create(4, state, (chr, state) =>
{
// Copy data into the newly created string
});
Recommendations
-
❌ Neměňte řetězce.
String.CreatePokud potřebujete složitou logiku kopírování, použijte rozhraní API k vytvoření nového řetězce. Jinak použijte.ToString(),StringBuilder,new string(...)nebo syntaxi interpolace řetězců.
18. Nezpracovaný kód IL (například System.Reflection.Emit a Mono.Cecil)
Generování nezpracovaných IL (ať už pomocí System.Reflection.Emit, knihoven třetích stran jako Mono.Cecil, nebo přímým psaním kódu IL) ze své podstaty obchází všechny záruky bezpečnosti paměti, které jazyk C# poskytuje.
Nepoužívejte takové techniky, pokud to není nezbytně nutné.
Recommendations
- ❌ Nevysílejte nezpracovaný kód IL, protože nemá žádná vodicí zábradlí, což může snadno vést k problémům s bezpečností typů a dalším chybám. Stejně jako jiné techniky generování dynamického kódu není generování nezpracovaného mezijazyka (IL) vhodné pro AOT, pokud se neprovede v době sestavení.
- ✔️ Pokud je to možné, použijte místo toho generátory zdrojů.
- ✔️ Raději upřednostňujte [UnsafeAccessor] místo generování nezpracovaného IL pro psaní kódu serializace s nízkou režií pro soukromé členy v případě potřeby.
- ✔️ Pokud nějaké rozhraní API chybí, vytvořte návrh rozhraní API proti modulu dotnet/runtime a místo toho musíte použít nezpracovaný kód IL.
- ✔️ Pokud potřebujete použít nezpracované IL, použijte
ilverifynebo podobné nástroje k ověření vygenerovaného kódu IL.
19. Neinicializované lokální proměnné [SkipLocalsInit] a Unsafe.SkipInit
[SkipLocalsInit] bylo zavedeno v .NET 5.0, aby JIT mohl přeskočit vynulování místních proměnných v metodách, a to buď pro jednotlivé metody, nebo pro celý modul. Tato funkce byla často použita k tomu, aby JIT odstraňoval redundantní nulové inicializace, například pro stackalloc. Pokud však místní hodnoty nejsou explicitně inicializovány před použitím, může to vést k nedefinovaným chováním. Díky nedávným vylepšením schopnosti JIT eliminace nulových inicializací a provádění vektorizace se výrazně snížila potřeba [SkipLocalsInit] a Unsafe.SkipInit.
Recommendations
-
❌ Nepoužívejte
[SkipLocalsInit]aUnsafe.SkipInitpokud nejsou pozorovány žádné výhody výkonu v horkém kódu nebo si nejste jistí riziky, která zavádějí. - ✔️ Kódujte defenzivně při použití API, jako
GC.AllocateUninitializedArrayaArrayPool<T>.Shared.Rent, které mohou podobně vracet neinicializované vyrovnávací paměti.
20. ArrayPool<T>.Shared a podobná rozhraní API pro sdružování
ArrayPool<T>.Shared je sdílený fond polí sloužících ke snížení tlaku GC v horkém kódu. Často se využívá k přidělování dočasných vyrovnávacích pamětí pro vstupně-výstupní operace nebo jiné krátkodobé scénáře. I když je rozhraní API jednoduché a ze své podstaty neobsahuje nebezpečné funkce, může vést k chybám typu "use-after-free" v jazyce C#. Příklad:
var buffer = ArrayPool<byte>.Shared.Rent(1024);
_buffer = buffer; // buffer object escapes the scope
Use(buffer);
ArrayPool<byte>.Shared.Return(buffer);
Jakékoli použití _buffer po volání Return je chyba typu use-after-free. Tento minimální příklad je snadno rozpoznatelný, ale chyba se stává obtížnější zjistit, kdy Rent a Return jsou v různých oborech nebo metodách.
Recommendations
- ✔️ Ponechávejte odpovídající volání do
RentaReturnv rámci stejné metody, pokud je to možné, abyste zúžili rozsah potenciálních chyb. -
❌ Nepoužívejte
try-finallyvzor k voláníReturnvfinallybloku, pokud si nejste jisti, že neúspěšná logika dokončila použití vyrovnávací paměti. Je lepší opustit vyrovnávací paměť, než riskovat chybu použití po uvolnění z důvodu neočekávaného předčasnéhoReturn. - ✔️ Mějte na paměti, že podobné problémy mohou nastat s jinými rozhraními API nebo vzory sdružování, například ObjectPool<T>.
21. bool<->int převody
Přestože standard ECMA-335 definuje logickou hodnotu jako 0–255, kde true je jakákoli nenulová hodnota, je lepší se vyhnout jakýmkoli explicitním převodům mezi celými čísly a logickými hodnotami, aby se zabránilo zavedení "denormalizovaných" hodnot jako čehokoli jiného než 0 nebo 1 pravděpodobně vede k nespolehlivému chování.
// Bad:
bool b = Unsafe.As<int, bool>(ref someInteger);
int i = Unsafe.As<bool, int>(ref someBool);
// Good:
bool b = (byte)someInteger != 0;
int i = someBool ? 1 : 0;
JIT, který je přítomný v dřívějších modulech runtime .NET, plně neoptimalizoval bezpečnou verzi této logiky, což vedlo k tomu, že vývojáři používali nebezpečné konstrukty k převodu mezi bool a int v cestách kódu citlivých na výkon. To už neplatí a moderní jity .NET dokážou efektivně optimalizovat bezpečnou verzi.
Recommendations
- ❌ Nezapisujte převody bez větví mezi celými čísly a logickými hodnotami pomocí nebezpečného kódu.
- ✔️ Místo toho používejte ternární operátory (nebo jinou logiku větvení). Moderní technologie JIT pro .NET je efektivně optimalizují.
-
❌ Nečtěte
boolpomocí nebezpečných rozhraní API, jako jsouUnsafe.ReadUnalignedneboMemoryMarshal.Cast, pokud vstupu nedůvěřujete. Místo toho zvažte použití ternárních operátorů nebo porovnání rovnosti:
// Bad:
bool b = Unsafe.ReadUnaligned<bool>(ref byteData);
// Good:
bool b = byteData[0] != 0;
// Bad:
ReadOnlySpan<byte> byteSpan = ReadDataFromNetwork();
bool[] boolArray = MemoryMarshal.Cast<byte, bool>(byteSpan).ToArray();
// Good:
ReadOnlySpan<byte> byteSpan = ReadDataFromNetwork();
bool[] boolArray = new bool[byteSpan];
for (int i = 0; i < byteSpan.Length; i++) { boolArray[i] = byteSpan[i] != 0; }
Další informace naleznete v tématu Binární (de)serializace struktur s odsazením nebo nelimitovatelnými členy.
22. Interoperabilita
I když většina návrhů v tomto dokumentu platí i pro scénáře spolupráce, doporučuje se postupovat podle průvodce osvědčenými postupy pro nativní interoperabilitu . Zvažte také použití automaticky generovaných obálek pro interop, jako CsWin32 a CsWinRT. Tím se snižuje potřeba psaní ručního interop kódu a omezuje riziko zavádění problémů s bezpečností paměti.
23. Bezpečnost závitů
Bezpečnost paměti a bezpečnost vláken jsou orthogonální koncepty. Kód může být bezpečný z hlediska paměti, ale může stále obsahovat souběžné konflikty v datech, roztržené čtení nebo chyby viditelnosti; naopak kód může být navržen pro bezpečné zpracování více vláken, zatímco stále vyvolává nedefinované chování prostřednictvím nebezpečné manipulace s pamětí. Širší doprovodné materiály najdete v osvědčených postupech spravovaných vláken a modelu paměti .NET.
24. Nebezpečný kód kolem SIMD/Vectorization
Další podrobnosti najdete v pokynech pro vektorizaci . V kontextu nebezpečného kódu je důležité mít na paměti:
- Operace SIMD mají komplexní požadavky, aby mohly poskytnout atomické záruky (někdy je vůbec neposkytují).
- Většina rozhraní API pro načítání a ukládání SIMD neposkytuje kontroly hranic.
25. Přibližné testování
Přibližné testování (neboli "fuzzing") je automatizovaná technika testování softwaru, která zahrnuje poskytování neplatných, neočekávaných nebo náhodných dat jako vstupů do počítačového programu. Poskytuje způsob, jak detekovat problémy s bezpečností paměti v kódu, které můžou mít mezery v pokrytí testů. Pomocí nástrojů, jako je SharpFuzz , můžete nastavit testování přibližných shod pro kód .NET.
26. Upozornění kompilátoru
Kompilátor jazyka C# obecně neposkytuje rozsáhlou podporu, jako jsou upozornění a analyzátory související s nesprávným používáním nebezpečného kódu. Existují však některá upozornění, která můžou pomoct odhalit potenciální problémy a neměly by se ignorovat ani potlačit bez pečlivého zvážení. Mezi příklady patří:
nint ptr = 0;
unsafe
{
int local = 0;
ptr = (nint)(&local);
}
await Task.Delay(100);
// ptr is used here
Tento kód generuje upozornění CS9123 (Operátor &by neměl být použit u parametrů nebo místních proměnných v asynchronních metodách), což znamená, že kód je pravděpodobně nesprávný.
Recommendations
- ✔️ Věnujte pozornost upozorněním kompilátoru a opravte podkladové problémy místo jejich potlačení.
- ❌ Nepředpokládáme, že absence upozornění kompilátoru znamená, že kód je správný. Kompilátor jazyka C# nemá žádnou podporu pro detekci nesprávného použití nebezpečného kódu.
Odkazy
- Nebezpečný kód, typy ukazatelů a ukazatele funkcí
- Nebezpečný kód, specifikace jazyka.
- Co každý vývojář CLR musí vědět před psaním kódu pro pokročilá témata týkající se CoreCLR a interních GC.
- Osvědčené postupy nativní interoperability
- Osvědčené postupy pro spravovaná vlákna
- Osvědčené postupy pro výjimky
- Pokyny pro vektorizaci
- Model paměti .NET
- ECMA-335
- ECMA-335 rozšiřuje