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 struct。ref struct 只适合栈上临时值或 byref-like 视图,不适合作为数组、字段、池化缓冲区、Span<byte> 背后的持久存储表示。只要 VectorBuffer* 继续是 ref struct,类似“把返回值直接写回 heap-backed buffer”这类路径就仍然存在未定义行为风险。
彻底修复应当把 VectorBuffer16/32/64/128/256/512/1024 从 ref struct 重构为可安全存在于堆上的普通值类型,把“存储”和“栈上视图”两个概念拆开:
- 存储层使用普通
struct。连续大块缓冲区优先用 [InlineArray] / InlineArrayN<T> 表达;需要多种宽度重解释的 16-byte / 32-byte 基元可以继续使用显式布局,但必须去掉 ref struct。
- 访问层通过
MemoryMarshal.Cast、Unsafe.As、辅助 ref/Span 访问器提供 Vector128/256/512<byte>、UInt128 等视图,而不是把 byref-like 类型本身当成存储对象。
- 调用层统一只读写普通
struct 或 Span<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)、数组/字段/池化缓冲区中的暂存都会回到受支持语义,问题才算从设计上消失,而不是只在个别热点调用点上打补丁。
Upstream: dotnet/runtime#127373
根因
VectorBuffer*当前被建模为ref struct。把这类 byref-like 值直接写入 heap-backed buffer,不属于受支持的用法;JIT 会按ref struct不会出现在堆上的前提做优化。.NET 10 JIT 优化更激进,会把写入目标直接复用为 return buffer;如果 callee 内发生压缩 GC,写入就可能落到陈旧地址,最终表现为偶发写丢失。
错误写法
彻底的修复方向
这次写丢失不是单个调用点的问题,而是
src/CryptoBase.Abstractions/Vectors把“会别名到 heap-backed buffer 的数据块”建模成了ref struct。ref struct只适合栈上临时值或 byref-like 视图,不适合作为数组、字段、池化缓冲区、Span<byte>背后的持久存储表示。只要VectorBuffer*继续是ref struct,类似“把返回值直接写回 heap-backed buffer”这类路径就仍然存在未定义行为风险。彻底修复应当把
VectorBuffer16/32/64/128/256/512/1024从ref struct重构为可安全存在于堆上的普通值类型,把“存储”和“栈上视图”两个概念拆开:struct。连续大块缓冲区优先用[InlineArray]/InlineArrayN<T>表达;需要多种宽度重解释的 16-byte / 32-byte 基元可以继续使用显式布局,但必须去掉ref struct。MemoryMarshal.Cast、Unsafe.As、辅助ref/Span访问器提供Vector128/256/512<byte>、UInt128等视图,而不是把 byref-like 类型本身当成存储对象。struct或Span<byte>,不再把ref struct返回值直接写入 heap-backed buffer,也不再让 heap-backedSpan<byte>通过ref struct身份长期别名。例如,
1024-byte缓冲区可以建模为InlineArray16<Vector512<byte>>,512-byte缓冲区更接近InlineArray8<Vector512<byte>>;小尺寸基础块则更适合普通显式布局struct。这里的重点不是某一个具体模板,而是彻底移除ref struct作为 heap-backed 存储类型的设计。这样修完后,
AsVectorBuffer*对 heap-backedSpan<byte>的别名、Unsafe.WriteUnaligned(..., value)、数组/字段/池化缓冲区中的暂存都会回到受支持语义,问题才算从设计上消失,而不是只在个别热点调用点上打补丁。