Skip to content

Commit dafc8af

Browse files
committed
docs: explain ZeroMemory tradeoffs for managed buffers
1 parent 46eb469 commit dafc8af

4 files changed

Lines changed: 50 additions & 0 deletions

File tree

‎docs/README.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
- [组件复制与包消费](components/consumption.md):源码导出、本地打包和仓库外验证。
1212
- [`src` 基础设施性能检查](infrastructure-performance-review-2026-09-23.md):逐项目的测量、改动与未改动原因。
1313
- [AI、Logging、Messaging、Persistence 性能检查](extension-modules-performance-review-2026-09-23.md):四个模块的测量、提交及 AOT 边界。
14+
- [托管缓冲区清零与 `ZeroMemory`](ZeroMemory-In-Managed-Services.md):结合 HTTP 幂等正文和 AES 密钥说明收益、成本与边界。
1415

1516
下方理论资料继续保留。执行当前仓库时优先使用以上入口;历史示例的路径和接口应先与当前代码核对。
1617
## 如何使用这个文档中心
@@ -334,6 +335,7 @@ flowchart TD
334335
| 文档 | 说明 |
335336
| --- | --- |
336337
| [Password-Hashing-With-IPasswordHasher.md](./Password-Hashing-With-IPasswordHasher.md) | 密码安全:`IPasswordHasher<User>`、旧 HMAC 迁移、自动重新哈希与项目实战 |
338+
| [ZeroMemory-In-Managed-Services.md](./ZeroMemory-In-Managed-Services.md) | 托管缓冲区清零:HTTP 正文与 AES 密钥的收益、成本和清理边界 |
337339
| [Lab-Only-Controller-Discovery.md](./Lab-Only-Controller-Discovery.md) | MVC 工程实践:ApplicationPart、FeatureProvider、Controller discovery 与实验端点隔离 |
338340
| [Separated-Interface.md](./Separated-Interface.md) | 接口分离:消费方定义接口降低耦合 |
339341
| [Enumeration.md](./Enumeration.md) | 枚举类:替代原生 enum 以支持多态 |
@@ -385,6 +387,7 @@ flowchart TD
385387
| [Serverless Architectures.md](./Serverless%20Architectures.md) | 无服务架构(Serverless Architectures) |
386388
| [Snapshot.md](./Snapshot.md) | 快照模式 |
387389
| [ValueObject.md](./ValueObject.md) | 值对象 |
390+
| [ZeroMemory-In-Managed-Services.md](./ZeroMemory-In-Managed-Services.md) | 托管缓冲区清零:`CryptographicOperations.ZeroMemory` 的收益与边界 |
388391

389392
### 子目录文档
390393

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# 托管缓冲区清零:`CryptographicOperations.ZeroMemory` 的收益与边界
2+
3+
带 `Idempotency-Key` 的请求进入 Reference 后,[HTTP 幂等执行器](../samples/Reference/MS.Microservice.Reference.Web/HttpIdempotency/ReferenceHttpIdempotencyExecutor.cs)会把 Body 读入 `MemoryStream`:先计算请求指纹,再把同一份正文交给端点。请求结束时释放流,并不等于擦除它的字节数组。.NET 10 的 `MemoryStream.Dispose` 会关闭流,但仍保留底层缓冲区,允许调用者继续用 `GetBuffer` 取得它。[.NET 10 源码](https://github.com/dotnet/runtime/blob/v10.0.0/src/libraries/System.Private.CoreLib/src/System/IO/MemoryStream.cs)
4+
5+
## 它清除的是哪块内存
6+
7+
`CryptographicOperations.ZeroMemory(Span<byte>)` 把传入的字节范围写成零。`Span` 是现有内存的视图;把数组的一段作为 `Span` 传入,不会因此复制数组。例如:
8+
9+
```csharp
10+
byte[] bytes = [1, 2, 3];
11+
CryptographicOperations.ZeroMemory(bytes.AsSpan(0, 2));
12+
// bytes 现在是 [0, 0, 3]
13+
```
14+
15+
它比普通清零调用多了一项明确的运行时保证:清零写入不会因后续没有读取而被优化掉。.NET 10 的实现为此禁止该方法被内联和优化,然后调用 `buffer.Clear()`。这不表示当前 .NET 会跳过普通的 `Array.Clear`;官方文档也说目前没有计划引入那种优化。[API 文档](https://learn.microsoft.com/dotnet/api/system.security.cryptography.cryptographicoperations.zeromemory)、[.NET 10 源码](https://github.com/dotnet/runtime/blob/v10.0.0/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/CryptographicOperations.cs)
16+
17+
在执行器里,`bodyBytes.Span` 和 `body.GetBuffer().AsSpan(0, (int)body.Length)` 指向同一个 `MemoryStream` 底层数组的已使用部分。代码多次出现清零调用,是因为退出位置不同:
18+
19+
| 请求走到哪里 | 清零位置 | 不清零时的功能结果 |
20+
| --- | --- | --- |
21+
| 读取时超过 1 MiB,提前返回 `413` | 返回前清理已读入的部分 | 仍返回 `413` |
22+
| JSON 或幂等键无效,提前返回 `400` | 对应异常分支清理正文副本 | 仍返回 `400` |
23+
| 查到记录并重放,或运行端点后结束 | `finally` 恢复原请求流,再清理正文副本 | 重放和业务结果不变 |
24+
25+
例如,请求发送 `{"sku":"SKU-42","quantity":2}`。清零后,执行器当前持有的正文数组中这段内容变成零;若不清零,已经持有该数组引用的代码在流释放后仍可通过 `GetBuffer()` 看到正文。两种情况下客户端得到的 `201` 或重放响应相同。这里的 `Idempotency-Key` 是 HTTP 请求头中的字符串,并不会被这些正文数组清零调用擦除。
26+
27+
[身份作用域代码](../samples/Reference/MS.Microservice.Reference.Web/HttpIdempotency/ReferenceIdempotencyActorScope.cs)清理的是另一块数组:把 issuer 和 subject 序列化后用于哈希的临时字节。它不清理认证中间件持有的 claims。[解密模型绑定器](../src/MS.Microservice.AspNetCore.Encryption/ApiDecryptModelBinder.cs)中的用法更直接:RSA 解出的 AES 密钥转成 `byte[]`,用完后在 `finally` 中清除该密钥数组。
28+
29+
## 不写会有什么影响
30+
31+
对当前 HTTP 幂等执行器,删掉这些正文清零调用不会改变请求指纹、事务、响应、重放或 AOT 行为。区别在于:这份正文副本在流释放后仍可能留在托管堆中,直到相关内存被回收或覆盖,因此也可能出现在进程内存转储中。清零缩短的是**当前数组中这份正文**的留存时间。
32+
33+
对 AES 密钥这样的密钥材料,清理更有明确价值:不清理时,密钥在该数组中的留存时间也交给内存回收与覆盖过程决定。清零至少能在使用结束时处理**当前持有的这一份数组**。
34+
35+
这项操作不能证明敏感数据已从进程内存消失。HTTP 服务器可能持有原始 Body;JSON 解析、编码转换或 `MemoryStream` 扩容可能产生其他数组。扩容后 `GetBuffer()` 指向新数组,清理当前数组不会回头清理旧数组。[`GetBuffer` 文档](https://learn.microsoft.com/dotnet/api/system.io.memorystream.getbuffer)也说明扩容可能更换底层数组。claims 和其他 `string` 不受这次字节清零影响;日志、数据库和已经发送的响应更不会被它改变。
36+
37+
还有一个当前代码没有覆盖的退出路径:若读取 Body 的 `ReadBodyAsync` 自身抛出异常,执行器尚未进入后面的 `try/finally`,不会运行该处的正文清零。`ArrayPool<byte>.Return(buffer, clearArray: true)` 处理的是另一个租用缓冲区;[官方文档](https://learn.microsoft.com/dotnet/api/system.buffers.arraypool-1.return)说明,清理行为取决于池是否保留该数组供后续复用。因此,不能把当前实现描述成“请求退出时所有正文副本都会清零”。
38+
39+
## 本项目如何取舍
40+
41+
密钥、解密后的明文等明确的敏感临时字节适合在最后一次使用后清理,并用 `finally` 覆盖异常路径。对普通 HTTP JSON 正文,清零是一项局部的防御措施,不能代替减少副本、限制日志内容和管理内存转储访问。是否保留,应依据这些正文的敏感程度及进程内存转储的威胁模型判断。
42+
43+
清零需要再写一遍目标缓冲区;`Span` 本身不产生这份数据的副本,也没有引入反射。当前项目没有针对这些调用的耗时、分配或 GC 前后数据,因此本文不声称清零的成本可忽略,也不以性能为由建议删除。若以后要调整请求正文的清理策略,先测量带键请求的成本,再核对实际还能存在哪些正文副本。

‎samples/Reference/MS.Microservice.Reference.Web/http-idempotency.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,6 @@ Profile 与 Order 都调用同一个 `ReferenceHttpIdempotencyExecutor`。执行
3333

3434
Reference 只保存 `2xx` 响应,带键请求 Body 上限为 1 MiB,响应正文上限为 64 KiB;快照只包含状态码、可为空的 `Content-Type`、`Location` 和正文。流式输出、文件下载或依赖其他响应头的接口不能直接套用。宿主不自动建表;部署前要应用所选提供者的迁移。两种迁移流都先有 `AddHttpIdempotency`,后有新增的 `AddOrders`,所以标准完整迁移即使在功能关闭时也会建幂等表;关闭仅表示请求运行时不访问该表。记录保留 24 小时,清理任务每小时运行一次。
3535

36+
执行器在已处理的返回路径清理当前请求正文缓冲区。读取时抛异常以及其他副本的边界,见[托管缓冲区清零](../../../docs/ZeroMemory-In-Managed-Services.md)。
37+
3638
测试代码覆盖两个创建路由的重放和冲突、JSON 字段重排、旧指纹同键冲突、禁用开关及事务回滚。本次没有延迟、分配或 GC 的前后基准数据,也没有完成整个 Reference 宿主的 Native AOT 发布验证。真实 PostgreSQL 并发、Wolverine 原生 Outbox 和进程崩溃恢复仍需对应环境验证。

‎src/MS.Microservice.AspNetCore.Encryption/README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@
33
This optional component binds versioned RSA-OAEP + AES-GCM request envelopes to explicitly registered MVC models. It depends on Core cryptology; the base AspNetCore host component does not depend on it.
44

55
See [加密请求怎样绑定为 MVC 模型](docs/encrypted-model-binding.md) for the problem, wire format, registration, examples and limits.
6+
7+
The binder clears its temporary AES key byte array after use. [托管缓冲区清零](../../docs/ZeroMemory-In-Managed-Services.md) explains what that protects and what remains in memory.

0 commit comments

Comments
 (0)