不安全演变

支持者问题:https://github.com/dotnet/csharplang/issues/9704

总结

我们将 C# 的定义 unsafe 从引用指针类型的位置更新为非运行时托管内存的位置。 这些位置是内存不安全发生的地方,负责分类为内存安全问题的大多数 CVE(常见漏洞和暴露)。

// Under the proposed rules:
void M()
{
    int i = 1;
    int* ptr = &i; // Allowed: creating a pointer is not itself unsafe
    unsafe
    {
        Console.WriteLine(*ptr); // Dereference of memory not managed by the runtime. This is unsafe.
        ref int intRef = Unsafe.AsRef(ptr); // Conversion of memory not managed by the runtime to a `ref`. This is unsafe.
    }
}

namespace System.Runtime.CompilerServices
{
    public static class Unsafe
    {
        unsafe public static ref T AsRef<T>(void* source) { /* ... */ } // `unsafe` marks the member as *requires-unsafe*.
    }
}

动机

此外,还可以找到 https://github.com/dotnet/designs/blob/main/accepted/2025/memory-safety/caller-unsafe.md此功能的背景,其中跟踪作为此建议的一部分所需的更广泛的生态系统更改。 其中包括 BCL 更新,以正确批注不安全的方法,以及用于更好地了解内存不安全发生位置的工具更新。 具体而言,对于 C#,我们希望确保语言正确跟踪内存不安全性;目前,可能很难全面查看某个程序,并了解内存不安全发生的所有位置。 这是因为各种帮助程序,如 System.Runtime.CompilerServices.Unsafe, System.Runtime.InteropServices.Marshal和其他人没有表示他们违反内存安全,需要特别考虑。 然后使用这些帮助程序的方法并不明显,在审核内存安全问题的代码时(在进行评审时或尝试确定所报告的漏洞的原因)时,很难确定可能导致问题的位置。

从历史上看, unsafe 在 C# 中,它引用了特定的内存安全洞:指针类型的存在。 不再涉及指针类型的那一刻,C# 非常乐意让内存不安全隐藏在代码中。 正是这个问题,我们希望通过 C# 和.NET生态系统的这种演变unsafe来解决,标记内存不安全可能发生的区域,使审阅者和审核员更容易了解程序中潜在内存不安全的边界。 重要的是,这意味着我们将 改变 它 unsafe的含义,而不仅仅是增加它。 指针的存在本身并不不安全;不安全的操作正在取消引用指针。 这进一步扩展到类型本身;类型本身就不安全。 它只是使用可能不安全的类型的操作,而不是该类型的存在。

为了使此信息流经系统,我们需要有一种方法来将方法本身 unsafe标记为。 目前, unsafe 作为方法修饰符没有外部影响,它只允许在成员的签名和正文中使用指针。 今后, unsafe 作为修饰符,实际上将公开更改成员的含义;它将指示该成员具有内存安全问题,并且必须使用该成员的程序员手动验证任何用法。 这是现有含义 unsafe的扩展: unsafe 在正文上将审核义务本地化为该正文,而 unsafe 签名则向调用方扩展了该义务。

对于 C# 用户群的特定段,这是一项潜在的重大更改。 我们希望,对于我们的许多用户来说,这是有效的透明,更新到新规则将是无缝的。 但是,鉴于一些大型 API 表面(如大部分反射)可能需要进行标记 unsafe,我们认为,新规则可能需要有一个体面的斜坡,以避免完全混淆生态系统。

破坏性更改

更新到实现此语言功能的编译器时,可以观察到以下重大更改。

  • 如果启用了更新的内存安全规则(这可能是默认值,甚至是将来.NET版本中的唯一选项):
    • unsafe 成员现在还将其标记为 “要求不安全”,这意味着调用方必须位于上下文中 unsafe ,并且重写不能是 unsafe 如果基成员是安全的。
    • unsafe在成员或类型上不会自动引入unsafe上下文,这意味着必须围绕unsafe成员正文和初始值设定项中的操作使用显式unsafe块。
    • extern 具有显式或扩展布局、非零打包大小或非零大小的类型的成员和字段需要显式unsafe/safe声明上的关键字。
    • stackalloc 在某些情况下,需要上下文unsafe。
    • unsafe 修饰符是类型声明、静态构造函数和析构函数上的错误,因为它没有任何影响。
  • 在新的 langversion 下:
    • Lambda 推理可能会考虑更多候选项,从而导致重载解析 不明确。
    • safe 现在是上下文关键字,可能会 中断 将其用作类型的代码。
    • 旧模式下的兼容模式可能会导致 更多错误。

详细设计

术语:我们调用成员 需要不安全 (以前称为 调用方不安全)

Syntax

此建议介绍:

  • 下面safe一个新的上下文关键字
  • 和 unsafe 表达式 (unsafe(expression)), 在不安全的表达式中详细说明。

此新语法在新的 LangVersion 下提供,但不管 选择加入,在尝试加入时需要执行的任何操作,都允许在选择加入之前执行任何操作。

safe 关键词

safe修饰符可以应用于允许unsafe将其标记为 requires-unsafe 的所有声明。 不允许对 safe 同一声明应用和 unsafe 修饰符。 适用于 unsafe 和 safe 修饰符的其他限制在 不安全修饰符和上下文部分指定。

编译器需要对具有显式或扩展布局、非零打包大小或非零大小的类型的成员和字段进行显式safe或unsafe修饰extern符。 即使在 safe 不需要声明的情况下(因此对编译器没有影响)也允许由源生成器(例如 LibraryImport)激励。

修饰 safe 符仅将声明标记为 不需要不安全,它不会引入安全上下文。 也没有 safe 块或表达式形式。

关键字的任何 safe 用法都需要 /unsafe 编译器选项(与 MSBuild 项目选项相对应 AllowUnsafeBlocks ),就像 unsafe 关键字一样。 这是因为可将成员置于 safe 块中将成员的实现包装在一个 unsafe 块中。

现有 unsafe 规则

现有的 C# 规范有一个大部分,专用于 unsafe: §24 不安全代码。 它被定义为有条件的规范性,因为有效的 C# 编译器不需要支持该功能 unsafe 。 由于大多数指针的定义本身不再被视为不安全,因此,目前被视为有条件的规范性的大部分内容将不再如此。 指针类型、固定变量和可移动变量、所有指针表达式(指针间接访问、指针成员访问和指针元素访问除外),fixed语句均不再考虑unsafe,并且存在于普通 C# 中,无需在上下文中使用unsafe。 同样,声明 固定大小缓冲区 或初始化 stackalloc 在安全 C# 中也是完全合法的。 对于所有这些情况,它只 访问 不安全的内存。

重要的是,无论程序集是否选择加入更新的内存安全规则,这些指针放宽都适用。 仅实际取消引用或其他直接访问指向内存的操作仍需要 unsafe 上下文。 这将启用增量迁移路径:用户可以在成员内部处理风险时向内移动,或者在调用方必须参与审核之前向内移动 unsafe 来重塑现有代码,然后再翻转程序集范围的选择加入开关。

鉴于此更改固有的代码节和其他部分 C# 规范的广泛重写 unsafe ,因此提供规范现有规则的逐行差异可能并不有用。 相反,我们将概述给定部分中要做出的更改,以及上下文中 unsafe 允许的内容的特定新规则。

重新定义需要不安全上下文的表达式

以下表达式在使用时需要 unsafe 上下文:

除了这些表达式,表达式和语句还可以有条件地要求 unsafe 上下文(如果它们依赖于标记为 unsafe的任何符号)。 例如,调用 需要不安全 的方法将导致 invocation_expression 需要 unsafe 上下文。 嵌入的调用(如 usings foreach和类似)的语句在使用unsafe的成员时,也可能需要上下文。

当我们说“需要不安全的上下文”或本文档中的类似内容时,这意味着发出一个 unsafe 错误,即构造需要使用上下文。

注释

本部分可能需要扩展才能正式声明每个表达式和语句必须考虑的 unsafe 上下文。

指针类型

如前所述,指针本身就不再不安全。 删除 对 §24.3 中不安全上下文的任何引用。 指针类型存在于普通 C# 中,不需要 unsafe 将其引入。 类型定义应作为其他类型处理到 §8.1 及其以下部分。

同样,指针 转换 应转换为 §10,同时删除对 unsafe 上下文的引用。

同样, 指针表达式(指针 间接访问、 指针成员访问和 指针元素访问除外)应处理到 §12 中,同时删除对 unsafe 上下文的引用。 没有有关这些表达式含义的语义变化;唯一 unsafe 的更改是不再需要使用上下文。

对于 指针间接访问、 指针成员访问和 指针元素访问,这些运算符仍然不安全,因为这些访问内存不是托管运行时。 它们保留在 §24 中,并继续要求 unsafe 使用上下文。 上下文之外 unsafe 的任何使用都是错误。 这些运算符的语义没有变化;他们仍然继续意味着他们今天所做的完全相同的事情。 这些表达式必须始终出现在上下文中 unsafe 。

固定语句移动到 §13,删除了对unsafe上下文的引用。

函数指针尚未合并到主要 C# 规范中,但它们同样受到影响;除了函数指针调用,其他所有内容都已移动到标准规范中。 函数指针调用表达式必须始终出现在上下文中 unsafe 。

固定大小的缓冲区

固定大小的缓冲区的故事类似于指针。 固定大小的缓冲区的定义本身并不危险,并移动到 §16.3。 在表达式中访问固定大小的缓冲区同样安全,除非表达式作为primary_expressionelement_access进行;这些缓冲区的计算结果为pointer_element_access(根据上述规则不安全)。

堆栈分配

同样, 堆栈分配 的故事与 指针非常相似。 将 a stackalloc 转换为指针不再不安全;它是该指针不安全的延迟。 但是,我们确实添加了一个新规则:

如果以下所有语句均为 true, 则stackalloc_expression 不安全:

  • stackalloc_expression正在转换为或Span<T>ReadOnlySpan<T>转换 。
  • stackalloc_expression没有stackalloc_initializer。
  • stackalloc_expression在已SkipLocalsInitAttribute应用的成员中使用。

在这些上下文中,生成的堆栈空间可能具有未知的内存内容,并且正转换为一种类型,该类型围绕非托管内存访问提供安全包装。 这违反了合同Span<T>ReadOnlySpan<T>,因此必须受到此类代码的作者和审阅者的额外审查。

与放宽规则的其他更改 unsafe 不同,这是一种紧缩,因此它仅适用于更新的内存安全规则的选择,以避免中断。

注释

这意味着,无论上下文如何,向 stackalloc 指针分配 始终 安全。

请注意,托管类型的一 stackalloc 个仍然是一个错误。

我们可能需要在语言规范 stackalloc 中明确提及指针无法生存 await。 在此功能之前,不允许在方法中使用 async 指针,因此在以前无法观察到。

await在上下文中unsafe

await 上下文中 unsafe 允许表达式。 这允许异步方法直接等待 需要不安全 的操作,而无需先将 awaitable 溢出到临时变量:

async Task M()
{
    unsafe
    {
        await DoUnsafeAsync();
    }
}

unsafe Task DoUnsafeAsync() => ...;

上下文 unsafe 仍表示程序员的手动安全审核边界。 如果指针、函数指针或其他不安全值保留在一个 await值中,则语言不会尝试证明在恢复后使用是安全的;该责任仍由 unsafe 代码作者承担。 这与不安全代码的其余部分一致,它允许在字段存储指针或在引用的生存期结束后使用指针等操作。

然而, await 在语句中 fixed 仍然不允许。 fixed语句在语句的持续时间内固定可移动变量,并允许在语句处于活动状态时挂起需要固定生存期才能跨越异步挂起点。

相应的 csharpstandard 更新位于 §15.15.1 异步函数 > 常规中。 在整个部分中, 删除线 指示从现有规范中删除的文本, 加粗 表示正在添加的文本。

它是异步函数的正式参数列表的编译时错误,用于指定任何 in、 out或 ref 参数或类型的任何参数 ref struct 。

包含表达式(§12.9.8)或语句(§13.15)是不安全上下文(§24.2)await的编译时错误。yield return

对于包含表达式(fixed)的语句(§24.7),这是编译await时错误。

sizeof

对于某些预定义类型, sizeof 始终是常量和安全(§12.8.19),且保持不变。 对于其他类型的, sizeof 用于要求不安全的上下文(§24.6.9),但现在是安全的,无论选择加入更新的内存安全规则。

重写、继承和实现

在最初unsafe的成员的任何重写或实现中添加内存安全错误,因为调用方可能正在使用基本定义,并且不会看到派生实现添加的任何内容unsafe。

委托和 lambda 表达式

将 需要不安全 的成员转换为上下文外部的 unsafe 委托类型是内存安全错误。 委托类型和 函数类型 不能 是需要不安全的。 在 lambda 符号上应用 unsafe 编译时错误。

extern

由于 extern 方法适用于运行时无法保证的本机位置,因此编译器无法判断它们是安全的还是不安全的。 即使是仅按值采用 unmanaged 参数的方法也不能由 C# 安全地调用,因为用于该方法的调用约定可能由用户错误地指定,并且必须通过评审手动验证。

因此,在更新的内存安全规则下,编译器要求将每个 extern 方法显式标记为或 unsafesafe。

extern 使用旧内存安全规则的程序集中的方法不会隐式 unsafe 考虑,因为 extern 被视为不属于公共图面的实现详细信息。 extern 不能保证在引用程序集中保留。

请注意,这与适用于旧规则程序集 的兼容模式 不同,因为签名中具有指针的方法在调用站点上始终需要不安全的上下文。

不安全修饰符和上下文

今天,正如不安全上下文规范所涵盖的那样,unsafe以词法方式行为,将块unsafe包含unsafe的整个文本正文标记为上下文(迭代器正文除外),在声明时也包括一些周围的上下文:

class A : Attribute
{
    public A(object o) { }
}
class C
{
    [A(default(int*[]))] void M1() { } // error: using pointers outside `unsafe` context
    [A(default(int*[]))] unsafe void M2() { } // ok
}

通过选择加入更新的内存安全规则, unsafe 在成员上将其标记为 “要求不安全”,将审核义务扩展到调用方, 并且不会 引入 unsafe 上下文(相反,只有正文中的显式 unsafe 区域建立 unsafe 上下文)。

unsafe 对以下声明生成错误,因为它不再具有意义:

  • delegate,
  • 静态构造函数,
  • 析构函数,
  • 类型声明(class等 struct)。

unsafe 在构造函数上引入 unsafe 其初始值设定项内的上下文,即 unsafe 构造函数可以调用 requires-unsafebase 或 this 构造函数。

在声明位置中,具有无参数 的构造 函数的类型不满足 new() 或 struct 约束。 在表达式位置中,具有无参数的类型需要不安全的构造函数仅在上下文中unsafe满足new()或struct约束。

class Unsafe { public unsafe Unsafe() { } }
class C<T> where T : new();
class D : C<Unsafe> // error: Unsafe cannot satisfy new() in this declaration position
{
    void M()
    {
        _ = new C<Unsafe>(); // error: Unsafe cannot satisfy new() in this expression position
        unsafe { _ = new C<Unsafe>(); } // ok: unsafe context
    }
}

unsafe / safe 成员 上不 应用于成员内的任何嵌套匿名或本地函数。 这同样适用于在块内 unsafe 声明的匿名函数和本地函数(它们仍然一如既往地处于上下文中 unsafe ,但它们不会变得 需要不安全)。 若要将本地函数标记为 requires-unsafe,必须手动将其标记为 unsafe。 不能将 Lambda 标记为 需要不安全 ( unsafe 不允许对它们使用关键字)。

当成员是 partial时,这两个部分必须就修饰符达成一致 unsafe/safe ,这与 C# 规则今天保持不变。

partial class C1
{
    public partial void M1(); // Error: both parts must be unsafe, or neither can be
    public partial unsafe void M2();
}

partial class C1
{
    public unsafe partial void M1() => Console.WriteLine("hello world");
    public partial void M2() => Console.WriteLine("hello world"); // Error: both parts must be unsafe, or neither can be
}

对于属性,get和set/init访问器可以独立声明为 。unsafe/safe 如果访问器没有 unsafe/safe 修饰符,则从属性继承它。 与 readonly 修饰符类似,我们对属性和访问器引入了以下限制:

  • unsafe / safe 修饰符可以应用于属性或其访问器,而不是两者。
  • 将同 unsafe/safe 一修饰符应用于所有属性访问器是错误的(该修饰符应改为应用于该属性)。

目前无法将任何修饰符置于事件访问器上,并且此建议不会更改,即 addremove 事件访问器不能单独声明为 unsafe。 仅当整个事件标记为 unsafe“不安全”时,它表示访问器不安全;否则它们是安全的。

Fields

unsafe 在字段中还将其标记为 “要求不安全”,并且不会在其初始值设定项中引入 unsafe 上下文。 兼容性模式也适用于字段。

将属性或事件 unsafe 标记为不使其后盾字段 需要不安全。

必须标记所有实例字段, safe 或者 unsafe 其包含类型是否包含以下任一类型:

  • [StructLayout(LayoutKind.Explicit)],
  • [StructLayout(...)] 具有非零 Pack 或 Size 自变量,而不考虑布局类型,
  • [ExtendedLayout]。

如果字段在自动属性或类似字段的事件后面“隐藏”,则 safe/unsafe 要求将改为移动到自动属性或类似字段的事件。

Metadata

使用新的内存安全规则编译程序集时,会用 MemorySafetyRulesAttribute (下面详述)进行标记,并填充 15 为语言版本。 这是向任何下游使用者发出信号,如果需要上下文来调用这些成员,则程序集中定义的任何成员都将使用(详见下RequiresUnsafeAttribute)正确属性unsafe。 此类程序集中未标记 RequiresUnsafeAttribute 的任何成员都不需要调用上下文,而不考虑 unsafe 成员签名中的类型。

在源中显式应用 MemorySafetyRulesAttribute 或任何 RequiresUnsafeAttribute 符号是错误的。

编译器忽略 RequiresUnsafeAttribute使用旧内存安全规则的程序集中标记的成员(而是在此处使用 兼容性模式 )。

当非类型成员 需要不安全时,编译器将在元数据中的成员上合成 RequiresUnsafeAttribute 应用程序。

MemorySafetyRulesAttribute如果需要,编译器会根据标准已知成员规则进行合成和RequiresUnsafeAttribute定义。

namespace System.Runtime.CompilerServices
{
    /// <summary>Indicates the language version of the memory safety rules used when the module was compiled.</summary>
    [AttributeUsage(AttributeTargets.Module, Inherited = false)]
    public sealed class MemorySafetyRulesAttribute : Attribute
    {
        /// <summary>Initializes a new instance of the <see cref="MemorySafetyRulesAttribute"/> class.</summary>
        /// <param name="version">The language version of the memory safety rules used when the module was compiled.</param>
        public MemorySafetyRulesAttribute(int version) => Version = version;
 
        /// <summary>Gets the language version of the memory safety rules used when the module was compiled.</summary>
        public int Version { get; }
    }

    [AttributeUsage(AttributeTargets.Event | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Constructor, AllowMultiple = false, Inherited = false)]
    public sealed class RequiresUnsafeAttribute : Attribute
    {
    }
}

Compat 模式

出于兼容性目的,为了减少启用新规则时发生的误报数,对于尚未更新到新规则的模块,我们有回退规则。 对于此类模块,如果成员包含其参数类型或返回类型的某个位置的指针或函数指针类型(可以嵌套在非指针类型(例如,)中,则将其视为int*[]。 请注意,这不适用于约束类型(例如)中的指针, where T : I<int*[]>因为这些指针以前也不需要调用站点上的不安全上下文。

这不包括替换的泛型参数(例如I<T>.M(T)替换方法)Tint*[],因为目标成员无论如何都无法使用该指针类型。

此类兼容性模式 要求不安全 的成员需要 unsafe 上下文,即使没有选择加入更新的内存安全规则的调用方也是如此。 这应该避免出现“dip”,其中仅更新 LangVersion(但不更新内存安全规则版本)会使大多数指针操作安全(包括调用签名中包含指针的函数,这些函数在选择更新的规则时可能会标记为 “需要不安全 ”),因此,在此迁移窗口中使代码受到保护更少。

VB

我们不需要为需要不安全的成员添加对Visual Basic的支持,因为目前 VB 中没有unsafe上下文,也没有办法处理其中任何一个指针。

unsafe 表达式

表达式 unsafe 引入了用于计算单个表达式的最小 unsafe 上下文。 在块不必要地扩大unsafe范围或unsafe无法语法使用块(例如筛选器、字段初始值设定项和构造函数初始值设定项)unsafe的情况下catch,这非常有用。

Syntax

添加为unsafe_expressionprimary_no_array_creation_expression:

unsafe_expression
    : 'unsafe' '(' expression ')'
    ;

它受到与其他地方关键字相同的 AllowUnsafeBlocks 要求 unsafe 。

语义学

unsafe_expression建立unsafe用于计算表达式的上下文。 这意味着,允许在封闭的表达式中使用指针取消引用、函数指针调用和对 requires-unsafe 成员的调用。 类型和值 unsafe_expression 是封闭 表达式的类型和值。

由 an unsafe 建立的unsafe_expression上下文不会超出其右括号。

动机和迁移示例

多个语法位置根本不承认 unsafe 块,但可能包含调用 需要不安全成员的 子表达式。 如果没有 unsafe 表达式,迁移此类代码需要将不安全的子表达式提取到帮助程序本地函数或临时变量中,或者使用比必要的更广泛的 unsafe 块,从而掩盖意向并增加详细程度。

await on a requires-unsafe 方法。 当等待的方法变得 需要不安全时, unsafe 可以在整个 await周围使用块,但 unsafe 表达式可以将不安全的上下文限制为需要它的操作:

// Without unsafe expressions: the unsafe context includes the await expression
unsafe
{
    // SAFETY: Discharges obligations because reasons
    await DoWork();
}

// With unsafe expressions: the unsafe context wraps only the call
// SAFETY: Discharges obligations because reasons
await unsafe(DoWork());

捕获筛选器。 子 when 句的 catch 筛选器是表达式,而不是语句正文。 块 unsafe 只能环绕语句,因此不能只将一个语句放在筛选器表达式周围。 当筛选器中使用的方法 变得需要不安全时,唯一没有 unsafe 表达式的替代方法是帮助程序:

// Without unsafe expressions: must spill to a local function
static bool FilterHelper(Exception e)
{
    // SAFETY: Discharges obligations because reasons
    unsafe { return NowUnsafeCall(e); }
}

try
{
    await DoWork(); // 'await' here prevents wrapping the whole try/catch in 'unsafe'
}
catch (Exception e) when (NowUnsafeCall(e))
{
}

// With unsafe expressions: inline and minimal scope
try
{
    await DoWork();
}
// SAFETY: Discharges obligations because reasons
catch (Exception e) when (unsafe(NowUnsafeCall(e)))
{
}

字段初始值设定项。 根据更新的规则, unsafe 字段上不会在其初始值设定项中引入 unsafe 上下文。 当字段的初始值设定项调用 需要不安全 的成员时, unsafe 表达式提供上下文而无需帮助程序方法:

// Without unsafe expressions: must spill to a helper method
static int InitialValue()
{
    // SAFETY: Discharges obligations because reasons
    unsafe { return ReadFromPointer(); }
}
static int _value = InitialValue();

// With unsafe expressions: inline
// SAFETY: Discharges obligations because reasons
static int _value = unsafe(ReadFromPointer());

构造函数初始值设定项。 this(...) 和 base(...) 初始值设定项参数列表是表达式,而不是语句主体。 如果其中一个参数调用 需要不安全 的成员,则再次没有插入块的位置 unsafe ,因此该调用必须移动到帮助程序:

class C(int x)
{
    // SAFETY: Discharges obligations because reasons
    C() : this(unsafe(GetUnsafeValue()))
    {
    }
}

class Derived : Base
{
    // SAFETY: Discharges obligations because reasons
    Derived() : base(unsafe(GetUnsafeValue()))
    {
    }
}

内联呼叫。 更通常,仅包装 requires-unsafe 子表达式会保持审核范围紧密,并避免将周围的安全代码(如参数计算或包含方法调用)拉取到上下文中 unsafe :

extern int Add(int i1, int i2); // Some fancy extern addition function

// Code I want to write:

// SAFETY: Discharges obligations because reasons
Console.WriteLine(unsafe(Add(1, 2)));

// Code I have to write without unsafe expressions, option 1
// (unsafe context unnecessarily includes the WriteLine call):

// SAFETY: Discharges obligations because reasons
unsafe
{
    Console.WriteLine(Add(1, 2));
}

// Code I have to write without unsafe expressions, option 2
// (very verbose and harder to read):
int result;
// SAFETY: Discharges obligations because reasons
unsafe
{
    result = Add(1, 2);
}
Console.WriteLine(result);

提问

(答)用于 RequiresUnsafeAttribute 表示 需要不安全 的成员

我们可以使用应用于成员的属性(unsafe而不是使用成员上的关键字来表示RequiresUnsafeAttribute的成员)(而不是更改对成员的unsafe修饰符的含义)。

unsafe优点:

  • 类似于其他语言,因此更易于理解,
  • 比属性更可发现。

属性的优点(或其他关键字):

  • 避免中断标记为
  • 增量采用可能(成员逐成员),
  • 不强制标记整个身体 unsafe (即使有 unsafe 关键字,我们可以 改变unsafe 对身体没有影响),
  • 允许取消所有 需要不安全 的错误,而无需将成员本身标记为 requires-unsafe (示例)。

讨论:

答:使用关键字 unsafe 表示 需要不安全 的成员。

本地函数/lambda 安全上下文

现在 unsafe ,方法正文的范围是词法范围的。 任何嵌套的本地函数或 lambda 都继承了这一点,其主体位于内存不安全的上下文中。 我们要保持语言的这种行为吗? 请注意,如果我们保留 unsafe 为用于公开调用方必须不安全的修饰符,则这可能会对方法的签名产生影响。 如目前建议的那样,嵌套的匿名函数和本地函数不会保留其包含成员的不安全上下文。

委托类型 unsafety

我们可以允许将委托类型和 lambda(和函数类型)标记为 需要不安全。 这需要其他几个规则(外部 unsafe 上下文):

  • 禁止将 requires-unsafe 委托用作类型参数,
  • 禁止将这些委托转换为不需要不安全的任何内容(Delegate以及Expressionobject),如果没有,则有可能在错误的位置强制unsafe批注,并具有一个没有正确标注实际unsafe区域的区域。

Lambda/方法组转换为安全委托类型

如果允许 unsafe lambda 和委托,则应将 requires-unsafe lambda 或方法组转换为允许的不要求不安全 的委托类型,而不会在 unsafe 上下文中出现警告或错误? 如果我们不这样做,那么生态系统的各个部分(尤其是通过 LINQ 查询传递的任何可枚举项)可能相当痛苦。

Lambda/方法组自然类型

目前,在签名中时,对语义和 codegen(除了其他元数据)的唯一实际影响是更改 lambda 或方法组unsafe。 如果我们要避免这样做,那么也不会对这一点产生真正的影响,这可能会给采用者更多的信心,即行为并没有在幕后巧妙地改变。

注释

如果我们决定保留 lambda 的能力 unsafe ,我们需要更新此建议,以包含语法更改,以允许首先声明 unsafe lambda。

指向托管类型的指针

C# 11 允许指向具有警告的托管类型的指针。 我们应该放宽针对操作地址的警告吗? 我们认为问题只是当用户取消引用此类指针时,该指针属于正常的不安全演变规则。 但是呢 sizeof?

stackalloc as initialized

目前, 该规范 始终将内存视为 stackalloc 未初始化的,并表示内容是未定义的,除非手动清除或分配。 我们是否将此视为规范 bug,或者是否需要更改我们 unsafe 出于目的考虑 stackalloc 的内容?

stackalloc 规则

LDM 应确认 stackalloc 上面定义的规则 ,以及它是否应该应用,而不考虑与其他指针相关的更改一样选择加入。

AllowUnsafeBlocks

此建议使 safe 关键字需要 AllowUnsafeBlocks 标志, unsafe 就像关键字一样。 此决定应由 LDM 审查。

(答) unsafe 表达 式

具有更全面的 unsafe 功能的其他语言已添加 unsafe 为一种表达式,可改进用户人体工学,并允许作者更准确地限制使用的位置 unsafe 。 这是我们想要在 C# 中具备的吗? 请考虑直接处理安全性的成员的内联调用 unsafe :现在,作者需要将整个语句包装在块中 unsafe ,扩展上下文的范围 unsafe ,或者需要将内部函数调用分解为中间变量。

extern int Add(int i1, int i2); // Some fancy extern addition function

// Code I want to write:
Console.WriteLine(unsafe(Add(1, 2)));

// Code I have to write option 1, unsafe context unnecessary includes the WriteLine call
unsafe
{
    Console.WriteLine(Add(1, 2));
}

// Code I have to write option 2, very verbose and harder to read:
int result;
unsafe
{
    result = Add(1, 2);
}
Console.WriteLine(result);

答: 是的。 请参阅 详细的设计部分。

(答) await在上下文中unsafe

我们应该放宽上下文 await 中 unsafe 有关表达式的限制吗? 特别允许await UnsafeMethod()非常有用,因为否则用户必须重写它。Task t; unsafe { t = UnsafeMethod(); } await t; 有关更多详细信息 ,请参阅迭代器/异步中的 ref/unsafe 。

答:是的。 在上下文中await查看unsafe。

更多 unsafe 上下文和放松

在上下文中await跟进unsafe,我们是否还应在上下文中yield允许unsafe异步和迭代器方法的指针参数? 有关更多详细信息 ,请参阅迭代器/异步中的 ref/unsafe 。

我们应该在安全上下文中允许 &UnsafeMethod 吗? 今天,正如提案所代表的那样,如果方法被标记为unsafe上下文,则需要unsafe上下文。 但是,由于我们只是获取其地址,因此在取消引用/调用时需要 unsafe 上下文,因此我们可以在安全上下文中允许地址本身。

LangVersion 上封闭的不安全放松

我们应该在 LangVersion 上无条件 地放宽不安全上下文 吗?

  • LDM 2026-04-06:它们不具有内存安全规则版本的条件
  • TODO:LangVersion 怎么样?

(答) unsafe on types

我们可以考虑不自动使类型的整个词法范围 unsafe 成为 unsafe 上下文,并在类型上发出警告 unsafe ,因为它没有任何意义。

答: unsafe 在类型上是错误(可以根据反馈重新访问)根据更新的规则。

(答)允许取消在边缘案例方案中 出现不安全 错误

在以下方案中,应如何允许禁止 显示需要不安全 的错误?

class A : System.Attribute
{
    unsafe public A() { } // declaring requires-unsafe constructor
}

class C
{
    [A] public void M() { } // error: applying requires-unsafe `A..ctor`
}

class B : A
{
    public B() { } // error: calling requires-unsafe `A..ctor` (implicit `: base()`)
}

class X<T> where T : new();
class D
{
    public void M(X<A> x) { } // error: using `X` which uses requires-unsafe `A..ctor`
}

为了禁止 出现需要不安全 的错误,我们需要以某种方式在这些成员的签名中引入 unsafe 上下文。 但是关键字unsafe不再引入unsafe上下文。 我们可以unsafe在签名中引入unsafe上下文,但当想要调用基要求不安全的构造函数时,强制使构造函数需要不安全,这似乎很不幸。 我们可以在签名警告中 要求不安全 的用法,如果用户遇到这些罕见的边缘情况,用户可能会取消就地使用。

类型存在类似的问题,因为它们unsafe不会引入unsafe上下文:

class A : Attribute
{
    unsafe public A() { } // declaring requires-unsafe constructor
}

[A] class C; // error: applying requires-unsafe `A..ctor`

class B() : A(); // error: calling requires-unsafe `A..ctor`

class X<T> where T : new();
class D : X<A>; // error: inheriting from `X` which uses requires-unsafe `A..ctor`
  • LDM 2026-05-13:
    • 非可执行代码(如属性应用程序)应保持不可抑制的错误(直到听到一些反馈)
    • 其他边缘情况(例如 new() )也可能保持错误,直到听到反馈
    • 不要禁止声明无参数 要求不安全 的构造函数
    • unsafe只有构造函数的初始值设定项可以调用 requires-unsafebase 或this构造函数

params 集合

class C
{
    unsafe public C() { } // declaring requires-unsafe constructor
}

class B
{
    public void M(params C c) { }
}

只需允许此声明,因为调用此类方法需要 unsafe 上下文,因为不安全 params 的集合是在调用站点构造的。 尽管这使得声明实际上 需要不安全,因此我们可以只需要 unsafe 批注或至少警告有关这一事实。 请注意, params 收集案例是今天的错误,符合其他类似功能在这里的行为方式(Obsolete, UnmanagedCallersOnly但这可能是一个实现 bug)。

已知成员

为了简单和理智实现,我们建议编译器可以自由假设所有已知成员(如 Array.Length)可以假定为安全(即,不需要 不安全)。

Xml 文档

通过对呼叫者的义务,有责任明确这一义务。 除了 xml 文档中已经可以表示的内容之外,我们是否应该正式化这一点?

标记 unsafe 的成员应具有注释,指示调用方必须执行的操作,以确保代码正确。 为了便于在文档中查看和更方便地区分,新的 XML 文档标记将很有帮助: <safety /> 预计所有前/后条件都将置于块中 <safety> 。

若要记录每个unsafe块的推理,我们建议使用类似于 // SAFETY 的注释。 我们还应该检查编译器的这些警告(例如,有一些默认警告),还是将其留给分析器?

更毫无 unsafe 意义的警告

更多的声明是否会产生毫无 unsafe 意义的警告或错误? 例如,具有空正文(或 extern)等的方法。不过,我们已经有了一个 IDE 分析器,用于不必要的 unsafe 。

[ModuleInitializer] unsafe void M() { }是否应是错误,类似于静态构造函数?

(答) unsafe 领域

今天,在一个领域没有提出 unsafe 任何建议。 不过,我们可能需要添加它,以便任何从或写入标记为 unsafe 必须位于上下文中的 unsafe 字段。 这样,我们就能更好地注释有关代码的担忧,例如:

class SafeWrapper
{
    internal byte* _p;

    public void DoStuff()
    {
        unsafe
        {
            // ... validate that the object state is good ...
            // ... perform operation with _p .... 
        }
    }
}

// Elsewhere in safe code:
void M(SafeWrapper w)
{
     w._p = stackalloc byte[10];
}

我们还应该将自动属性的后盾字段 unsafe标记为?

为了符合我们对成员 的决定 ,最好在一个领域做出 unsafe ,也不会引入 unsafe 上下文。 如果在字段的初始值设定项中使用 了需要不安全 的操作,则用户始终可以将这些操作封装到方法中,或者我们可以引入 unsafe 表达式。

  • LDM 2026-05-13:字段可以通过以下方法进行标记:unsafe初始值设定项不在上下文中unsafe。

(答)显式布局

是否应将结构中的字段标记为 [StructLayout(Explicit)] 或 [ExtendedLayout] 必须标记为 unsafe?

建议:是的。

包或大小

显式布局要求已扩展到非零 Pack 或 Size 参数, StructLayoutAttribute因为它们的字段也可以不对齐。 请参阅 https://github.com/dotnet/roslyn/issues/85173。 此决定应由 LDM 审查。

显式布局和后盾字段

如果编译器在字段 需要显式安全注释的类型中为自动属性或类似字段的事件合成后盾字段,我们是否应改为对属性/事件进行要求 safe/unsafe ? 否则,用户将被迫将这些自动声明扩展到手动字段和包装器成员声明。 获取支持字段的主构造函数参数怎么样? safe当前不允许对unsafe参数声明使用修饰符。

合成成员

当用户声明的成员被标记为 “需要不安全”时,是否应该通过反射(例如迭代器)调用关联的编译器合成成员(例如 MoveNext 迭代器)在元数据中用 [RequiresUnsafe] 属性进行调用?

请注意,这不适用于已明确 指定 何时 需要不安全 的属性和事件访问器,因此它们会相应地获取 [RequiresUnsafe] 元数据中的属性。

[Out] 和 [SkipLocalsInit]

例如,VB 不保证 [Out] 参数已初始化,因此可以结合 [SkipLocalsInit]C# 考虑 unsafe 调用此类参数。 另一方面,感觉就像被调用方的问题,即它不维护合同 [Out] (同样,在许多其他方面可能不安全)。

如果我们决定这些情况, unsafe我们可以排除我们知道 已选择加入 的方法(目前这些方法来自 C#,保证正确使用 [Out] ,但如果其他语言实现新规则,它们也应该保证这一点)。

获取未初始化变量的地址

今天,采用未明确分配的变量的地址可以考虑明确分配的变量,公开未初始化的成员。 我们有几个选项可以解决此问题:

  1. 要求在允许对变量使用地址运算符之前明确分配变量。
  2. 使获取未初始化变量的地址不安全。

示例:

static void SkipInit<T>(out T value)  
{
    // value is considered definitely assigned after the address-of
    fixed (void* ptr = &value);
}
int i;
// i is considered definitely assigned after the address-of
_ = &i;
// Incrementing whatever was on the stack
i++;

MemorySafetyRulesAttribute 的值

“已启用”/“更新”内存安全规则版本应是什么? 2? 15? 11? 另请参阅 SDK 不安全采用 和 更渐进的选择加入?。

(答)更渐进的选择加入?

今天,选择加入会同时为你提供两件事:在自己的代码中强制实施不安全的规则,以及发布到使用者(通过程序集级属性)的信号,说明你的批注是有意的。 可能有用户想要在批注时开始获取强制诊断,而无需准备好发布已完全批注其程序集。 我们是否应该有一个“中间”选择级别,这将将不安全的诊断显示为警告,并且完全选择加入会将它们提升为错误?

(答)细粒度选择加入

提供类似于我们为可为空引用类型而构建的基于区域的精细选择加入机制,用户可以使用指令为源代码的特定区域启用该功能? 另请参阅 选择加入/退出代码区域。

(答) extern 隐式不安全

这是目前唯一没有显式RequiresUnsafeAttribute关键字进行合成的位置unsafe。 我们是否对这个离群值没关系?

此外,CoreLib 今天将许多外部方法 (FCalls) 公开为安全方法。 将 extern 方法视为隐式不安全需要用安全包装包装隐式不安全的 extern 方法。 我们可能会遇到以下情况:由于运行时实现详细信息,添加额外的包装器很困难。

答: extern 成员必须标记为或 unsafesafe (我们为其添加新关键字,但可以根据反馈重新访问该功能之前)。

(答)允许 safe 非extern 成员 (LibraryImport)

这不是我们第一次考虑这个问题。 工作组最初讨论过extern,成员LibraryImport一起讨论。 LDM 随后考虑是否应要求具有 unsafe 块或指针的成员携带显式 safe 标记,并 决定 可分析的方法机构不需要此仪式:与边界不同 extern ,可以检查其实现以确定它们是否履行安全义务。 因此,safe目前只允许在需要显式安全选择的情况下,例如需要extern显式安全注释的成员和字段。

此后,库团队提供了源LibraryImport生成的新数据,该数据值得重新访问问题。 LibraryImport 是 P/Invoke 的首选新式形式,但其生成的部分实现是否是 extern 实现详细信息。 对于 blittable 签名,生成器可以发出直接 extern 实现(简化的示例):

// User code
[LibraryImport("kernel32.dll")]
static partial int Blit(int x);

// Generated code
[DllImport("kernel32.dll", EntryPoint = "Blit", ExactSpelling = true)]
static extern partial int Blit(int x);

生成的 extern 声明必须标记 safe ,或者 unsafe,部分声明必须就其安全修饰符达成一致。 因此,用户创作的声明需要相同的修饰符。

对于需要封送的签名,生成器会围绕专用 extern发出托管包装器:

// User code
[LibraryImport("kernel32.dll")]
static partial int NotBlit(ref int x);

// Generated code
static partial int NotBlit(ref int x)
{
    fixed (int* xNative = &x)
    {
        return __PInvoke(xNative);
    }

    [DllImport("kernel32.dll", EntryPoint = "NotBlit", ExactSpelling = true)]
    static extern unsafe int __PInvoke(int* xNative);
}

此处不允许externsafe使用面向用户的分部方法。 因此,用户的可用语法取决于生成器选择的实现,即使该选择不是协定的 LibraryImport 一部分,并且随着生成器的发展而可能发生变化。 始终生成包装器会使语法保持一致,但会悲观可以直接发出的签名,并会在两LibraryImport者之间DllImport创建用户体验差异。

应该 safe 允许在每个声明 unsafe 上允许标记成员 需要不安全,即使不需要该声明? 或者,语言是否应该为源生成器实现的部分成员提供更窄的规则?

工作组建议:允许safe在标记声明unsafe的任何位置作为声明修饰符。 如果不需要, safe 则为 no-op。 当语言不(如LibraryImport生成非extern包装器)需要显式修饰符时需要显式修饰符的方案将需要分析器来强制实施或safe存在unsafe。

  • LDM 2026-07-22:允许 safe 作为声明修饰符,可在任何位置 unsafe 将声明标记为 requires-unsafe

(答) unsafe 成员中的上下文默认值

我们可以考虑不自动使方法的 unsafe 整个正文成为 unsafe 上下文。 Rust 在 RFC 2585 中做到了这一点,其动机有助于将块的范围 unsafe 减少到实际使用的位置 unsafe 。 我们可以在 C# 中执行相同的操作,例如警告或错误,其动机类似。

  • LDM 2026-04-22:是的, unsafe 在签名中不会使正文 unsafe

(答) new() 约束

我们希望支持new()需要不安全的功能(我们当前似乎不支持编译器中的其他功能,例如Obsolete)?

M<C>(); // should be an error outside `unsafe` context since `M` calls the requires-unsafe `C..ctor`?

void M<T>() where T : new()
{
    _ = new T();
}

class C
{
    unsafe public C() { }
}
  • LDM 2026-05-13:具有 需要不安全 的无参数构造函数的类型不应满足 new() 声明位置中的约束(其中无法引入 unsafe 上下文)

new() 约束和 usings

它在别名和静态用法中的行为应如何?

  • 如果是声明中的错误 using ,请通过 unsafe 我们已经支持的关键字禁止显示,或者
  • 它通常是使用站点上的错误,就像直接使用时没有别名或静态使用一样?

注释

第二种情况下,我们需要添加“无 unsafe意义”警告,以使用别名和静态 usings。

class C
{
    unsafe public C() { }
}

class D<T> where T : new()
{
    public static void M() { _ = new T(); }
}
using X = D<C>;
using unsafe X = D<C>;

X.M();
using static D<C>;
using static unsafe D<C>;

M();

请注意,其他约束的行为类似于以前的选项:

using X = D<C>; // error here

_ = new X(); // ok
_ = new D<C>(); // error here

class C
{
    public C(int x) { }
}

class D<T> where T : new();

是否应使用 unsafe更多构造?

  • dynamic (可能应与 BCL 为反射 API 决定的内容匹配)

(答)我们要如何扭曲

问题文本

最初的提议是一种最大突破的方法,主要是对我们想要多么咄咄逼人的试金石测试。 它建议在升级编译器时一次性强制迁移(然后可能重复作为依赖项更新并添加到unsafe已使用的成员)时,它不能够选择加入/退出代码部分、更改方法的含义unsafe、禁止使用unsafe类型、使用错误而不是警告,并且通常会强制迁移一次性发生。 但是,在进行这样的更改方面,我们有着丰富的经验,我们可以利用这些更改来限定细分的规模,并允许增量采用。 下面介绍了这些选项。

选择加入/退出代码区域

这不是 C# 首次重新定义未批注代码的“base”事例。 C# 8.0 引入了可为 null 的引用类型功能,在很多方面都可以将其视为功能如何调整的 unsafe 蓝图。 它具有类似的目标(通过重新定义默认 C# 解释方式来防止花费数十亿美元的 bug)和类似的常规特征集(向类型添加新信息以传播状态并避免 bug)。 它还严重中断,需要一组强大的选择加入和选择退出功能,以允许代码库随时间推移采用该功能。 此功能是“可为 null 的引用类型上下文”。 这是一个词法范围,它告知编译器代码中的给定区域,如何解释未批注的类型引用以及向用户提供哪些类型的警告。 我们也可以使用它作为模型 unsafe ,添加一个“安全规则上下文”或类似的模型,以允许控制这些新规则是否正在应用。

我们拥有新功能 unsafe 的一个优点是它们不太普遍。 虽然顶级库中有相当数量的 unsafe 调用,但我们对使用 unsafe 排名靠前的库的百分比的猜测远远低于“曾经编写的每一行 C# 代码”。 希望这意味着,虽然可能需要一些选择加入/退出的功能,但我们不需要像可为空机制那样复杂,使用专用的预处理器开关等。

警告与错误

该建议目前指出,内存安全要求当前通过警告而不是错误强制执行。 这源于我们处理可为 null 的功能的经验,其中警告允许代码库以增量方式采用新功能,无需一次性转换大量代码。 我们预计不安全的警告需要类似的过程:许多基本代码只需在全球启用新规则并继续其生活。 但是,我们期望我们最关心的代码库将有大量的代码进行批注,我们希望它们能够继续处理该功能,而不是看到一堵错误墙并立即放弃。 通过发出要求警告,我们允许这些代码库根据需要修复警告文件或方法,并禁用其他位置的警告。

方法签名中断

现在,我们建议 unsafe 作为方法上的关键字从词法范围中移出一些词法范围,而不会对具有语义影响的内容产生语义影响,并且没有词法范围。 我们可以通过在方法或成员的调用方必须位于上下文中 unsafe 时引入新关键字来限制此中断;例如, callerunsafe 作为修饰符。

源生成器的默认值

对于可为 null,我们强制生成器作者显式选择加入为 null,而不考虑整个项目默认是否已选择加入该功能,以便生成器输出不会被打开可为 null 的用户破坏,并发出警告作为错误。 对于源生成器,我们是否应该执行相同的操作?

结束语

在 LDM 2025-11-05 中回答。 当打开新规则时,我们将报告内存安全问题的错误,并且不会对源生成器执行任何异常。

(答)错误或警告?

(答)源生成器提供

(答)需要 safe 具有 unsafe 块或指针的成员的创建者?

(答)非选择加入呼叫者的模式也?

答:签名中带有指针的非选择加入成员被视为 需要不安全 选择加入调用方。

(答)扩展兼容性模式?

我们也应该考虑 nint 并 System.IntPtr 作为指针? 我们是否应该考虑 extern/DllImport 来自非选择加入调用方,因为 也需要不安全 ? 当选择加入程序集引用非选择加入程序集时,我们是否应该有一个全面警告?

  • LDM 2026-04-29:无扩展(分析器可能涵盖一些不太确定的不安全信号)