Skip to content

[API Proposal]: Batch #1 of caller-unsafe APIs #129751

Description

@EgorBo

Background and motivation

Follow-up to #126956, #127098, #128075 and #129750. This proposal batches the remaining new caller-unsafe public APIs found across several smaller reference assemblies into a single issue (each one is small on its own):

  • System.Runtime (ArgIterator, GC, RuntimeMethodHandle, TypedReference, RuntimeHelpers, GCHandle, SafeBuffer)
  • System.Security.Cryptography
  • System.Diagnostics.StackTrace
  • System.Speech
  • System.Transactions.Local
  • System.Diagnostics.PerformanceCounter
  • System.Memory (SequenceMarshal)
  • System.Security.Principal.Windows
  • System.ServiceProcess.ServiceController

APIs that already have an unmanaged pointer (void*/T*) in their signature are excluded — they are already effectively caller-unsafe today. This proposal covers only the new additions: APIs that read/write through a raw IntPtr/nint address, or that reinterpret raw bytes as a generic T.

I'm using a diff format where + indicates that it's now caller-unsafe (gains the unsafe modifier on the method) and - indicates the unsafe modifier has been removed (a non-breaking change). Each + line keeps a short // comment citing which criterion (below) it satisfies.

The explicit [RequiresUnsafe] attribute is being removed in favor of the language-level unsafe modifier on members.

Criteria

The // comment on each line references one of the following. An API is marked caller-unsafe if it meets any of:

  1. Unmanaged pointer in the signature — already caller-unsafe today, so excluded from the list below.
  2. May access memory the process may not own, without safety checks — e.g. dereferencing a caller-supplied IntPtr/nint address or an arbitrary ref byte.
  3. May access uninitialized memory — reinterpreting raw bytes / a generic T whose struct padding & alignment gaps may leak.
  4. May set the value of a potentially unsafe field in an unknown generic T without an unsafe context.
  5. May produce a misaligned pointer from a properly aligned input.

BORDERLINE marks members where the criterion applies indirectly (e.g. a raw native/COM IntPtr the implementation dereferences, or an object-relative offset overload).

API Proposal

namespace System;

public struct ArgIterator
{
+   public unsafe System.TypedReference GetNextArg();                           // BORDERLINE crit 2: walks a native va_list, can read past the real args
+   public unsafe System.TypedReference GetNextArg(System.RuntimeTypeHandle rth); // BORDERLINE crit 2: reinterprets next vararg slot as caller-chosen type
+   public unsafe System.RuntimeTypeHandle GetNextArgType();                    // BORDERLINE crit 2: advances a native va_list cursor
}
namespace System;

public static class GC
{
+   public static unsafe T[] AllocateUninitializedArray<T>(int length, bool pinned = false); // BORDERLINE crit 3: exposes uninitialized heap bytes (info disclosure)
}
namespace System;

public struct RuntimeMethodHandle
{
+   public unsafe System.IntPtr GetFunctionPointer();                  // BORDERLINE: returns a callable code address; invoking it is the unsafe op
}
namespace System;

public ref struct TypedReference
{
+   public static unsafe System.TypedReference MakeTypedReference(object target, System.Reflection.FieldInfo[] flds); // BORDERLINE: fabricates a managed ref to a field chain
+   public static unsafe void SetTypedReference(System.TypedReference target, object? value);                         // BORDERLINE: stores through a fabricated managed ref
}
namespace System.Runtime.CompilerServices;

public static class RuntimeHelpers
{
+   public static unsafe object? Box(ref byte target, System.RuntimeTypeHandle type);                           // crit 2+3: reinterpret-reads SizeOf(type) bytes from an arbitrary ref (may read unowned/uninit memory)
}
namespace System.Runtime.InteropServices;

public abstract class SafeBuffer
{
+   public unsafe T Read<T>(ulong byteOffset) where T : struct;                                 // crit 2+3: reinterprets native bytes as T (may read unowned/padding bytes)
+   public unsafe void ReadArray<T>(ulong byteOffset, T[] array, int index, int count) where T : struct; // crit 2+3: reinterprets native bytes as T[]
+   public unsafe void ReadSpan<T>(ulong byteOffset, System.Span<T> buffer) where T : struct;   // crit 2+3: reinterprets native bytes as Span<T>
+   public unsafe void Write<T>(ulong byteOffset, T value) where T : struct;                    // crit 3+4: writes T bytes (incl. padding) to native memory
+   public unsafe void WriteArray<T>(ulong byteOffset, T[] array, int index, int count) where T : struct; // crit 3+4: writes T[] bytes to native memory
+   public unsafe void WriteSpan<T>(ulong byteOffset, System.ReadOnlySpan<T> data) where T : struct;      // crit 3+4: writes Span<T> bytes to native memory
}
namespace System.Security.Cryptography;

public sealed partial class DSAOpenSsl : System.Security.Cryptography.DSA
{
+   public unsafe DSAOpenSsl(System.IntPtr handle); // BORDERLINE crit 2: dereferences a caller-supplied raw EVP_PKEY* pointer with no ownership/validity check
}
namespace System.Security.Cryptography;

public sealed partial class ECDiffieHellmanOpenSsl : System.Security.Cryptography.ECDiffieHellman
{
+   public unsafe ECDiffieHellmanOpenSsl(System.IntPtr handle); // BORDERLINE crit 2: dereferences a caller-supplied raw EVP_PKEY* pointer with no ownership/validity check
}
namespace System.Security.Cryptography;

public sealed partial class ECDsaOpenSsl : System.Security.Cryptography.ECDsa
{
+   public unsafe ECDsaOpenSsl(System.IntPtr handle); // BORDERLINE crit 2: dereferences a caller-supplied raw EVP_PKEY* pointer with no ownership/validity check
}
namespace System.Security.Cryptography;

public sealed partial class RSAOpenSsl : System.Security.Cryptography.RSA
{
+   public unsafe RSAOpenSsl(System.IntPtr handle); // BORDERLINE crit 2: dereferences a caller-supplied raw EVP_PKEY* pointer with no ownership/validity check
}
namespace System.Security.Cryptography.X509Certificates;

public partial class X509Certificate : System.IDisposable, System.Runtime.Serialization.IDeserializationCallback, System.Runtime.Serialization.ISerializable
{
+   public unsafe X509Certificate(System.IntPtr handle); // BORDERLINE crit 2: dereferences a caller-supplied raw PCCERT_CONTEXT pointer with no ownership/validity check
}
namespace System.Security.Cryptography.X509Certificates;

public partial class X509Certificate2 : System.Security.Cryptography.X509Certificates.X509Certificate
{
+   public unsafe X509Certificate2(System.IntPtr handle); // BORDERLINE crit 2: dereferences a caller-supplied raw PCCERT_CONTEXT pointer with no ownership/validity check
}
namespace System.Security.Cryptography.X509Certificates;

public partial class X509Chain : System.IDisposable
{
+   public unsafe X509Chain(System.IntPtr chainContext); // BORDERLINE crit 2: dereferences a caller-supplied raw PCCERT_CHAIN_CONTEXT pointer with no ownership/validity check
}
namespace System.Security.Cryptography.X509Certificates;

public sealed partial class X509Store : System.IDisposable
{
+   public unsafe X509Store(System.IntPtr storeHandle); // BORDERLINE crit 2: dereferences a caller-supplied raw HCERTSTORE handle/pointer with no ownership/validity check
}
namespace System.Diagnostics.SymbolStore;

public partial interface ISymbolBinder1
{
+   unsafe System.Diagnostics.SymbolStore.ISymbolReader? GetReader(System.IntPtr importer, string filename, string searchPath); // BORDERLINE crit 2: dereferences a raw native/COM importer interface pointer
}
namespace System.Diagnostics.SymbolStore;

public partial interface ISymbolWriter
{
+   unsafe void Initialize(System.IntPtr emitter, string filename, bool fFullBuild); // BORDERLINE crit 2: dereferences a raw native/COM emitter interface pointer
+   unsafe void SetUnderlyingWriter(System.IntPtr underlyingWriter); // BORDERLINE crit 2: dereferences a raw native/COM writer interface pointer
}
namespace System.Speech.Synthesis.TtsEngine;

public partial interface ITtsEngineSite
{
+   unsafe int Write(System.IntPtr data, int count); // crit 2: writes `count` bytes read from a raw caller-supplied IntPtr address, no bounds/ownership check
}
namespace System.Speech.Synthesis.TtsEngine;

public abstract partial class TtsEngineSsml
{
+   public abstract unsafe System.IntPtr GetOutputFormat(System.Speech.Synthesis.TtsEngine.SpeakOutputFormat speakOutputFormat, System.IntPtr targetWaveFormat); // BORDERLINE crit 2: reads a wave-format structure through the raw targetWaveFormat IntPtr
+   public abstract unsafe void Speak(System.Speech.Synthesis.TtsEngine.TextFragment[] fragment, System.IntPtr waveHeader, System.Speech.Synthesis.TtsEngine.ITtsEngineSite site); // BORDERLINE crit 2: writes audio through the raw waveHeader IntPtr address, no bounds/ownership check
}
namespace System.Transactions;

public partial interface IDtcTransaction
{
+   unsafe void Abort(System.IntPtr reason, int retaining, int async); // BORDERLINE crit 2: COM interface, reads abort-reason struct through a raw caller-supplied IntPtr
+   unsafe void GetTransactionInfo(System.IntPtr transactionInformation); // BORDERLINE crit 2: COM interface, writes XACTTRANSINFO through a raw caller-supplied IntPtr
}
namespace System.Diagnostics;

public partial interface ICollectData
{
+   unsafe void CollectData(int id, System.IntPtr valueName, System.IntPtr data, int totalBytes, out System.IntPtr res); // BORDERLINE crit 2: reads/writes performance data through raw caller-supplied IntPtr addresses, no bounds/ownership check
}
namespace System.Runtime.InteropServices;

public static partial class SequenceMarshal
{
+   public static unsafe bool TryRead<T>(ref System.Buffers.SequenceReader<byte> reader, out T value) where T : unmanaged; // BORDERLINE crit 3: reinterprets raw bytes from the reader as an unmanaged generic T (padding/alignment), MemoryMarshal.Read-style
}
namespace System.Security.Principal;

public sealed partial class SecurityIdentifier : System.Security.Principal.IdentityReference, System.IComparable<System.Security.Principal.SecurityIdentifier>
{
+   public unsafe SecurityIdentifier(System.IntPtr binaryForm); // crit 2: reads the binary SID through a raw caller-supplied address, no bounds/ownership check
}
namespace System.ServiceProcess;

public partial class ServiceBase : System.ComponentModel.Component
{
+   public unsafe void ServiceMainCallback(int argCount, System.IntPtr argPointer); // crit 2: dereferences a raw caller-supplied IntPtr argument vector
}

API Usage

The annotated APIs will require unsafe { } context at their call site once the language-level unsafe requirement is enforced.

Alternative Designs

No response

Risks

No response

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions