From 5cfeb3937018172bd0eab2abdb21de577e8eadf5 Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Fri, 7 Mar 2025 14:30:28 -0800 Subject: [PATCH 01/14] First draft: caller unsafe --- proposed/caller-unsafe.md | 60 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 proposed/caller-unsafe.md diff --git a/proposed/caller-unsafe.md b/proposed/caller-unsafe.md new file mode 100644 index 000000000..551049e20 --- /dev/null +++ b/proposed/caller-unsafe.md @@ -0,0 +1,60 @@ + +# Annotating unsafe code with CallerUnsafe + +## 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. + +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. + +## Goals + +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 + +## Proposal + +We need to be able to annotate code as unsafe, even if it doesn't use pointers. + +Mechanically, this would be done with a new attribute: + +```C# +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Constructor)] +public sealed class CallerUnsafe : System.Attribute +{ + 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 `CallerUnsafe` 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, `ShowAllCallerUnsafeWarnings`, is set to true. + +### Implementation + +Since only warnings are an output of the above design, the feature could be implemented as an analyzer for C#. + +### Definition of Unsafe + +`CallerUnsafe` 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 +* No access to uninitialized memory + +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. + +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: + +* ArrayPool.Rent +* The `stackalloc` C# feature used with `SkipLocalsInit` and no initializer + +### 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). \ No newline at end of file From 9e6e73044dbbaf7cbc9d083e1d9f5e0319677427 Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Fri, 7 Mar 2025 17:43:11 -0800 Subject: [PATCH 02/14] Rename `CallerUnsafe` to `RequiresUnsafe` in documentation --- proposed/caller-unsafe.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/proposed/caller-unsafe.md b/proposed/caller-unsafe.md index 551049e20..7bb296896 100644 --- a/proposed/caller-unsafe.md +++ b/proposed/caller-unsafe.md @@ -1,5 +1,5 @@ -# Annotating unsafe code with CallerUnsafe +# Annotating unsafe code with RequiresUnsafe ## Background @@ -25,15 +25,15 @@ Mechanically, this would be done with a new attribute: ```C# [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Constructor)] -public sealed class CallerUnsafe : System.Attribute +public sealed class RequiresUnsafe : System.Attribute { 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 `CallerUnsafe` 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. +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, `ShowAllCallerUnsafeWarnings`, is set to true. +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. ### Implementation @@ -41,7 +41,7 @@ Since only warnings are an output of the above design, the feature could be impl ### Definition of Unsafe -`CallerUnsafe` 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: +`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 * No access to uninitialized memory From 4522b7c7aaee8aee4e5e4c4a264943d2e7923a52 Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Tue, 18 Mar 2025 11:56:54 -0700 Subject: [PATCH 03/14] Respond to PR comments --- proposed/caller-unsafe.md | 77 ++++++++++++++++++++++++++++++--------- 1 file changed, 60 insertions(+), 17 deletions(-) diff --git a/proposed/caller-unsafe.md b/proposed/caller-unsafe.md index 7bb296896..dea902fcc 100644 --- a/proposed/caller-unsafe.md +++ b/proposed/caller-unsafe.md @@ -21,40 +21,83 @@ We want to achieve all of the following goals: We need to be able to annotate code as unsafe, even if it doesn't use pointers. -Mechanically, this would be done with a new attribute: +Mechanically, this would be done with a modification to the C# language and a new property to the compilation. When the compilation property "EnableRequiresUnsafe" is set to true, the `unsafe` keyword on C# _members_ would require that the call appear in an unsafe context. An `unsafe` block would be unchanged -- the statements in the block would be in an unsafe context, while the code outside would have no requirements. + +For example, the code below would produce an error: ```C# -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Constructor)] -public sealed class RequiresUnsafe : System.Attribute +void Caller() { - public WarnByDefault { get; init; } = true; + M(); // error, the call to M() is not in an unsafe context } + +unsafe void M() { } ``` -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. +This can be addressed by callers in two ways: + +```C# +unsafe void Caller1() +{ + M(); +} +void Caller2() +{ + unsafe + { + M(); + } +} +unsafe void M() { } +``` + +In the case of `Caller1`, the call to `M()` doesn't produce an error because it is inside an unsafe context. However, calls to `Caller1` will now produce an error for the same reason as `M()`. + +`Caller2` will also not produce an error because `M()` is in an unsafe context. However, this code creates a responsibility for the programmer: by presenting a safe API around an unsafe call, they are asserting that all safety concerns of `M()` have been addressed. + +Notably, unsafe did not change the requirement that the code in the block must be correct. It merely offset the responsibility from the language and the runtime to the user in verification. -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. +For more precise details on the error semantics of unsafe blocks and unsafe members, the rules will mirror 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). The only addition is the presence of the `unsafe` block, which effectively provides a local `Requires` context. -### Implementation +## Implementation -Since only warnings are an output of the above design, the feature could be implemented as an analyzer for C#. +In addition to compiler enforcement, the following attribute will be added for annotating unsafe members. It is an error to use this attribute directly in C#. Instead, the `unsafe` keyword should be used. -### Definition of Unsafe +```C# +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Constructor)] +public sealed class RequiresUnsafeAttribute : System.Attribute +{ +} +``` + +### Globally-valid properties -`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: +The overall goal is to ensure .NET code is "valid," in the sense that certain properties are always true. Generating a complete list of such properties is out of scope of this document. However, at least the following properties are required: * Memory safety * No access to uninitialized memory -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. +In this document **memory safety** is strictly defined as: 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. + +No access to uninitialized memory means that all memory is either never read before it has been initialized, or it has been initialized to a zero value. + +These properties are particularly important for security purposes. Any methods that can potentially violate these guarantees should be unsafe. Additionally, all such methods should have documentation that describes what conditions need to be satisfied to ensure that these guarentees are preserved. + +These properties are guaranteed by "safe" code through a combination of compiler and .NET runtime enforcement. For `unsafe` code, these properties must be guaranteed by the programmer. + +`unsafe` members are used to identify the places that cannot be automatically checked by the compiler and runtime for validity. Inside unsafe blocks, the programmer is responsible for ensuring that all requirements of the unsafe code are met, and that all code outside the block will have validity properly enforced by the system. + + +### Examples and APIs + +**ArrayPool.Rent** -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. +This method is unsafe because it returns an array with unintialized memory. Code must not read the contents of the returned array without initialization. -Some examples of APIs or features that are unsafe due to exposing uninitialized memory include: +**stackalloc** -* ArrayPool.Rent -* The `stackalloc` C# feature used with `SkipLocalsInit` and no initializer +This language feature is unsafe if used in a `SkipLocalsInit` context because the stack allocated buffer is uninitialized. If an initializer is used and the converted type is `Span`, this code is safe. -### Detailed semantics +**P/Invoke** -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). \ No newline at end of file +All P/Invoke methods are unsafe because they may compromise memory safety if the callee function does not match the P/Invoke method specification. \ No newline at end of file From 66d883c12d3c9411b474a9dfd8ce1b1c5db8040b Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Tue, 18 Mar 2025 11:57:41 -0700 Subject: [PATCH 04/14] PR comments --- proposed/caller-unsafe.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposed/caller-unsafe.md b/proposed/caller-unsafe.md index dea902fcc..c4ff9fdad 100644 --- a/proposed/caller-unsafe.md +++ b/proposed/caller-unsafe.md @@ -1,5 +1,5 @@ -# Annotating unsafe code with RequiresUnsafe +# Annotating members as `unsafe` ## Background From dde56cce92899a6b668083790a392d243e954cb8 Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Tue, 18 Mar 2025 13:31:16 -0700 Subject: [PATCH 05/14] Clarify memory safety definition. --- proposed/caller-unsafe.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposed/caller-unsafe.md b/proposed/caller-unsafe.md index c4ff9fdad..1b450d84e 100644 --- a/proposed/caller-unsafe.md +++ b/proposed/caller-unsafe.md @@ -77,7 +77,7 @@ The overall goal is to ensure .NET code is "valid," in the sense that certain pr * Memory safety * No access to uninitialized memory -In this document **memory safety** is strictly defined as: 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. +In this document **memory safety** is strictly defined as: 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, or memory that is acquired for legal use by the runtime through any other means. No access to uninitialized memory means that all memory is either never read before it has been initialized, or it has been initialized to a zero value. From 322ea0e54c52c3ccda5f3229d4484c438f5fc928 Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Wed, 19 Mar 2025 00:24:30 -0700 Subject: [PATCH 06/14] Update proposed/caller-unsafe.md Co-authored-by: Jan Kotas --- proposed/caller-unsafe.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposed/caller-unsafe.md b/proposed/caller-unsafe.md index 1b450d84e..d546e3599 100644 --- a/proposed/caller-unsafe.md +++ b/proposed/caller-unsafe.md @@ -77,7 +77,7 @@ The overall goal is to ensure .NET code is "valid," in the sense that certain pr * Memory safety * No access to uninitialized memory -In this document **memory safety** is strictly defined as: 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, or memory that is acquired for legal use by the runtime through any other means. +In this document **memory safety** is strictly defined as: code can never acquire a reference to memory that is not managed by the runtime. "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, or memory that is acquired for legal use by the runtime through any other means. No access to uninitialized memory means that all memory is either never read before it has been initialized, or it has been initialized to a zero value. From dd98d2b844a16ed1e54af91618e9c108a9dabb00 Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Wed, 19 Mar 2025 00:26:12 -0700 Subject: [PATCH 07/14] Update proposed/caller-unsafe.md Co-authored-by: Jan Kotas --- proposed/caller-unsafe.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposed/caller-unsafe.md b/proposed/caller-unsafe.md index d546e3599..f34fae507 100644 --- a/proposed/caller-unsafe.md +++ b/proposed/caller-unsafe.md @@ -21,7 +21,7 @@ We want to achieve all of the following goals: We need to be able to annotate code as unsafe, even if it doesn't use pointers. -Mechanically, this would be done with a modification to the C# language and a new property to the compilation. When the compilation property "EnableRequiresUnsafe" is set to true, the `unsafe` keyword on C# _members_ would require that the call appear in an unsafe context. An `unsafe` block would be unchanged -- the statements in the block would be in an unsafe context, while the code outside would have no requirements. +Mechanically, this would be done with a modification to the C# language and a new property to the compilation. When the compilation property "EnableRequiresUnsafe" is set to true, the `unsafe` keyword on C# _members_ would require that their uses appear in an unsafe context. An `unsafe` block would be unchanged -- the statements in the block would be in an unsafe context, while the code outside would have no requirements. For example, the code below would produce an error: From 8e5657a6387667d90d88cf71779c24b6a9c886ea Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Fri, 18 Apr 2025 11:56:01 -0700 Subject: [PATCH 08/14] Fix typos and clarify memory safety definition --- proposed/caller-unsafe.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/proposed/caller-unsafe.md b/proposed/caller-unsafe.md index f34fae507..2caeebacf 100644 --- a/proposed/caller-unsafe.md +++ b/proposed/caller-unsafe.md @@ -5,7 +5,7 @@ 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. -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 @@ -77,7 +77,7 @@ The overall goal is to ensure .NET code is "valid," in the sense that certain pr * Memory safety * No access to uninitialized memory -In this document **memory safety** is strictly defined as: code can never acquire a reference to memory that is not managed by the runtime. "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, or memory that is acquired for legal use by the runtime through any other means. +In this document **memory safety** is strictly defined as: code never accesses memory that is not managed by the runtime. "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, or memory that is acquired for legal use by the runtime through any other means. No access to uninitialized memory means that all memory is either never read before it has been initialized, or it has been initialized to a zero value. From 656ade997fceb32d6ff1c3ebd5a8ef10e798361e Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Thu, 24 Apr 2025 11:10:39 -0700 Subject: [PATCH 09/14] Update caller-unsafe.md --- proposed/caller-unsafe.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/proposed/caller-unsafe.md b/proposed/caller-unsafe.md index 2caeebacf..afbbc13d9 100644 --- a/proposed/caller-unsafe.md +++ b/proposed/caller-unsafe.md @@ -79,7 +79,7 @@ The overall goal is to ensure .NET code is "valid," in the sense that certain pr In this document **memory safety** is strictly defined as: code never accesses memory that is not managed by the runtime. "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, or memory that is acquired for legal use by the runtime through any other means. -No access to uninitialized memory means that all memory is either never read before it has been initialized, or it has been initialized to a zero value. +No access to uninitialized memory means that all memory is either never read before it has been initialized, or it has been initialized to a zero value. This term is defined relative to the application context, not the system context. Therefore, allocating memory using a mechanism where .NET guarantees that the memory has been zeroed would be considered initialized. Reading memory from a function which does not guarantee zero initialization would not. Reusing memory that has previously been initialized but now may hold an unknown value due to previous application execution may be incorrect logic from the application's perspective, but is not invalid code according to .NET. These properties are particularly important for security purposes. Any methods that can potentially violate these guarantees should be unsafe. Additionally, all such methods should have documentation that describes what conditions need to be satisfied to ensure that these guarentees are preserved. @@ -100,4 +100,4 @@ This language feature is unsafe if used in a `SkipLocalsInit` context because th **P/Invoke** -All P/Invoke methods are unsafe because they may compromise memory safety if the callee function does not match the P/Invoke method specification. \ No newline at end of file +All P/Invoke methods are unsafe because they may compromise memory safety if the callee function does not match the P/Invoke method specification. From ab8769917255df34074ed2ef2ed50ab983dce3ad Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Tue, 23 Sep 2025 15:42:35 -0700 Subject: [PATCH 10/14] Update caller-unsafe.md --- proposed/caller-unsafe.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposed/caller-unsafe.md b/proposed/caller-unsafe.md index afbbc13d9..c282db09a 100644 --- a/proposed/caller-unsafe.md +++ b/proposed/caller-unsafe.md @@ -79,7 +79,7 @@ The overall goal is to ensure .NET code is "valid," in the sense that certain pr In this document **memory safety** is strictly defined as: code never accesses memory that is not managed by the runtime. "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, or memory that is acquired for legal use by the runtime through any other means. -No access to uninitialized memory means that all memory is either never read before it has been initialized, or it has been initialized to a zero value. This term is defined relative to the application context, not the system context. Therefore, allocating memory using a mechanism where .NET guarantees that the memory has been zeroed would be considered initialized. Reading memory from a function which does not guarantee zero initialization would not. Reusing memory that has previously been initialized but now may hold an unknown value due to previous application execution may be incorrect logic from the application's perspective, but is not invalid code according to .NET. +No access to uninitialized memory means that all memory is either never read before it has been initialized, or it has been initialized to a zero value. This term is defined relative to the application context, not the system context. Therefore, allocating memory using a mechanism where .NET guarantees that the memory has been zeroed would be considered initialized. Reading memory from a function which does not guarantee zero initialization would not. Reusing memory that has previously been initialized but now may hold an unknown value due to previous application execution is considered safe. This may be incorrect logic from the application's perspective, but is valid code according to .NET. These properties are particularly important for security purposes. Any methods that can potentially violate these guarantees should be unsafe. Additionally, all such methods should have documentation that describes what conditions need to be satisfied to ensure that these guarentees are preserved. From c70550e20e0d63b884bb8970e006f44a67256d56 Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Sun, 5 Oct 2025 23:42:25 -0700 Subject: [PATCH 11/14] Apply suggestion from @KathleenDollard Co-authored-by: Kathleen Dollard --- proposed/caller-unsafe.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposed/caller-unsafe.md b/proposed/caller-unsafe.md index c282db09a..8900efd69 100644 --- a/proposed/caller-unsafe.md +++ b/proposed/caller-unsafe.md @@ -3,7 +3,7 @@ ## 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. +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 compiler error that would otherwise occur. 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. From eff6303eaba190c3e674d52c86f37345175c7094 Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Mon, 6 Oct 2025 00:07:25 -0700 Subject: [PATCH 12/14] Clarify 'unsafe' keyword usage and global invariants Added details on the usage of the 'unsafe' keyword and its context in .NET. --- proposed/caller-unsafe.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/proposed/caller-unsafe.md b/proposed/caller-unsafe.md index 8900efd69..5ff3e340e 100644 --- a/proposed/caller-unsafe.md +++ b/proposed/caller-unsafe.md @@ -57,7 +57,15 @@ In the case of `Caller1`, the call to `M()` doesn't produce an error because it Notably, unsafe did not change the requirement that the code in the block must be correct. It merely offset the responsibility from the language and the runtime to the user in verification. -For more precise details on the error semantics of unsafe blocks and unsafe members, the rules will mirror 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). The only addition is the presence of the `unsafe` block, which effectively provides a local `Requires` context. +### Details + +When the feature is enabled, the `unsafe` keyword will now only be allowed in the following places: + + * As a modifier in a method or local function declaration + * As part of the "unsafe block" syntax + * As a modifier on property declarations + +As detailed below, pointer types themselves are no longer unsafe, only pointer dereferences. Therefore, `unsafe` is only necessary to annotate executable code. ## Implementation @@ -70,7 +78,7 @@ public sealed class RequiresUnsafeAttribute : System.Attribute } ``` -### Globally-valid properties +### Global invariants The overall goal is to ensure .NET code is "valid," in the sense that certain properties are always true. Generating a complete list of such properties is out of scope of this document. However, at least the following properties are required: From 76f661b10b0bf16f36855a83d8bf133a97fed6e2 Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Mon, 6 Oct 2025 11:47:59 -0700 Subject: [PATCH 13/14] Refine memory safety definitions in caller-unsafe.md Clarify the definition of uninitialized memory and its safety in .NET context. --- proposed/caller-unsafe.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/proposed/caller-unsafe.md b/proposed/caller-unsafe.md index 5ff3e340e..41fdd4639 100644 --- a/proposed/caller-unsafe.md +++ b/proposed/caller-unsafe.md @@ -87,7 +87,9 @@ The overall goal is to ensure .NET code is "valid," in the sense that certain pr In this document **memory safety** is strictly defined as: code never accesses memory that is not managed by the runtime. "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, or memory that is acquired for legal use by the runtime through any other means. -No access to uninitialized memory means that all memory is either never read before it has been initialized, or it has been initialized to a zero value. This term is defined relative to the application context, not the system context. Therefore, allocating memory using a mechanism where .NET guarantees that the memory has been zeroed would be considered initialized. Reading memory from a function which does not guarantee zero initialization would not. Reusing memory that has previously been initialized but now may hold an unknown value due to previous application execution is considered safe. This may be incorrect logic from the application's perspective, but is valid code according to .NET. +No access to uninitialized memory means that all memory is either never read before it has been initialized, or it has been initialized to a zero value. "Uninitialized memory" means memory which contains an undefined value. Memory that has been zeroed by the runtime will always have a defined zero value. Memory initialized by the user will have a defined value if the user code has defined behavior. Memory subject to race conditions in .NET managed code has multiple potential defined values, which is considered safe. + +Therefore, allocating zeroed memory using .NET safe methods is considered safe. This would include things like the .NET `new` operator and `NativeMemory.AllocZeroed`. Reading memory from a function which does not guarantee zero initialization would not, like `NativeMemory.Alloc`. Reusing memory that has previously been initialized but now may hold an unknown value due to previous application execution is considered safe. This includes object pooling functions. Bugs concerning incorrect reuse of pooled objects are not considered memory safety issues and the `unsafe` feature will not provide further protection against these vulnerabilities. These properties are particularly important for security purposes. Any methods that can potentially violate these guarantees should be unsafe. Additionally, all such methods should have documentation that describes what conditions need to be satisfied to ensure that these guarentees are preserved. From 7173c950855d5e2a7fba395ac606c404b44ad425 Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Mon, 6 Oct 2025 11:49:25 -0700 Subject: [PATCH 14/14] Apply suggestion from @jkotas Co-authored-by: Jan Kotas --- proposed/caller-unsafe.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposed/caller-unsafe.md b/proposed/caller-unsafe.md index 41fdd4639..7af5a0510 100644 --- a/proposed/caller-unsafe.md +++ b/proposed/caller-unsafe.md @@ -3,7 +3,7 @@ ## 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 compiler error that would otherwise occur. +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 a compiler error that would otherwise occur. 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.