Metodtips för osäker kod

Den här artikeln innehåller detaljerade rekommendationer för specifika osäkra mönster, de risker de medför och hur du minimerar dessa risker. Dessa riktlinjer riktar sig till alla utvecklare som skriver eller granskar osäker kod i C#. Andra .NET-språk som F# och Visual Basic ligger utanför omfånget för den här artikeln, även om vissa rekommendationer kan gälla även för dessa språk.

Ordlista

  • AVE – Undantag för åtkomstöverträdelse.
  • Byref – en hanterad pekare (ref T t) som liknar en ohanterad pekare men spåras av GC. Pekar vanligtvis på godtyckliga delar av objekt eller stack. En referens är i praktiken en hanterad pekare med +0 offset.
  • CVE – Offentligt avslöjade sårbarheter för cybersäkerhet.
  • JIT – Just-in-time-kompilator (RyuJIT i CoreCLR och NativeAOT).
  • PGO – Profilstyrd optimering.
  • Ohanterad pekare (eller rå pekare) – En pekare (T* p) som pekar på godtycklig minnesplats och inte hanteras eller spåras av GC.

Andra termer finns i .NET Runtime Glossary.

Vanliga otillförlitliga mönster

C# ger en säker miljö där utvecklare inte behöver oroa sig för det interna arbetet i körningen och GC. Med osäker kod kan du kringgå dessa säkerhetskontroller, vilket kan leda till otillförlitliga mönster som kan leda till minnesskada. Även om sådana mönster kan vara användbara i vissa scenarier bör du använda dem med försiktighet och endast när det är absolut nödvändigt. Inte bara tillhandahåller C# och .NET inga verktyg för att verifiera säkerheten av osäker kod (som olika C/C++-sanitizers kan göra), utan GC-specifika beteenden kan också innebära ytterligare risker i osäker C# utöver dem som traditionella C/C++-utvecklare kanske är bekanta med.

Osäker kod runt hanterade referenser bör skrivas med följande konservativa antaganden i åtanke:

  • GC kan avbryta exekveringen av vilken metod som helst när som helst vid vilken instruktion som helst.
  • GC kan flytta objekt i minnet och uppdatera alla spårade referenser.
  • GC vet exakt när referenser inte längre behövs.

Ett klassiskt exempel på heap-korrumpering uppstår när en GC tappar en objektreferens eller behandlar ogiltiga pekare som heapreferenser. Detta resulterar ofta i icke-deterministiska krascher eller minnesskada. Fel med heapkorruption är särskilt svåra att diagnostisera och återskapa eftersom:

  • Dessa problem kan förbli dolda under en lång tid och endast manifestera sig efter en orelaterad kodändring eller körtidsuppdatering.
  • De kräver ofta exakt tidpunkt för att reproducera, till exempel att GC avbryter programkörningen på en viss plats och startar hopkompaktering, vilket är en sällsynt och icke-deterministisk händelse.

I nästa avsnitt beskrivs vanliga osäkra mönster med ✔️ DO- och ❌ DON'T-rekommendationer.

1. Ospårade hanterade pekare (Unsafe.AsPointer och liknande element)

Det går inte att konvertera en hanterad (spårad) pekare till en ohanterad (ospårad) pekare i säker C#. När sådana behov uppstår kan det vara frestande att använda Unsafe.AsPointer<T>(T) för att undvika overheaden av en fixed anvisning. Även om det finns giltiga användningsfall för detta medför det en risk att du skapar ospårade pekare för flyttbara objekt. Exempel:

unsafe void UnreliableCode(ref int x)
{
    int* nativePointer = (int*)Unsafe.AsPointer(ref x);
    nativePointer[0] = 42;
}

Om GC avbryter körningen av metoden UnreliableCode direkt efter att pekaren har lästs (adressen refereras av x) och flyttar det refererade objektet, kommer GC att korrekt uppdatera platsen som lagras i x, men kommer inte att känna till nativePointer och därmed inte uppdatera värdet som den innehåller. Vid den tidpunkten innebär skrivning till nativePointer att man skriver till godtyckligt minne.

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.
}

När GC återupptar körningen av metoden, kommer den att skriva 42 till den gamla platsen x, vilket kan leda till ett oväntat undantag, global tillståndskorruption eller processavslutning på grund av en åtkomstkränkning.

Den rekommenderade lösningen är i stället att använda nyckelordet fixed och & adressoperatorn för att säkerställa att GC inte kan flytta målreferensen under åtgärdens varaktighet.

unsafe void ReliableCode(ref int x)
{
    fixed (int* nativePointer = &x) // `x` cannot be relocated for the duration of this block.
    {
        nativePointer[0] = 42;
    }
}

Recommendations

  1. ❌ Använd ref X inte argument med ett implicit kontrakt som X alltid är stackallokerat, fäst eller på annat sätt inte kan flyttas av GC. Samma sak gäller oformaterade objekt och Spans – introducera inte kontrakt om livslängd som är beroende av vem som anropar, och som inte är uppenbara, i metodsignaturer. Överväg att i stället ta ett referens-struct-argument eller ändra argumentet till en rå pekartyp (X*).
  2. ❌ Använd inte en pekare från Unsafe.AsPointer<T>(T) om den kan överleva det ursprungliga objektet som det pekar på. Enligt API:ets dokumentation är det upp till anroparen Unsafe.AsPointer<T>(T) att garantera att GC inte kan flytta referensen. Se till att det är tydligt för kodgranskare att anroparen har uppfyllt den här förutsättningen.
  3. Använd ✔️ GCHandle eller fixed omfång istället för Unsafe.AsPointer<T>(T) för att definiera explicita omfång för ohanterade pekare och säkerställa att objektet alltid förblir fäst.
  4. ✔️ Använd ohanterade pekare (med fixed) i stället för byrefs när du behöver justera en matris till en specifik gräns. Detta säkerställer att GC inte flyttar objektet och ogiltigförklarar eventuella justeringsantaganden som logiken kan förlita sig på.

2. Exponera pekare utanför omfånget fixed

Även om det fasta nyckelordet definierar ett omfång för pekaren som hämtas från det fästa objektet, är det fortfarande möjligt för den pekaren att komma ifrån omfånget fixed och introducera buggar, eftersom C# inte ger något ägarskaps-/livscykelskydd för det. Ett vanligt exempel är följande kodfragment:

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.
    }
}

I det här exemplet fästs matrisen korrekt med nyckelordet fixed (se till att GC inte kan flytta den inom fixed blocket), men pekaren exponeras utanför fixed blocket. Detta skapar en dinglande pekare vars dereference resulterar i odefinierat beteende.

Recommendations

  1. ✔️ Se till att pekare i fixed block inte lämnar det definierade omfånget.
  2. ✔️ Föredrar säkra primitiver på låg nivå med inbyggd escape-analys, till exempel C#:s referens-struct. För mer information, se förbättringar av låg-nivå struct.

3. Interna implementeringsdetaljer av körmiljön och biblioteken

Även om det är dålig praxis att komma åt eller förlita sig på intern implementeringsinformation i allmänhet (och inte stöds av .NET), är det värt att framhäva specifika vanliga fall. Detta är inte avsett att vara en fullständig lista över alla möjliga saker som kan gå fel när kod felaktigt förlitar sig på en intern implementeringsinformation.

Recommendations

  1. ❌ FÅR INTE ändra eller läsa några delar av ett objekts header.

    • Objektrubriker kan skilja sig åt mellan körmiljöer.
    • I CoreCLR går det inte att komma åt objektrubriken på ett säkert sätt utan att fästa objektet först.
    • Ändra aldrig objektets typ genom att ändra MethodTable-pekaren.
  2. ❌ Lagra inga data i ett objekts utfyllnad. Anta inte att utfyllnadsinnehåll bevaras eller att utfyllnad alltid nollställs som standard.

  3. ❌ Gör inga antaganden om storlekar och förskjutningar av något annat än primitva datatyper och strukturer med sekventiell eller explicit layout. Även då förekommer undantag, till exempel när GC-handtag är i användning.

  4. ❌ Anropa inte icke-offentliga metoder, få åtkomst till icke-offentliga fält eller mutera skrivskyddade fält i BCL-typer med reflektion eller osäker kod.

  5. ❌ Anta inte att någon viss icke-offentlig medlem i BCL alltid kommer att finnas eller har en specifik form. .NET-teamet ändrar eller tar ibland bort icke-offentliga API:er i serviceversioner.

  6. ❌ Ändra static readonly inte fält med reflektion eller osäker kod, eftersom de antas vara konstanta. RyuJIT infogar dem till exempel vanligtvis som explicita konstanter.

  7. ❌ Anta inte bara att en referens inte kan flyttas. Den här vägledningen gäller för sträng- och UTF-8-literaler,"..."u8 statiska fält, RVA-fält, LOH-objekt och så vidare.

    • Det här är informationsdetaljer om exekveringstidsimplementeringar som kan gälla för vissa runtime-miljöer men inte för andra.
    • Icke hanterade pekare till sådana objekt kanske inte hindrar komponenter från att laddas ur, vilket gör att pekarna blir dinglande. Använd fixed omfång för att säkerställa korrekthet.
    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.
    
  8. ❌ Skriv inte kod som förlitar sig på implementationsdetaljerna för en specifik körmiljö.

4. Ogiltiga hanterade pekare (även om de aldrig derefereras)

Vissa kategorier av kod lutar sig mot pekarmanipulering och aritmetik, och sådan kod har ofta ett val mellan att använda ohanterade pekare (T* p) och hanterade pekare (ref T p). Dessa pekare kan manipuleras godtyckligt, till exempel med operatorer för ohanterade pekare (p++) och genom Unsafe-metoder för hanterade pekare (p = ref Unsafe.Add(ref p, 1)). Båda betraktas som "osäker kod" och det är möjligt att skapa opålitliga mönster med båda. För vissa algoritmer kan det dock vara enklare att oavsiktligt skapa GC-osäkra mönster när hanterade pekare manipuleras. Eftersom ohanterade pekare inte spåras av GC är värdet de innehåller endast relevant när det avkodas av utvecklarkoden. Däremot är en hanterad pekares värde relevant inte bara när det avrefereras av utvecklarkoden, utan även när det granskas av GC. Därför kan en utvecklare skapa ogiltiga ohanterade pekare utan konsekvens så länge de inte derefereras, men att skapa en ogiltig hanterad pekare är en bugg. Exempel:

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
    }
}

Liknande kod som använder byrefs (hanterade pekare) är dock ogiltig.

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
}

Även om den hanterade implementeringen här undviker mindre fästkostnader är det osund eftersom invalidPtrdet kan bli en yttre pekare medan den faktiska adressen array[0] för uppdateras av GC. Sådana buggar är subtila, och även .NET har stött på problem med dem under utvecklingen.

Recommendations

  1. ❌ Skapa inte ogiltiga hanterade pekare, även om de inte blir derefererade eller om de finns i kodvägar som aldrig körs.
  2. ✔️ Använd låsta ohanterade pekare om algoritmen kräver sådana manipulationer.

5. Omtolka typgjutningar

Även om alla typer av struktur-till-klass- eller klass-till-struktur-omvandlingar är ett odefinierat beteende enligt definitionen, är det också möjligt att stöta på otillförlitliga mönster med struktur-till-struktur- eller klass-till-klassomvandlingar. Ett typiskt exempel på ett opålitligt mönster är följande kod:

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.

Och även om layouten är liknande bör du fortfarande vara försiktig när GC-referenser (fält) ingår.

Recommendations

  1. ❌ Kasta inte structs till klasser eller vice versa.
  2. ❌ Använd Unsafe.As inte för struct-to-struct- eller klass-till-klass-konverteringar såvida du inte är helt säker på att casten är laglig. Mer information finns i avsnittet Kommentarer i Unsafe.As API-dokumenten.
  3. ✔️ Du bör föredra säkrare fält-för-fält kopiering, externa bibliotek såsom AutoMapper, eller Source Generators för sådana konverteringar.
  4. ✔️ Föredrar Unsafe.BitCast framför Unsafe.As, som BitCast ger vissa rudimentära användningskontroller. Observera att dessa kontroller inte ger fullständiga korrekthetsgarantier, vilket innebär att BitCast det fortfarande betraktas som ett osäkert API.

6. Att kringgå skrivbarriären och icke-atomiska operationer på GC-referenser

Normalt är alla typer av skrivningar eller läsningar av GC-referenser alltid atomiska. Dessutom går alla försök att tilldela en GC-referens (eller en byref till struct med GC-fält) till en potentiell heapplats genom skrivbarriären som säkerställer att GC är medveten om nya anslutningar mellan objekt. Med osäker kod kan vi dock kringgå dessa garantier och införa otillförlitliga mönster. Exempel:

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;
    }
}

På samma sätt är följande kod med hanterade pekare också otillförlitlig:

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

  1. ❌ Använd inte icke-atomiska åtgärder på GC-referenser (till exempel tillhandahåller SIMD-åtgärder ofta inte dem).
  2. ❌ Använd inte ohanterade pekare för att lagra GC-referenser på heapplatser (utelämna skrivbarriären).

7. Antaganden om objektets livslängd (finalizers, GC.KeepAlive)

Undvik att göra antaganden om livslängden för objekt från GC:s perspektiv. Anta inte att ett objekt fortfarande lever när det kanske inte är det. Objektets livslängd kan variera mellan olika körningar eller till och med mellan olika nivåer av samma metod (Nivå0 och Nivå1 i RyuJIT). Finalizers är ett vanligt fall där sådana antaganden kan visa sig vara felaktiga.

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();

I det här exemplet kan DestroyHandle anropas innan DoWork är klart eller till och med innan det börjar. Därför är det viktigt att inte anta att objekt, till exempel this, förblir aktiva fram till slutet av metoden.

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);
}

Därför rekommenderar vi att du uttryckligen förlänger livslängden för objekt med GC.KeepAlive(Object) eller SafeHandle.

En annan klassisk instans av det här problemet är Marshal.GetFunctionPointerForDelegate<TDelegate>(TDelegate) 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

  1. ❌ Gör inga antaganden om objektets livslängd. Anta till exempel aldrig att this alltid förblir aktiv fram till metodens slut.
  2. ✔️ SafeHandle Använd för att hantera interna resurser.
  3. ✔️ Använd GC.KeepAlive(Object) för att förlänga livslängden för objekt vid behov.

8. Åtkomst mellan trådar till lokala variabler

Att komma åt lokala variabler från en annan tråd anses vanligtvis vara dålig praxis. Det blir dock uttryckligen odefinierat beteende när hanterade referenser ingår, enligt beskrivningen i .NET Memory Model.

Exempel: En struct som innehåller GC-referenser kan nollställas eller skrivas över på ett icke-trådsäkert sätt i en no-GC region medan en annan tråd läser den, vilket leder till odefinierat beteende.

Recommendations

  1. ❌ Undvik att komma åt lokala variabler över trådar (särskilt om de innehåller GC-referenser).
  2. ✔️ Använd heapminne eller icke-hanterat minne (till exempel NativeMemory.Alloc) i stället.

9. Borttagning av osäkra gränskontroller

I C# innehåller alla idiomatiska minnesåtkomster gränskontroller som standard. JIT-kompilatorn kan ta bort dessa kontroller om det kan bevisa att de är onödiga, som i exemplet nedan.

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];
    }
}

Även om JIT ständigt förbättras när det gäller att identifiera sådana mönster, finns det fortfarande scenarier där det lämnar kontrollerna på plats, vilket kan påverka prestanda i frekvent kod. I sådana fall kan du vara frestad att använda osäker kod för att manuellt ta bort dessa kontroller utan att helt förstå riskerna eller korrekt utvärdera prestandafördelarna.

Överväg till exempel följande metod.

int FetchAnElement(int[] array, int index)
{
    return array[index];
}

Om JIT inte kan bevisa att det index alltid är lagligt inom gränserna för array, kommer det att skriva om metoden för att se ut ungefär som nedan.

int FetchAnElement_AsJitted(int[] array, int index)
{
    if (index < 0 || index >= array.Length)
        throw new IndexOutOfBoundsException();
    return array.GetElementAt(index);
}

Om du vill minska omkostnaderna från den kontrollen i varm kod kan du vara frestad att använda osäkerhetsekvivalenta API:er (Unsafe och 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);
}

Eller använd fästpekare och råpekare:

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];
    }
}

Detta kan leda till slumpmässiga krascher eller skadade tillstånd om index ligger utanför gränserna på array. Sådana osäkra transformeringar kan ha prestandafördelar på mycket heta vägar, men dessa fördelar är ofta tillfälliga, eftersom varje .NET-version förbättrar JIT:s möjlighet att eliminera onödiga gränskontroller när det är säkert att göra det.

Recommendations

  1. ✔️ Kontrollera om den senaste versionen av .NET fortfarande inte kan eliminera gränskontrollen. Om det kan skriver du om med hjälp av säker kod. Annars kan du skicka ett problem mot RyuJIT. Använd det här spårningsproblemet som en bra utgångspunkt.
  2. ✔️ Mät den verkliga prestandapåverkan. Om prestandavinsten är försumbar eller om koden inte har visat sig vara presterande utanför ett trivialt mikrobenchmark bör du skriva om det med säker kod.
  3. ✔️ Ge ytterligare tips till JIT, till exempel manuella gränskontroller före loopar och spara fält till lokala variabler, eftersom .NET Memory Model kan försiktigt förhindra JIT från att ta bort gränskontroller i vissa scenarier.
  4. ✔️ DO-skyddskod med Debug.Assert gränskontroller om osäker kod fortfarande är nödvändig. Tänk på exemplet nedan.
Debug.Assert(array is not null);
Debug.Assert((index >= 0) && (index < array.Length));
// Unsafe code here

Du kan till och med omstrukturera dessa kontroller till återanvändbara hjälpmetoder.

[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);
}

Inkludering av Debug.Assert ger inga tillförlitlighetskontroller för release-versioner, men det kan hjälpa till att identifiera potentiella buggar i debug-versioner.

10. Koalescering av minnesåtkomst

Du kan vara frestad att använda osäker kod för att samla minnesåtkomster för att förbättra prestanda. Ett klassiskt exempel är följande kod för att skriva "False" till en teckenmatris:

// 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)
}

I tidigare versioner av .NET var den osäkra versionen med hjälp av MemoryMarshal mätbart snabbare än den enkla säkra versionen. Moderna versioner av .NET innehåller dock en mycket förbättrad JIT som ger motsvarande kodgen för båda fallen. Från och med .NET 10 är x64-kodgenen:

; 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

Det finns en ännu enklare och mer läsbar version av koden:

"False".CopyTo(dst);

Från och med .NET 10 genererar det här anropet identisk kodgen som ovan. Det finns till och med en extra fördel: det signalerar till JIT att strikta skrivoperationer för varje element inte behöver vara atomära. JIT kan kombinera det här tipset med andra sammanhangsberoende kunskaper för att ge ännu fler optimeringar utöver det som beskrivs här.

Recommendations

  1. ✔️ Föredrar idiomatisk säker kod i stället för osäker för sammankoppling av minnesåtkomst:
    • Föredrar Span<T>.CopyTo och Span<T>.TryCopyTo för att kopiera data.
    • Föredrar String.Equals och Span<T>.SequenceEqual för att jämföra data (även när du använder StringComparer.OrdinalIgnoreCase).
    • Föredra Span<T>.Fill för att fylla data och Span<T>.Clear för att rensa data.
    • Tänk på att skrivningar/läsningar per element eller per fält kan sammanslagas automatiskt av JIT.
  2. ✔️ Skapa ett ärende för dotnet/runtime om du skriver idiomatisk kod och observerar att den inte är optimerad som förväntat.
  3. ❌ Sammanfoga inte minnesåtkomst manuellt om du inte är säker på feljusterade minnesåtkomstrisker, atomicitetsgarantier eller eventuella prestandafördelar.

11. Ojusterad minnesåtkomst

Sammankopplingen av minnesåtkomst som beskrivs i Sammankoppling av minnesåtkomst resulterar ofta i explicita eller implicita feljusterade läsningar/skrivningar. Även om detta vanligtvis inte orsakar allvarliga problem (förutom potentiella prestandapåföljder på grund av att cacheminnet och sidgränserna korsas), utgör det fortfarande några verkliga risker.

Tänk till exempel på scenariot där du rensar två element i en matris samtidigt:

uint[] arr = _arr;
arr[i + 0] = 0;
arr[i + 1] = 0;

Anta att de tidigare värdena på dessa platser var båda uint.MaxValue (0xFFFFFFFF). .NET-minnesmodellen garanterar att båda skrivningarna är atomiska, så alla andra trådar i processen kommer bara att observera det nya värdet 0 eller det gamla värdet 0xFFFFFFFF, aldrig "sönderrivna" värden som 0xFFFF0000.

Anta dock att följande osäkra kod används för att kringgå gränskontrollen och noll båda elementen med ett enda 64-bitarslager:

ref uint p = ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(arr), i);
Unsafe.WriteUnaligned<ulong>(ref Unsafe.As<uint, byte>(ref p), 0UL);

Den här koden har sidoeffekten att ta bort atomicitetsgarantin. Rivna värden kan observeras av andra trådar, vilket leder till odefinierat beteende. För att en sådan sammansejsad skrivning ska vara atomisk måste minnet justeras efter skrivstorleken (8 byte i det här fallet). Om du försöker justera minnet manuellt före åtgärden måste du tänka på att GC kan flytta (och i praktiken ändra justeringen av) matrisen när som helst om den inte är fäst. Mer information finns i dokumentationen för .NET Memory Model .

En annan risk för ojusterad minnesåtkomst är risken för en programkrasch i vissa scenarier. Även om vissa .NET-runtimes förlitar sig på operativsystemet för att korrigera feljusterade åtkomster, finns det fortfarande vissa scenarier på vissa plattformar där felaktig åtkomst kan leda till en DataMisalignedException (eller SEHException). Några av exemplen är:

  • Interlocked åtgärder på feljusterat minne på vissa plattformar (till exempel).
  • Feljusterade flyttalsoperationer på ARM.
  • Åtkomst till särskilt enhetsminne med vissa justeringskrav (stöds egentligen inte av .NET).

Recommendations

  1. ❌ Använd inte ojusterade minnesåtkomster i låsfria algoritmer och andra scenarier där atomitet är viktigt.
  2. ✔️ Justera data manuellt om det behövs, men tänk på att GC kan flytta objekt när som helst och effektivt ändra justeringen dynamiskt. Detta är särskilt viktigt för olika StoreAligned/LoadAligned API:er i SIMD.
  3. ✔️ Använd explicita ojusterade API:er för läsning/skrivning, till exempel Unsafe.ReadUnaligned/Unsafe.WriteUnaligned i stället för justerade api:er,Unsafe.Read<T>(Void*)/Unsafe.Write<T>(Void*, T) till exempel eller Unsafe.As<TFrom,TTo>(TFrom) om data kan vara feljusterade.
  4. ✔️ Tänk på att olika API:er för minnesmanipulering som Span<T>.CopyTo(Span<T>) inte heller ger atomitetsgarantier.
  5. ✔️ Mer information om atomicitetsgarantier finns i dokumentationen om .NET Memory Model (se referenser).
  6. ✔️ Mät prestanda på alla dina målplattformar, eftersom vissa plattformar medför ett betydande prestandastraff för ej justerade minnesåtkomster. På de här plattformarna kanske naiv kod presterar bättre än smart kod.
  7. ✔️ Tänk på att det finns scenarier och plattformar där ojusterad minnesåtkomst kan leda till ett undantag.

12. Binär (de)serialisering av structar med minnesutfyllnad eller icke-blitbara medlemmar

Var försiktig när du använder olika serialiseringsliknande API:er för att kopiera eller läsa structs till eller från bytematriser. Om en struct innehåller utfyllnader eller icke-blittable-medlemmar (till exempel bool eller GC-fält) kan klassiska osäkra minnesåtgärder som Fill, CopyTooch SequenceEqual oavsiktligt kopiera känsliga data från stacken till utfyllnadsdata eller behandla skräpdata som betydande vid jämförelser som skapar sällan reproducerbara buggar. Ett vanligt antimönster kan se ut så här:

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.
}

Den enda korrekta metoden är att använda fält-för-fält-inläsningar/arkiv som är specialiserade för varje TObject indata (eller generaliseras med reflektion, källgeneratorer eller (de)serialiseringsbibliotek).

Recommendations

  1. ❌ Använd inte osäker kod för att kopiera/läsa in/jämföra strukturer med utfyllnad eller icke-hanterbara medlemmar. Inläsningar från ej betrodda indata är problematiska även för grundläggande typer som bool eller decimal. Samtidigt kan lagringssystem oavsiktligt serialisera känslig information från stacken i en structs mellanrum/fyllnad.
  2. ❌ Förlita dig inte på T : unmanaged villkor, RuntimeHelpers.IsReferenceOrContainsReferenceseller liknande API:er för att garantera att en allmän typ är säker att utföra bitvis åtgärder på. När dessa riktlinjer skrivs finns det inget tillförlitligt programmatiskt sätt att avgöra om det är lagligt att utföra godtyckliga bitvisåtgärder på en viss typ.
    • Om du måste utföra sådan bitvis manipulation gör du det bara mot den här hårdkodade listan med typer och är medveten om den aktuella datorns endianitet:
      • De primitiva integraltyperna Byte, SByte, Int16, UInt16, Int32, UInt32, Int64och UInt64;
      • En Enum som backas upp av någon av ovanstående primitiva integraltyper;
      • Char, Int128, UInt128, Half, Single, Double, , IntPtr. UIntPtr
  3. ✔️ Använd fält-för-fält inläsningar och lagringsserialisering/deserialisering i stället. Överväg att använda populära och säkra bibliotek för (de)serialisering.

13. Nullhanterade pekare

Vanligtvis är byrefs (hanterade pekare) sällan null och det enda säkra sättet att skapa en null byref från och med idag är att initiera en ref struct med default. Sedan är alla dess ref fält nullhanterade pekare:

RefStructWithRefField s = default;
ref byte nullRef = ref s.refFld;

Det finns dock flera osäkra sätt att skapa null-byrefs. Vissa exempel inkluderar:

// 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);

Risken för problem med minnessäkerhet är låg, och alla försök att avreferera en null byref leder till en tydligt definierad NullReferenceException. C#-kompilatorn förutsätter dock att avreferering av en byref alltid lyckas och inte ger någon observerbar bieffekt. Därför är det en tillåten optimering att utelämna alla dereference vars resultat omedelbart ignoreras. Se dotnet/runtime#98681 (och den här relaterade kommentaren) för ett exempel på en nu åtgärdad bugg i .NET där bibliotekskod felaktigt förlitade sig på avrefereringen som utlöser en bieffekt, omedveten om att C#-kompilatorn effektivt förkortar logiken.

Recommendations

  1. ❌ Skapa inte null-byrefs i C# om det inte är nödvändigt. Överväg att använda vanliga hanterade referenser, null-objektmönstret eller tomma intervall i stället.
  2. ❌ Ignorera inte resultatet av en byref-dereference eftersom den kan optimeras och leda till potentiella buggar.

14. stackalloc

stackalloc har tidigare använts för att skapa små, icke-escapeing matriser på stacken, vilket minskar GC-trycket. I framtiden kan JIT:s Escape Analysis börja optimera icke-escapeing GC-allokeringar av matriser till stackobjekt, vilket kan göra stackalloc redundant. Tills dess fortsätter stackalloc att vara användbart för att allokera små buffertar på stacken. För större buffertar eller utrymningsbuffertar kombineras det ofta med ArrayPool<T>.

Recommendations

  1. ✔️ Använd alltid stackalloc på vänster sida av uttrycket med ReadOnlySpan<T>/Span<T> för att tillhandahålla gränskontroller.

    // 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.
    
  2. ❌ Använd inte stackalloc i loopar. Stackutrymmet frigörs inte förrän metoden returnerar, så att inkludera en stackalloc inuti en loop kan resultera i att processen avslutas på grund av stacköverskridning.

  3. ❌ Använd inte stora längder för stackalloc. Till exempel kan 1 024 byte betraktas som en rimlig övre gräns.

  4. ✔️ Kontrollera intervallet med variabler som används som stackalloc längder.

    void ProblematicCode(int length)
    {
        Span<int> s = stackalloc int[length]; // Bad practice: check the range of `length`!
        Consume(s);
    }
    

    Fixad version:

    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);
    }
    
  5. ✔️ Använd moderna C#-funktioner som samlingsliteraler (Span<int> s = [1, 2, 3];), params Span<T>och infogade matriser för att undvika manuell minneshantering när det är möjligt.

15. Buffertar med fast storlek

Buffertar med fast storlek var användbara för interop-scenarier med datakällor från andra språk eller plattformar. De ersattes sedan av säkrare och bekvämare infogade matriser. Ett exempel på en buffert med fast storlek (kräver unsafe kontext) är följande kodfragment:

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.

Ett modernt och säkrare alternativ är infogade matriser:

[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.

En annan anledning till att undvika buffertar med fast storlek till förmån för infogade matriser, som alltid är nollinitierade som standard, är att buffertar med fast storlek kan ha icke-nollat innehåll i vissa scenarier.

Recommendations

  1. ✔️ Byt helst ut buffertar med fast storlek mot infogade matriser eller IL-marshallingattribut där det är möjligt.

16. Överföra sammanhängande data som pekare + längder (eller förlita sig på nollterminering)

Undvik att definiera API:er som accepterar ohanterade eller hanterade pekare till sammanhängande data. Använd Span<T> i stället eller 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);

Nollterminering är särskilt riskabelt eftersom inte alla buffertar är nollterminerade, och om du läser förbi någon nollterminator kan det leda till avslöjande av information, att data skadas eller att processen avslutas via ett åtkomstfel.

Recommendations

  1. ❌ Exponera inte metoder vars argument är pekartyper (ohanterade pekare T* eller hanterade pekare ref T) när argumenten är avsedda att representera buffertar. Använd säkra bufferttyper som Span<T> eller ReadOnlySpan<T> i stället.

  2. ❌ Använd inte implicita kontrakt för byref-argument, till exempel att kräva att alla anropare allokerar indata i stacken. Om ett sådant kontrakt är nödvändigt bör du överväga att använda referens struct i stället.

  3. ❌ Anta inte att buffertar är nollterminerade om inte scenariot uttryckligen dokumenterar att detta är en giltig förutsättning. Även om .NET garanterar att string instanser och "..."u8 literaler är null-terminerade, gäller detta inte för andra bufferttyper såsom ReadOnlySpan<char> eller char[].

    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];
        }
    }
    
  4. ❌ Skicka inte en pinnad Span<char> eller ReadOnlySpan<char> över ett p/invoke-gränssnitt om du inte också har skickat ett explicit längdargument. Annars kan koden på andra sidan p/invoke-gränsen felaktigt tro att bufferten är null-avslutad.

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);
    }
}

Lös detta genom att använda en alternativ p/invoke-signatur som accepterar både datapekaren och längden om möjligt. Annars, om mottagaren inte har något sätt att acceptera ett separat längdargument, se till att de ursprungliga data konverteras till en string innan du fäster dem och skickar dem över p/invoke-gränsen.

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. Strängmutationer

Strängar i C# kan inte ändras avsiktligt, och alla försök att mutera dem med osäker kod kan leda till odefinierat beteende. Exempel:

string s = "Hello";
fixed (char* p = s)
{
    p[0] = '_';
}
Console.WriteLine("Hello"); // prints "_ello" instead of "Hello"

Om du ändrar en intern sträng (de flesta strängliteraler) ändras värdet för alla andra användningsområden. Även utan stränginternering bör att skriva till en nyskapad sträng ersättas med det säkrare String.Create-API:et.

// 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

  1. ❌ Mutera inte strängar. Använd API:et String.Create för att skapa en ny sträng om komplex kopieringslogik behövs. Använd annars .ToString(), StringBuilder, new string(...) eller syntaxen för stränginterpolering.

18. Raw IL-kod (till exempel System.Reflection.Emit och Mono.Cecil)

Att generera rå IL (antingen via System.Reflection.Emit, bibliotek från tredje part, till exempel Mono.Cecil, eller skriva IL-kod direkt) kringgår per definition alla minnessäkerhetsgarantier som C# ger. Undvik att använda sådana tekniker om det inte är absolut nödvändigt.

Recommendations

  1. ❌ Generera inte rå IL-kod eftersom den inte levereras med några ledstänger och det gör det enkelt att införa typsäkerhet och andra problem. Precis som andra tekniker för dynamisk kodgenerering är det inte heller AOT-vänligt att generera rå-IL om det inte görs vid byggtiden.
  2. ✔️ Använd källgeneratorer i stället, om möjligt.
  3. ✔️ Föredra [UnsafeAccessor] i stället för att använda rå IL för att skriva kod för serialisering med låg overhead för privata medlemmar vid behov.
  4. ✔️ Skicka ett API-förslag mot dotnet/runtime om något API saknas och du tvingas använda rå IL-kod i stället.
  5. ✔️ Använd ilverify eller liknande verktyg för att verifiera den genererade IL-koden om du måste använda rå-IL.

19. Oinitierade lokala variabler [SkipLocalsInit] och Unsafe.SkipInit

[SkipLocalsInit] introducerades i .NET 5.0 så att JIT kan hoppa över nollning av lokala variabler i metoder, antingen per metod eller modulomfattande. Den här funktionen användes ofta för att hjälpa JIT att eliminera redundanta nollinitieringar, till exempel de för stackalloc. Det kan dock leda till odefinierat beteende om lokala variabler inte initieras uttryckligen före användning. Med de senaste förbättringarna i JIT:s förmåga att eliminera nollinitieringar och utföra vektorisering har behovet [SkipLocalsInit] av och Unsafe.SkipInit minskat avsevärt.

Recommendations

  1. ❌ Använd inte [SkipLocalsInit] och Unsafe.SkipInit om inga prestandafördelar i kritisk kod märks eller om du inte är säker på de risker de medför.
  2. Koda försiktigt när du använder API:er som GC.AllocateUninitializedArray och ArrayPool<T>.Shared.Rent, som på liknande sätt kan returnera oinitialiserade buffertar.

20. ArrayPool<T>.Shared och liknande pool-API:er

ArrayPool<T>.Shared är en delad pool med matriser som används för att minska GC-trycket i intensivt använd kod. Den används ofta för att allokera tillfälliga buffertar för I/O-åtgärder eller andra kortvariga scenarier. API:et är enkelt och innehåller inte osäkra funktioner, men det kan leda till use-after-free buggar i C#. Exempel:

var buffer = ArrayPool<byte>.Shared.Rent(1024);
_buffer = buffer; // buffer object escapes the scope
Use(buffer);
ArrayPool<byte>.Shared.Return(buffer);

All användning av _buffer efter Return samtalet är en användningsfri bugg. Det här minimala exemplet är enkelt att upptäcka, men buggen blir svårare att identifiera när Rent och Return finns i olika omfång eller metoder.

Recommendations

  1. ✔️ Håll matchade anrop till Rent och Return inom samma metod om möjligt för att begränsa omfattningen av potentiella buggar.
  2. ❌ Använd inte ett try-finally mönster för att anropa Return i finally blocket om du inte är säker på att den misslyckade logiken har slutförts med hjälp av bufferten. Det är bättre att överge bufferten i stället för att riskera en "use-after-free"-bugg på grund av ett oväntat tidigt Return.
  3. ✔️ Tänk på att liknande problem kan uppstå med andra pool-API:er eller mönster, till exempel ObjectPool<T>.

21. bool<->int konverteringar

Även om ECMA-335-standarden definierar ett booleskt värde som 0–255 där är ett värde som true inte är noll, är det bättre att undvika explicita konverteringar mellan heltal och booleska värden för att undvika att införa "avnormaliserade" värden som något annat än 0 eller 1 som sannolikt leder till opålitligt beteende.

// 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 som fanns i tidigare .NET-körningar optimerade inte helt den säkra versionen av den här logiken, vilket resulterade i att utvecklare använde osäkra konstruktioner för att konvertera mellan bool och int i prestandakänsliga kodsökvägar. Detta är inte längre fallet, och moderna .NET JITs kan optimera den säkra versionen effektivt.

Recommendations

  1. ❌ Skriv inte "grenlösa" konverteringar mellan heltal och booleska värden med osäker kod.
  2. ✔️ Använd ternary-operatorer (eller annan förgreningslogik) i stället. Moderna .NET JITs optimerar dem effektivt.
  3. ❌ Läs bool inte med osäkra API:er, till exempel Unsafe.ReadUnaligned eller MemoryMarshal.Cast om du inte litar på indata. Överväg att använda ternary-operatorer eller likhetsjämförelser i stället:
// 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; }

Mer information finns i Binär (de)serialisering av strukturer med utfyllnad eller icke-blittbara medlemmar.

22. Interop

Även om de flesta av förslagen i det här dokumentet även gäller för interop-scenarier rekommenderar vi att du följer den interna guiden för bästa praxis för samverkan . Överväg också att använda automatiskt genererade interop-omslutningar som CsWin32 och CsWinRT. Detta minimerar behovet av att skriva manuell interop-kod och minskar risken för problem med minnessäkerhet.

23. Trådsäkerhet

Minnessäkerhet och trådsäkerhet är ortoggoniska begrepp. Koden kan vara minnessäker men innehåller fortfarande dataraser, sönderrivna läsningar eller synlighetsbuggar. omvänt kan kod vara trådsäker samtidigt som odefinierat beteende anropas genom osäker minnesmanipulering. Mer information finns i Metodtips för hanterad trådning och .NET-minnesmodell.

24. Osäker kod kring SIMD/vektorisering

Mer information finns i Riktlinjer för vektorisering . När det gäller den osäkra koden är det viktigt att tänka på följande:

  • SIMD-åtgärder har komplexa krav för att tillhandahålla atomitetsgarantier (ibland tillhandahåller de dem inte alls).
  • De flesta API:er för SIMD-inläsning/lagring tillhandahåller inte gränskontroller.

25. Fuzz-testning

Fuzz-testning (eller "fuzzing") är en automatiserad teknik för programvarutestning som omfattar att tillhandahålla ogiltiga, oväntade eller slumpmässiga data som indata till ett datorprogram. Det ger ett sätt att identifiera minnessäkerhetsproblem i kod som kan ha luckor i testtäckningen. Du kan använda verktyg som SharpFuzz för att konfigurera fuzz-testning för .NET-kod.

26. Kompilatorvarningar

I allmänhet ger C#-kompilatorn inte omfattande stöd, till exempel varningar och analysverktyg kring felaktig användning av osäker kod. Det finns dock vissa befintliga varningar som kan hjälpa till att identifiera potentiella problem och bör inte ignoreras eller undertryckas utan noggrant övervägande. Vissa exempel inkluderar:

nint ptr = 0;
unsafe
{
    int local = 0;
    ptr = (nint)(&local);
}
await Task.Delay(100);

// ptr is used here

Den här koden genererar varnings-CS9123 ("Operatorn '&' bör inte användas på parametrar eller lokala variabler i asynkrona metoder"), vilket innebär att koden sannolikt är felaktig.

Recommendations

  1. ✔️ Var uppmärksam på kompilatorvarningar och åtgärda de underliggande problemen i stället för att ignorera dem.
  2. ❌ Anta inte att avsaknaden av kompilatorvarningar innebär att koden är korrekt. C#-kompilatorn har begränsat till inget stöd för att identifiera felaktig osäker kodanvändning.

Referenser