Historically the unsafe keyword was needed for anything that touched pointer types, even harmless operations such as declaring a pointer or taking an address. That made unsafe blocks large and hid the operations that actually read and write unmanaged memory, which is where memory safety vulnerabilities come from. Marking a method unsafe also told its callers nothing.
C# 15 ties unsafe to the operations that access memory rather than to the existence of pointer types. With LangVersion set to preview the following no longer require an unsafe context:
- Declaring a pointer type and taking the address of a variable with
& - The
fixedstatement - Converting a
stackallocexpression to a pointer - The
sizeofoperator on any unmanaged type
Dereferencing (*p), member access (p->member), element access (p[i]), element access on fixed-size buffers and invoking function pointers still require unsafe, so reviewers can find them easily.
Turning on the updated memory safety rules also changes what unsafe on a member means: the member becomes requires-unsafe and every caller must call it from an unsafe context. C# 15 adds an unsafe(expression) expression that establishes an unsafe context for just one expression, useful where an unsafe block can't be used such as field initializers, constructor initializers and catch filters.
Code
// Needs LangVersion preview, AllowUnsafeBlocks and the updated-memory-safety-rules feature
int number = 42;
int* pointer = &number; // No unsafe context needed
int[] numbers = [10, 20, 30];
fixed (int* first = numbers) // No unsafe context needed
{
unsafe
{
Console.WriteLine(*first); // Dereferencing still needs an unsafe context
}
}
class Header
{
// ReadSignature is requires-unsafe so calling it needs an unsafe context,
// and a field initializer can hold an unsafe expression but not an unsafe block
static readonly int Signature = unsafe(ReadSignature());
// unsafe on the signature passes the audit obligation to callers
static unsafe int ReadSignature()
{
int rawValue = 0x1234;
int* pointer = &rawValue;
unsafe
{
// The modifier no longer makes the body unsafe, so the dereference needs its own block
return *pointer;
}
}
}unsafe
{
int number = 42;
int* pointer = &number;
int[] numbers = [10, 20, 30];
fixed (int* first = numbers)
{
Console.WriteLine(*first);
}
}
class Header
{
// Nothing tells this caller that ReadSignature dereferences a pointer
static readonly int Signature = ReadSignature();
// unsafe here only allowed pointers in the body, callers were unaffected
static unsafe int ReadSignature()
{
int rawValue = 0x1234;
int* pointer = &rawValue;
return *pointer;
}
}Notes
- Set
LangVersiontopreviewto enable the new syntax and pointer relaxations - Enable the
updated-memory-safety-rulescompiler feature to opt in to the updated rules, including requires-unsafe members unsafeexpressions and thesafekeyword need theAllowUnsafeBlockscompiler option- Under the updated rules
unsafeon a member no longer makes its body an unsafe context, so dereferences inside it need their ownunsafeblock - Under the updated rules
unsafeon a type declaration is an error - Under the updated rules every
externmember, and every field of a type with explicit or extended layout, must be marked with the newsafecontextual keyword or withunsafe - The compiler records that an assembly uses the updated rules with
System.Runtime.CompilerServices.MemorySafetyRulesAttribute