-
Notifications
You must be signed in to change notification settings - Fork 169
First draft: caller unsafe #330
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
5cfeb39
9e6e730
4522b7c
66d883c
dde56cc
322ea0e
dd98d2b
8e5657a
656ade9
ab87699
c70550e
eff6303
76f661b
7173c95
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,60 @@ | ||||||
|
|
||||||
| # Annotating unsafe code with RequiresUnsafe | ||||||
|
|
||||||
| ## 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. | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
could be good to clarify that if
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Correct. Even pointers themselves will now be safe, only dereference is unsafe.
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. | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
|
||||||
| ## Goals | ||||||
|
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 | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about for a binary? Should we set
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||||||
|
|
||||||
| ## Proposal | ||||||
|
|
||||||
| We need to be able to annotate code as unsafe, even if it doesn't use pointers. | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the critical part.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
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.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm a bit lost here.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Propagating I think the key point is to make the customers aware of unsafety at use site.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is also a widely-seen formalism to disallow |
||||||
|
|
||||||
| 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 | ||||||
|
agocke marked this conversation as resolved.
Outdated
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. | ||||||
|
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 | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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,
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Nearly all memory safety bugs can crash the runtime. For example, incorrect use of
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it actually possible to guarantee this ( Basically, I think we can guarantee memory safety if no |
||||||
|
|
||||||
| 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. | ||||||
|
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: | ||||||
|
agocke marked this conversation as resolved.
Outdated
|
||||||
|
|
||||||
| * ArrayPool.Rent | ||||||
|
agocke marked this conversation as resolved.
Outdated
|
||||||
| * The `stackalloc` C# feature used with `SkipLocalsInit` and no initializer | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||||||
|
|
||||||
|
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). | ||||||
Uh oh!
There was an error while loading. Please reload this page.