AI coding assistants can generate C# code remarkably quickly.
They can produce:
That speed creates an important review problem.
A developer may receive code that compiles successfully but still contains assumptions about memory ownership, buffer length, pointer lifetime, or native resources that require careful human verification.
C# already provides an unsafe context for operations that cannot be verified as memory-safe by the .NET runtime. C# 15 and .NET 11 introduce an updated memory-safety model in preview that makes the safety obligation more explicit at member boundaries. In this model, an unsafe member can communicate an obligation to its callers rather than treating unsafety as something that exists only inside the method body.
This makes C# 15 particularly interesting for AI-generated code review.
The question is no longer simply:
Does the generated code compile?
A stronger question is:
What safety assumptions does this generated API
require every caller to understand and preserve?
Why AI-Generated Unsafe Code Needs Extra Review
Consider this generated method:
public static unsafe int ReadValue(int* pointer)
{
return *pointer;
}
The implementation is short.
But what does the caller need to guarantee?
Is pointer null?
Does it point to valid memory?
Is the memory still alive?
Is the memory readable?
Is the pointer correctly aligned?
Is the referenced object pinned?
The compiler cannot infer all of those business-level and ownership assumptions.
This is the central auditing problem:
Unsafe code moves some safety responsibility from the runtime to the developer.
Microsoft explicitly notes that unsafe code introduces security and stability risks because the runtime cannot verify the safety of those operations.
What C# Normally Protects You From
Most C# code uses managed memory:
var customer = new Customer();
var orders = new List<Order>();
var buffer = new byte[4096];
The runtime manages object lifetime and provides memory-safety guarantees for ordinary managed operations.
Unsafe code can instead work with:
int* pointer;
byte* buffer;
delegate* unmanaged<void> callback;
Now the developer must reason about memory directly.
That is why an AI-generated unsafe method should be treated differently from ordinary generated business logic.
C# 15 Introduces a Refined Memory-Safety Model
The updated C# 15 memory-safety model separates two concepts that were previously more closely coupled:
Pointer exists
≠
Memory is accessed unsafely
Under the preview model, simply declaring a pointer or taking an address does not necessarily require an unsafe context.
Operations that actually access unmanaged memory, such as pointer dereference or pointer element access, still require unsafe handling.
For example:
int value = 42;
int* pointer = &value;
can be permitted under the preview model without the same broad unsafe context required by the legacy rules.
But:
int result = *pointer;
still represents an operation that requires unsafe handling.
This distinction is important when auditing generated code because the presence of a pointer does not by itself tell you where the actual safety boundary exists.
The New Idea: Unsafe as a Contract
The updated model introduces an important concept:
unsafe member
↓
caller has a safety obligation
This means an API can communicate that callers must understand a safety contract.
Microsoft's documentation describes unsafe on a member under the updated model as a caller-facing obligation to audit safety.
Consider:
public static unsafe byte ReadByte(
byte* buffer,
int offset)
{
return buffer[offset];
}
An AI-generated implementation may look reasonable.
But the API contract is incomplete unless callers understand:
buffer must point to valid readable memory
offset must be within the valid buffer
the memory must remain valid during access
Document the Safety Contract
The C# 15 preview documentation describes a <safety> documentation block for explicitly documenting caller obligations.
For example:
/// <summary>
/// Reads one byte from unmanaged memory.
/// </summary>
/// <safety>
/// The pointer must reference a readable buffer and
/// offset must identify a valid byte within that buffer.
/// </safety>
public static unsafe byte ReadByte(
byte* buffer,
int offset)
{
return buffer[offset];
}
This is valuable when reviewing AI-generated code because the reviewer can compare:
Implementation
vs.
Declared safety assumptions
If the contract cannot be clearly stated, the API probably needs additional design work.
Add Internal SAFETY Comments
The preview model also describes using // SAFETY: comments inside unsafe regions to explain why a particular operation is considered valid.
For example:
public static byte ReadAt(
byte* buffer,
int length,
int index)
{
ArgumentOutOfRangeException.ThrowIfNegative(index);
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(
index,
length);
unsafe
{
// SAFETY: bounds checks ensure index is within
// the caller-provided buffer length.
return buffer[index];
}
}
The important distinction is:
<safety>
=
API-level caller contract
// SAFETY:
=
Implementation-level justification
For AI-generated unsafe code, both provide useful review points.
Audit the Boundary, Not Just the Unsafe Block
A common mistake is reviewing only this:
unsafe
{
// pointer operations
}
The actual security boundary may exist earlier.
For example:
public static unsafe void Process(
byte* buffer,
int length)
{
Validate(buffer, length);
// unsafe operation
}
The reviewer should verify:
Who owns buffer?
Who guarantees length?
How was buffer allocated?
How long does it remain valid?
Can another operation invalidate it?
Unsafe code auditing is therefore an API-contract problem, not simply a syntax problem.
AI-Generated Pointer Code Checklist
When an AI generates pointer-based code, inspect:
| Area | Question |
|---|
| Nullability | Can the pointer be null? |
| Bounds | Is every access bounded? |
| Lifetime | Is referenced memory still valid? |
| Ownership | Who owns the memory? |
| Pinning | Does managed memory need to remain pinned? |
| Alignment | Is the pointer correctly aligned? |
| Initialization | Can memory contain uninitialized data? |
| Threading | Can another thread modify or release the memory? |
| Interop | Does the native API expect a different layout? |
| Cleanup | Who releases unmanaged resources? |
This checklist is more valuable than simply searching for the unsafe keyword.
Review stackalloc Carefully
AI coding tools frequently use stackalloc when generating performance-oriented code.
For example:
Span<byte> buffer = stackalloc byte[256];
This can be safe when used correctly.
However, generated code should still be reviewed for:
Allocation size
Lifetime
Escaping references
Uninitialized memory
Data sensitivity
The C# 15 preview specifically changes which stackalloc operations require unsafe context, but that does not eliminate the need to reason about the memory itself.
Avoid Unbounded Stack Allocation
This is dangerous design:
Span<byte> buffer =
stackalloc byte[userSuppliedSize];
Even if the code is syntactically valid, an attacker-controlled size can create excessive stack usage.
Prefer explicit limits:
const int MaxBufferSize = 4096;
if (size is < 0 or > MaxBufferSize)
{
throw new ArgumentOutOfRangeException(nameof(size));
}
Span<byte> buffer = stackalloc byte[size];
The generated code should be evaluated against the application's threat model rather than accepted simply because it compiles.
Audit Native Interop
AI assistants may generate P/Invoke code such as:
[DllImport("native.dll")]
private static extern int Process(
byte* buffer,
int length);
This creates another safety boundary.
Review:
Calling convention
Character encoding
Struct layout
Parameter types
Buffer length
Ownership
Return values
Error handling
Library loading
Platform compatibility
An incorrect native signature can cause memory corruption even when the C# code looks correct.
C# 15 Makes extern Safety More Explicit
Under the updated memory-safety model, extern members must explicitly communicate their safety contract. Microsoft documents safe and unsafe handling for extern declarations as part of the preview model.
This matters for generated interop code because the implementation of an extern member cannot be verified by the C# compiler.
An AI-generated declaration therefore deserves careful review.
Review Function Pointers
Generated code may use:
delegate* unmanaged<int, int> callback;
Function pointers can be appropriate for specialized interop or performance scenarios.
But reviewers should verify:
Calling convention
Signature compatibility
Lifetime
Nullability
Native ownership
Invocation context
The compiler cannot turn an incorrect native contract into a safe one.
Async and Unsafe Code Need Special Attention
Unsafe operations and asynchronous methods have important restrictions.
For example, taking the address of parameters or local variables in an async method can be problematic because the lifetime and storage location of those variables changes around suspension points. Microsoft documents compiler diagnostics such as CS9123 for this case.
A safer design is to isolate the unsafe operation:
public async Task ProcessAsync(
CancellationToken cancellationToken)
{
var data = await LoadDataAsync(
cancellationToken);
ProcessBuffer(data);
}
private static unsafe void ProcessBuffer(
byte[] data)
{
fixed (byte* pointer = data)
{
// SAFETY:
// The array remains pinned for this block.
ProcessNative(pointer, data.Length);
}
}
The async method remains separate from the unsafe operation.
Review fixed Usage
Generated code may contain:
fixed (byte* pointer = buffer)
{
Process(pointer);
}
The key question is whether all pointer operations remain within the fixed lifetime.
Do not move pointer usage outside the block:
byte* pointer;
fixed (byte* p = buffer)
{
pointer = p;
}
Process(pointer);
The pointer's validity after the fixed region is not guaranteed in the way the generated code assumes.
Audit Memory Ownership
A recurring AI-generated-code problem is unclear ownership.
For example:
IntPtr memory = Marshal.AllocHGlobal(size);
Who calls:
Marshal.FreeHGlobal(memory);
If the answer is unclear, the implementation is incomplete.
A robust wrapper should make ownership explicit:
public sealed class NativeBuffer : IDisposable
{
private IntPtr _memory;
private bool _disposed;
public NativeBuffer(int size)
{
_memory = Marshal.AllocHGlobal(size);
}
public void Dispose()
{
if (_disposed)
return;
Marshal.FreeHGlobal(_memory);
_memory = IntPtr.Zero;
_disposed = true;
}
}
AI-generated native resource wrappers should always be reviewed for cleanup paths.
Use Safe Managed Abstractions Where Possible
Unsafe code is not automatically better-performing.
If the requirement can be implemented safely using:
Span<T>
ReadOnlySpan<T>
Memory<T>
ArrayPool<T>
MemoryMarshal
SafeHandle
prefer the abstraction that provides stronger safety guarantees.
For example:
static void Copy(
ReadOnlySpan<byte> source,
Span<byte> destination)
{
source.CopyTo(destination);
}
is easier to reason about than a manually implemented pointer loop.
The purpose of an unsafe implementation should be clear.
Do Not Let AI Optimize Into Unsafe Code Automatically
A prompt such as:
"Make this code as fast as possible."
can encourage an AI coding assistant to introduce:
Pointers
stackalloc
Unsafe APIs
Native interop
Manual memory management
The optimization objective should therefore include constraints:
Prefer safe managed APIs.
Use unsafe code only when required.
Document memory ownership.
Document caller safety requirements.
Add tests for boundary conditions.
This gives the coding agent a narrower design space.
Test Safety Contracts
Unsafe APIs need more than happy-path tests.
For example:
[Fact]
public void ReadAt_RejectsNegativeIndex()
{
var exception = Assert.Throws<ArgumentOutOfRangeException>(
() => ReadAt(buffer, length, -1));
Assert.Equal("index", exception.ParamName);
}
Also test:
Index == 0
Index == length - 1
Index == length
Index > length
Null pointer
Zero-length buffer
Maximum supported size
Disposed resource
The exact cases depend on the API contract.
Use Static Analysis and Compiler Diagnostics
The C# compiler already exposes diagnostics around the updated unsafe model, including caller requirements and unsafe-member contract violations. Examples include CS9362, CS9364, CS9365, CS9366, CS9377, and CS9389.
These diagnostics are particularly useful when auditing generated code.
Do not suppress them simply to make generated code compile.
Investigate why the compiler is reporting the violation.
Unsafe Member Inheritance Matters
The updated model protects safety expectations across inheritance.
For example, an unsafe member should not override a safe base member. Microsoft documents this through diagnostic CS9364.
Likewise, an unsafe implementation should not silently replace a safe interface contract.
This is important when AI generates implementations from existing interfaces.
The generated implementation must preserve the safety contract expected by callers.
Example: Safe Interface, Unsafe Implementation
Suppose:
public interface IDataReader
{
byte ReadByte(int index);
}
The interface communicates a normal safe API.
An AI-generated implementation should not secretly turn the implementation into a caller-unsafe operation.
The interface contract is part of the architecture.
This is exactly the type of boundary the updated C# memory-safety model is designed to make more explicit.
Create an Unsafe-Code Review Gate
A practical CI workflow can be:
AI-generated change
↓
Build
↓
Compiler diagnostics
↓
Static analysis
↓
Unsafe-code detection
↓
Security review
↓
Tests
↓
Merge
You can also flag files containing:
unsafe
*
stackalloc
fixed
DllImport
LibraryImport
delegate*
Marshal.AllocHGlobal
Marshal.FreeHGlobal
The objective is not necessarily to reject all of them.
The objective is to ensure they receive appropriate review.
Add a Simple Unsafe-Code Audit Checklist
Before merging AI-generated unsafe code:
[ ] Why is unsafe code required?
[ ] Can a safe API solve the problem?
[ ] Is the memory lifetime documented?
[ ] Are bounds validated?
[ ] Is ownership explicit?
[ ] Are unmanaged resources released?
[ ] Are async boundaries safe?
[ ] Are native signatures correct?
[ ] Are caller obligations documented?
[ ] Are unsafe blocks minimal?
[ ] Are boundary cases tested?
[ ] Are compiler warnings understood?
This checklist can become part of a pull-request template.
Common Mistakes
Assuming AI-Generated Code Is Safe Because It Compiles
Compilation verifies language correctness, not every memory-safety invariant.
Using unsafe for Ordinary Performance Work
Try safe APIs such as Span<T> before introducing pointers.
Omitting Caller Contracts
Unsafe APIs need clear safety assumptions.
Returning Pointers Without Ownership Rules
The caller must know who owns and maintains the referenced memory.
Using Pointers Across await
Keep unsafe operations isolated from asynchronous suspension points.
Ignoring Native ABI Details
Incorrect interop signatures can cause serious runtime failures.
Suppressing C# 15 Safety Diagnostics
Diagnostics should be investigated rather than hidden.
Treating safe as a Security Guarantee
The C# 15 safe modifier is part of a preview language-level memory-safety contract. It is not a substitute for application security review.
Troubleshooting
The Compiler Says an Unsafe Context Is Required
Under the updated C# 15 model, individual operations can require an unsafe context even when pointer declaration itself does not.
Inspect the exact operation.
For example:
int* pointer = &value;
and:
int result = *pointer;
have different safety requirements under the preview model.
An Unsafe Override Is Rejected
Check the base member.
If the base member is safe, the override must preserve that safety contract.
An Interface Implementation Is Rejected
Verify whether the interface defines a safe member while the generated implementation is unsafe.
The implementation must preserve the interface's caller expectations.
safe Is Not Recognized
The updated memory-safety model is still a preview feature and its implementation has evolved across .NET 11 previews.
Use the appropriate .NET 11 preview SDK and LangVersion=preview when experimenting, and verify the exact compiler version before publishing sample code.
Best Practices
Treat AI-generated unsafe code as security-sensitive code.
Prefer safe managed APIs where practical.
Keep unsafe regions as small as possible.
Document caller safety requirements.
Document why each unsafe operation is valid.
Validate pointer bounds.
Make ownership explicit.
Keep unsafe operations away from async suspension points.
Review P/Invoke and native ABI contracts carefully.
Test boundary conditions.
Investigate compiler safety diagnostics.
Do not suppress warnings without justification.
Review inheritance and interface safety contracts.
Use CI checks to identify unsafe code.
Reassess whether unsafe code is still necessary after refactoring.
Frequently Asked Questions
Is unsafe C# always dangerous?
No.
Microsoft explicitly distinguishes unsafe code from code that is inherently malicious. Unsafe means the runtime cannot verify certain memory-safety properties. It is commonly used for native interoperability and specialized low-level operations.
What changed with C# 15?
C# 15 introduces an updated memory-safety model in preview. It separates pointer existence from operations that actually access unmanaged memory and introduces stronger caller-facing safety contracts for unsafe members.
Is the C# 15 memory-safety model finalized?
No. It is currently documented as a preview feature in C# 15 and .NET 11. The exact behavior should therefore be verified against the compiler/SDK version being used.
Should AI-generated unsafe code always be rejected?
No.
The correct response is additional scrutiny.
If unsafe code is genuinely required, it should have a clear purpose, explicit contracts, bounded operations, tests, and appropriate review.
What is a safety contract?
A safety contract documents the conditions a caller must satisfy for an unsafe operation to be valid.
Can safe APIs completely replace unsafe code?
No.
Native interoperability and specialized low-level scenarios can still require unsafe or unmanaged operations.
The goal is to minimize unnecessary exposure.
Conclusion
AI coding assistants can produce unsafe C# code faster than many developers can manually review it.
That changes the review problem.
The question is not simply:
Does the generated code work?
It is:
What assumptions must remain true
for this code to stay safe?
C# 15's updated memory-safety model provides a useful direction by making safety obligations more explicit at API boundaries. Microsoft documents caller-facing unsafe contracts, safety documentation, compiler diagnostics, and explicit handling of unsafe members as part of the preview model.
For AI-generated code, that creates a practical review strategy:
Generated code
↓
Identify unsafe operations
↓
Understand memory ownership
↓
Document safety contract
↓
Validate bounds and lifetime
↓
Run compiler/static analysis
↓
Test boundary cases
↓
Security review
The most important principle is simple:
Do not review unsafe code by asking only whether the implementation works. Review the assumptions that make the implementation safe.
When AI can generate low-level C# in seconds, explicit safety contracts become an important part of making that generated code understandable, reviewable, and maintainable.