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:
- Unmanaged pointer in the signature — already caller-unsafe today, so excluded from the list below.
- 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.
- May access uninitialized memory — reinterpreting raw bytes / a generic
T whose struct padding & alignment gaps may leak.
- May set the value of a potentially unsafe field in an unknown generic
T without an unsafe context.
- 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
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.CryptographySystem.Diagnostics.StackTraceSystem.SpeechSystem.Transactions.LocalSystem.Diagnostics.PerformanceCounterSystem.Memory(SequenceMarshal)System.Security.Principal.WindowsSystem.ServiceProcess.ServiceControllerAPIs 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 rawIntPtr/nintaddress, or that reinterpret raw bytes as a genericT.I'm using a diff format where
+indicates that it's now caller-unsafe (gains theunsafemodifier on the method) and-indicates theunsafemodifier 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-levelunsafemodifier on members.Criteria
The
//comment on each line references one of the following. An API is marked caller-unsafe if it meets any of:IntPtr/nintaddress or an arbitraryref byte.Twhose struct padding & alignment gaps may leak.Twithout an unsafe context.BORDERLINEmarks members where the criterion applies indirectly (e.g. a raw native/COMIntPtrthe implementation dereferences, or anobject-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-levelunsaferequirement is enforced.Alternative Designs
No response
Risks
No response