Skip to content

Bug: 把 ref struct 写入 heap buffer 违反 JIT 契约,.NET 10 暴露出偶发写丢失 #114

@HMBSbige

Description

@HMBSbige

Upstream: dotnet/runtime#127373

根因

VectorBuffer* 当前被建模为 ref struct。把这类 byref-like 值直接写入 heap-backed buffer,不属于受支持的用法;JIT 会按 ref struct 不会出现在堆上的前提做优化。

.NET 10 JIT 优化更激进,会把写入目标直接复用为 return buffer;如果 callee 内发生压缩 GC,写入就可能落到陈旧地址,最终表现为偶发写丢失。

错误写法

ref VectorBuffer16 ks = ref stream.AsVectorBuffer16();
ks = _blockCipher.Encrypt(c);

Unsafe.WriteUnaligned(ref MemoryMarshal.GetReference(buffer), crypto.Encrypt(...));

彻底的修复方向

这次写丢失不是单个调用点的问题,而是 src/CryptoBase.Abstractions/Vectors 把“会别名到 heap-backed buffer 的数据块”建模成了 ref structref struct 只适合栈上临时值或 byref-like 视图,不适合作为数组、字段、池化缓冲区、Span<byte> 背后的持久存储表示。只要 VectorBuffer* 继续是 ref struct,类似“把返回值直接写回 heap-backed buffer”这类路径就仍然存在未定义行为风险。

彻底修复应当把 VectorBuffer16/32/64/128/256/512/1024ref struct 重构为可安全存在于堆上的普通值类型,把“存储”和“栈上视图”两个概念拆开:

  • 存储层使用普通 struct。连续大块缓冲区优先用 [InlineArray] / InlineArrayN<T> 表达;需要多种宽度重解释的 16-byte / 32-byte 基元可以继续使用显式布局,但必须去掉 ref struct
  • 访问层通过 MemoryMarshal.CastUnsafe.As、辅助 ref/Span 访问器提供 Vector128/256/512<byte>UInt128 等视图,而不是把 byref-like 类型本身当成存储对象。
  • 调用层统一只读写普通 structSpan<byte>,不再把 ref struct 返回值直接写入 heap-backed buffer,也不再让 heap-backed Span<byte> 通过 ref struct 身份长期别名。

例如,1024-byte 缓冲区可以建模为 InlineArray16<Vector512<byte>>512-byte 缓冲区更接近 InlineArray8<Vector512<byte>>;小尺寸基础块则更适合普通显式布局 struct。这里的重点不是某一个具体模板,而是彻底移除 ref struct 作为 heap-backed 存储类型的设计。

这样修完后,AsVectorBuffer* 对 heap-backed Span<byte> 的别名、Unsafe.WriteUnaligned(..., value)、数组/字段/池化缓冲区中的暂存都会回到受支持语义,问题才算从设计上消失,而不是只在个别热点调用点上打补丁。

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions