Skip to content
Merged
60 changes: 60 additions & 0 deletions proposed/caller-unsafe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@

# Annotating unsafe code with RequiresUnsafe
Comment thread
agocke marked this conversation as resolved.
Outdated

## Background

C# has had the `unsafe` feature since 1.0. There are two different syntaxes for the feature: a block syntax that can be used inside methods and a modifier that can appear on members and types. The original semantics only concern pointers. An error is produced if a variable of pointer type appears outside an unsafe context. For the block syntax, this is anywhere inside the block; for members this is inside the member; for types this is anywhere inside the type. Pointer operations are not fully validated by the type system, so this feature is useful at identifying areas of code needing more validation. Unsafe has subsequently been augmented to also turn off lifetime checking for ref-like variables, but the fundamental semantics are unchanged -- the `unsafe` context serves only to avoid an error that would otherwise occur.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for members this is inside the member

could be good to clarify that if unsafe is on the member, it has an effect on the member's signature as well

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original semantics only concern pointers.

It also included things adjacent, like sizeof(T). As part of this effort, I'd like us to revisit those choices. Based on the direction in this doc, seems sizeof(T) should not be unsafe.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. Even pointers themselves will now be safe, only dereference is unsafe.

Comment thread
agocke marked this conversation as resolved.
Outdated

While existing `unsafe` is useful, it is limited by only applying to pointers and ref-like lifetimes. Many methods may be considered unsafe, but the unsafety may not be related to pointers or ref-like lifetimes. For example, almost all methods in the System.RuntimeServices.CompilerServices.Unsafe class has the same safety issues as pointers, but do not require an `unsafe` block. The same is true of the System.RuntimeServices.CompilerServices.Marshal class.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
While existing `unsafe` is useful, it is limited by only applying to pointers and ref-like lifetimes. Many methods may be considered unsafe, but the unsafety may not be related to pointers or ref-like lifetimes. For example, almost all methods in the System.RuntimeServices.CompilerServices.Unsafe class has the same safety issues as pointers, but do not require an `unsafe` block. The same is true of the System.RuntimeServices.CompilerServices.Marshal class.
While existing `unsafe` is useful, it is limited by only applying to pointers and ref-like lifetimes. Many methods may be considered unsafe, but the unsafety may not be related to pointers or ref-like lifetimes. For example, almost all methods in the System.RuntimeServices.CompilerServices.Unsafe class have the same safety issues as pointers, but do not require an `unsafe` block. The same is true of the System.RuntimeServices.CompilerServices.Marshal class.


## Goals
Comment thread
agocke marked this conversation as resolved.

The existing unsafe system does not clearly identify which methods need hand-verification to use safely, and it doesn't indicate which methods claim to provide that verification (and produce a safe API).

We want to achieve all of the following goals:

* Clearly annotate which methods require extra verification to use safely
* Clearly identify in source code which methods are responsible for performing extra verification
* Provide an easy way to identify in source code if a project doesn't use unsafe code
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about for a binary? Should we set UnverifiableCodeAttribute or similar?

Copy link
Copy Markdown
Member Author

@agocke agocke Mar 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. In this design it’s allowed to define a RequiresUnsafe method without an unsafe block if you never actually call the method. If you did, there would be an unsafe block and the assembly would have unverifiable code. I’m not sure if defining such a method, but never using it except through other callers unsafe methods, should be unverifiable.

My inclination is yes: anything that contains unsafe code should be marked, even if it’s not called from safe code.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sentence says project. That should be very simple to achieve: does the project have AllowUnsafeBlocks or not? That is the barrier today for allowing unsafe usage and that should not change. If the desire is to detect in the binary of we had this attribute or not then yes we'd need to add more metadata.


## Proposal

We need to be able to annotate code as unsafe, even if it doesn't use pointers.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the critical part.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assuming the rules are going to be the same for everyone (BCL / SDKs / everybody else), wouldn't we need the opposite as well?

We need to be able to annotate code as safe, even if it uses pointers.

Otherwise it seems to me that this will become viral (similar to the trimming attributes), where a lot of APIs are considered unsafe, because they eventually do something unsafe in some code path.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That’s what unsafe does today. It lets you call unsafe things, but has no requirements on the callers.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit lost here.
Does this mean that to produce "safe" packages I'll have to get rid of all the code the uses Unsafe.As, CollectionsMarshal and others?
How can I "guaranty" to others that my implementation IS safe (even if I use a ImmutableCollectionsLarshal.AsImmutableArray in a single place in my code base)?
Will all my packages be categorized as "unsafe" because in one low-level assembly I do use a Unsafe.As?
Also, I fully agree with @rolfbjarne: FieldOffset are unsafe...
So the question is: what code base remains on the "safe" side?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That’s what unsafe does today. It lets you call unsafe things, but has no requirements on the callers.

Ah, I (think I) see, this new attribute would need to be added manually, it won't be added automatically whenever someone uses unsafe code.

Copy link
Copy Markdown
Member

@EgorBo EgorBo Mar 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit lost here. Does this mean that to produce "safe" packages I'll have to get rid of all the code the uses Unsafe.As, CollectionsMarshal and others? How can I "guaranty" to others that my implementation IS safe (even if I use a ImmutableCollectionsLarshal.AsImmutableArray in a single place in my code base)? Will all my packages be categorized as "unsafe" because in one low-level assembly I do use a Unsafe.As? Also, I fully agree with @rolfbjarne: FieldOffset are unsafe... So the question is: what code base remains on the "safe" side?

Does this doc mention marking packages as unsafe? I don't see it. It's perfectly fine to have unsafe code, it just needs to be recognized as such at this point.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@olivier-spinelli -- Fair question. As @EgorBo says, that's not what is being communicated. In terms of helping people understand what we're thinking, this was probably a poor doc to go first. Expect some higher-level docs that better explain a plan.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Propagating unsafe warnings too much would be very disappointing and cause developers disabling the new unsafe mechanism at all. We should take acceptance into consideration.

I think the key point is to make the customers aware of unsafety at use site.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is also a widely-seen formalism to disallow unsafe syntax totally, instead using the Marshal family which is inefficient and typically more unsafe. We should provide a path for moving people away from that.


Mechanically, this would be done with a new attribute:

```C#
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Constructor)]
public sealed class RequiresUnsafe : System.Attribute
Comment thread
agocke marked this conversation as resolved.
Outdated
Comment thread
agocke marked this conversation as resolved.
Outdated
{
public WarnByDefault { get; init; } = true;
}
```

This would be the dual of the existing `unsafe` context. The `unsafe` context eliminates unsafe errors, indicating that the code outside of the unsafe context should be considered safe. The `RequiresUnsafe` attribute does the opposite: it indicates that the code inside is unsafe unless specific obligations are met. These obligations cannot be represented in C#, so they must be listed in documentation and manually verified by the user of the unsafe code. Only when all obligations are discharged can the code be wrapped in an `unsafe` context to eliminate any warnings.

The `WarnByDefault` flag is needed for backwards-compatibility. If an existing API is unsafe, adding warnings would be a breaking change. If `WarnByDefault` is set to `false`, warnings are not produced unless a project-level property, `ShowAllRequiresUnsafeWarnings`, is set to true.
Comment thread
agocke marked this conversation as resolved.
Outdated

### Implementation

Since only warnings are an output of the above design, the feature could be implemented as an analyzer for C#.

### Definition of Unsafe

`RequiresUnsafe` is only useful if there is an accepted definition of what is considered unsafe. For .NET there are two properties that we already consider safe code to preserve:

* Memory safety
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's crucial to differentiate the unsafety levels. Type safety and GC safety should get highest severity because of the ability to break runtime. There are continuous tickets for inappropriate unsafe API usage causing hard-to-diagnose bugs.

For example, Unsafe.AsPointer is super-unsafe and strictly audited in runtime repo. Unsafe.As between unmanaged types are "safer" and frequently used in buffer management. There should be a mechanism to disallow the former only.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type safety and GC safety should get highest severity because of the ability to break runtime

Nearly all memory safety bugs can crash the runtime. For example, incorrect use of NativeMemory.Alloc/Free can cause very hard to diagnose crash in the runtime code as well.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The safety property I suggested, memory safety, is derived from an analysis of severe security breaches across the industry. Things in this category are known to be very dangerous and award attackers with significant power. I don’t think there’s further subdivision of this category that is interesting. Unsafe.As and Unsafe.AsPointer are both capable of producing memory safety problems and are therefore both unsafe.

The only other property I’m suggesting is viewing uninitialized memory. This is less severe. We could drop it if necessary. At the moment I’m proposing no other properties that would be covered by the unsafe feature and therefore further categorization is irrelevant.

* No access to uninitialized memory
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it actually possible to guarantee this (No access to uninitialized memory) w.r.t. the above paragraph? e.g., you can write an ArrayPool without any unsafe code, with the only difference being that it would have to be initialized each time it creates a new array for sure; but that doesn't stop the fact that it effectively contains uninitialized values, for the purpose of that usage, on any subsequent rent of that array - if this is not counted as unsafe, then ArrayPool.Shared.Rent could be made not unsafe-callers-only for the same reason as my custom ArrayPool above, solely by replacing usages of AllocateUninitializedArray with new T[] (& any other similar things, if any), which definitely feels against the intent of the definition of "No access to uninitialized memory". I don't know if I'm missing something, but it seems impossible to guarantee these certain properties are achieved when a developer writes only safe code in full generality with these particular definitions, unless we blame the unsafe-callers-only of ArrayPool.Rent solely on APIs like AllocateUninitializedArray rather than also the fact that they're not guaranteed to get cleared on return & re-rent.

Basically, I think we can guarantee memory safety if no unsafe code exists, but I don't think we can guarantee "no access to uninitialized memory", depending on how it's defined (specifically, whether we only care about the first allocation of the array, or also subsequent uses of the array when re-renting not being zeroed). If the intent is that we only care about the first allocation, it would be good to explicitly mention this, as I don't think that's immediately clear from the document that that's for sure the intent.


In this document **memory safety** is strictly defined as: safe code can never acquire a reference to memory that is not managed by the application. "Managed" here does not refer to solely to heap-allocated, garbage collected memory, but also includes stack-allocated variables that are considered allocated by the runtime.
Comment thread
agocke marked this conversation as resolved.
Outdated

No access to uninitialized memory means that all managed memory is either never read before it has been initialized by C# code, or it has been initialized to a zero value.

Some examples of APIs or features that are unsafe due to exposing uninitialized memory include:
Comment thread
agocke marked this conversation as resolved.
Outdated

* ArrayPool.Rent
Comment thread
agocke marked this conversation as resolved.
Outdated
* The `stackalloc` C# feature used with `SkipLocalsInit` and no initializer
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* The `stackalloc` C# feature used with `SkipLocalsInit` and no initializer
* The `stackalloc` C# feature with no initializer

SkipLocalsInit is irrelevant as stackallocs are not guaranteed to be initialized.

Copy link
Copy Markdown
Member

@EgorBo EgorBo Mar 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's not true. we consider any non-zero-initialized stackalloc as a bug if no SkipLocalsInit is set. We might want to make an adjustment to the ECMA with that.

Copy link
Copy Markdown

@hamarb123 hamarb123 Mar 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what the c# standard specifies, it doesn't guarantee you get localloc-like behaviour which has the required guarantees. If we updated the c# spec, that would work too. Or if roslyn explicitly documented their stackalloc's behaviour, like they do with bool values, that would work too.


Comment thread
agocke marked this conversation as resolved.
### Detailed semantics

The exact rules on when a warning would be produced will follow the rules defined for "Requires" attributes defined in [Feature attribute semantics](https://github.com/dotnet/runtime/blob/main/docs/design/tools/illink/feature-attribute-semantics.md#requiresfeatureattribute).