SILO Server 20260903 发布前复审
本文记录 SILO 20260903(RELEASE.2026-09-03T13-18-01Z,对应提交 9b11dc9469e650815b775cb47b039610644f5da4,候选复审提交 6e112d1856d4f3655f30fc81ee47e9f43d50d8f3,对比基线 RELEASE.2026-08-06T00-00-00Z 即 3be10fcc1a44f6620ded0bd303461f9d688cca23)的发布前工程复审:第二轮复审发现了哪些遗漏缺陷、修复边界如何收窄,以及发布门禁如何组织。后续九月与十月版本的修复见复制可靠性、Server 20260916 与 Server 20261018。
为什么需要第二轮审查
第一轮实现修复了大多数已知问题并通过了现有测试。第二轮复审进一步检查各安全不变量在所有合法协议形态与异常状态下是否严密:
- 同一条不变量能否被另一种合法的请求格式(wire representation)绕过?
- 鉴权前的快路径是否仍会触发磁盘 I/O 或修改内存状态?
- 元数据存在但加载失败时会发生什么?
- 两条各自正确的“读-改-写”路径是否共享同一个串行化边界?
- 请求头清洗是否保留了完整的 SigV4 流式签名状态?
- 验收记录对应的是最终代码树,还是此前的中间提交?
- 复杂的防御逻辑对应的是已复现的故障,还是仅停留在假想场景?
第二轮复审在首轮实现之上定位出多处真实缺陷,并将每项修复与验收结论严格收窄到具体的不变量与代码提交。
分领域复审结果
| 领域 | 第二轮复审发现 | 最终解决方案 | 状态 |
|---|---|---|---|
| 桶元数据 | 各自独立的配置锁会丢失共享 .metadata.bin 记录中的并发更新(#102) |
用一把有界的 metadata.lock 覆盖所有整记录写入、迁移、导入、纳管与自愈路径;站点复制仅合并变更字段,避免旧快照覆盖整条记录 |
候选已关闭 |
| 桶创建 | ForceCreate 与站点纳管可能用默认值覆盖已有桶配置 |
保留已有记录,仅更新创建与纳管状态;补充防覆盖回归测试 | 候选已关闭 |
| Object Lock | 直接将配置字节与单一标准 XML 比对,会漏判包含默认保留规则(Default Retention)的合法配置 | 先解析 Object Lock 配置,再从启用状态推导版本控制不变量;验证更新、读回与磁盘重载 | 候选已关闭 |
| 预鉴权 CORS | 任意 URL 路径段都会触发元数据读取与缓存膨胀 | CORS 查询仅读取常驻内存的元数据,不触发对象层 I/O | 候选已关闭 |
| CORS 启动期 | 元数据尚未初始化时,非常驻桶名可能回退到全局 CORS | 保持显式的启动期默认拒绝(fail-closed)状态 | 候选已关闭 |
| CORS 加载失败 | 真实桶一旦丢失加载失败记录,就会与不存在的桶混淆,导致预签名请求回退到全局 CORS 配置 | 维护有界的失败桶集合,在成功加载、删除与刷新路径上清除记录;集合内的桶保持默认拒绝 | 候选已关闭 |
| CORS 恢复 | 按需调用 GetConfig 成功重载后,最初未清除加载失败标记 |
提交 84e1580a4 补齐单行状态清除,并增加定向并发竞态测试 |
候选已关闭 |
| 复制信任 | 多个处理函数仅凭客户端可控的内部请求头就放行特权分支 | 先完成鉴权,要求精确标记并具备 s3:ReplicateObject 或 s3:ReplicateDelete 权限,将决策写入私有上下文后再清洗请求头 |
候选已关闭 |
| 流式上传 | 清洗请求头时克隆出的请求对象起初未共享原请求的 Trailer 映射 |
保留 Trailer 映射,确保流式上传末尾到达的校验和可见 |
候选已关闭 |
| Snowball | 请求级的信任标记可能在归档解压条目之间串用 | 按归档条目独立推导信任状态,并在并发工作协程间保留请求默认值 | 候选已关闭 |
| SSE-C | 零字节读取与 GetObjectAttributes 可能跳过客户端密钥认证 |
强制要求成功解封客户端密钥;仅对已授权的副本请求保留独立豁免分支 | 候选已关闭 |
| 删除授权 | 指定版本删除仅检查普通删除权限,未要求 s3:DeleteObjectVersion |
统一单对象与批量删除的权限检查,复制删除继续检查 s3:ReplicateDelete,并保留鉴权与审计上下文 |
候选已关闭 |
| 管理授权 | 用户与组状态变更始终检查启用权限 | 按请求的目标状态检查对应的启用或禁用权限 | 候选已关闭 |
| Checksum | 分片上传与复制路径遗漏校验和字段、接受非法组合,或在压缩/加密后的字节流上计算校验和 | 补齐算法与类型校验、服务端分片校验和计算、联邦透传、AWS 错误码及 CopyObject 流变换顺序 |
候选已关闭 |
| 发布证据 | 全量验收记录最初对应的是后续仍有改动的中间提交 | 分别记录 ebac0ca73 的全量验收结果与最终候选提交的定向门禁 |
证据缺陷已关闭 |
定义候选的四条不变量
信任只在鉴权后推导一次
看似内部专用的请求头本质上仍是客户端输入。请求必须先以原始签名形态通过既有鉴权路径,随后处理函数才能综合以下四项条件推导复制特权:
- 唯一且精确匹配的复制标记;
- 非匿名的已认证身份;
- 目标资源上的
s3:ReplicateObject或s3:ReplicateDelete权限; - 仅限副本(replica-only)语义所要求的副本状态。
推导结果存入私有请求上下文;后续剥离内部请求头仅作为面向旧调用链的纵深防御,而非权限来源。
这一顺序至关重要:SigV4 签名可能覆盖这些请求头,若在鉴权前剥离,合法的复制请求就会因 SignatureDoesNotMatch 被拒绝。此外,清洗后的克隆请求必须与原请求共享 Trailer 映射,因为流式上传的校验和会在请求体读取完毕后通过 trailer 到达。
完整的接收端信任模型见鉴权前不做 I/O,Header 不产生权限。
共享记录只有一个写边界
桶策略、生命周期、服务端加密(SSE)、标签、配额、复制、Object Lock、版本控制与 CORS 在逻辑上各自独立,但在存储层同属一条桶元数据记录。按字段拆分的互斥锁无法保护整条记录的“读-改-写”周期。
修复方案没有引入重型的事务层,而是统一收敛到单一的元数据写锁:
该锁不涉及对象数据 I/O,生命周期严格限制在单次桶元数据操作内。元数据迁移与自愈同样会替换整条记录,因此也必须受该锁保护。站点复制的接收端仅合并发生变更的字段,防止远端较旧的快照覆盖本地其他配置。
区分 CORS 加载失败与桶不存在
CORS 热路径必须明确区分以下四种状态:
| 状态 | 处理结果 |
|---|---|
| 元数据子系统尚未初始化 | 不返回 CORS 响应头 |
| 已知存在的真实桶,但元数据加载失败 | 不返回 CORS 响应头 |
| 内存常驻桶且配有桶级 CORS 规则 | 按该桶的 CORS 规则求值 |
| 无常驻元数据,且无已知的加载失败记录 | 回退至服务端全局 CORS 配置 |
表中第二行是保留失败桶集合的原因。预签名 URL 由签名本身完成授权,可在不经过桶策略求值的情况下访问私有对象;此时桶级 CORS 配置就是浏览器跨源访问的唯一边界。如果丢弃加载失败状态并回退到宽松的全局 CORS 配置,这一隔离边界就会失效。
该集合仅在真实桶加载失败时记录,规模受实际桶数量约束,并通过 noteLoadFailure 与 clearLoadFailure 两个辅助函数维护。成功加载、删桶、陈旧桶清理、刷新、重置与并发加载均已补齐测试覆盖。
按解析后的 XML 语义归一化 Object Lock
任何合法且处于启用状态的 Object Lock 配置都要求开启版本控制。XML 空白字符、元素顺序以及是否包含默认保留规则(Default Retention)都不会改变这一语义。因此,状态归一化必须在解析 XML 之后执行,而不能通过与单一标准 XML 进行字节比对来判断。
开启 Object Lock 后的版本控制状态固定为纯粹的 Enabled;暂停状态(Suspended)与前缀排除扩展均与 Object Lock 不变量冲突,会在配置更新、读回和磁盘重载时自动剥离。
复杂度审计
发布前复审专门排查了过度设计、重复逻辑、缺乏威胁模型的防御性代码,以及陈旧的兼容机制。
因保护已复现故障而保留
- 单一元数据锁: 确定性的跨配置类型并发测试已复现更新丢失。
- CORS 删除墓碑(tombstone): 缺少墓碑记录时,站点复制无法区分“配置已删除”与“从未配置”。
- CORS 加载失败状态: 预签名 URL 提供了已通过认证但绕过桶策略的具体反例。
- 两级复制信任判定: 普通复制与副本密文/SSE 透传具有不同的请求特征与权限要求。
- 鉴权后清洗请求头: 在 SigV4 验签前剥离请求头会破坏合法的签名请求。
- 多存储池与
null版本边界测试: 单池常规路径无法覆盖它们捕获到的跨池状态选择缺陷。
删除或收窄的复杂度
- CORS 失败集合的修改统一收口到
noteLoadFailure与clearLoadFailure。 - 站点复制导入路径改为仅应用变更字段,不再整条覆盖可能过期的记录。
- 删除废弃的加密辅助函数、无调用的事件目标函数与死代码分支。
- 兼容性守卫不再逐一枚举所有导出的源码符号,改为保护实际对外暴露的路由及冻结的协议/配置表面。
- 移除旧的
wait_pipe静态检查豁免,并以gomodguard_v2替代已弃用的配置。 - 动态超时测试不再从并行测试包中调用全局
rand.Seed。 - 服务端依赖从临时
silo-go分叉切回经审查的上游兼容minio-go版本。
刻意没有引入
- 不引入通用的元数据事务框架;
- 不引入第二套 CORS 缓存或无界的负缓存;
- 不新增公开的“可信复制”请求头;
- 不设置让服务端发布受制于后续 Console 或文档站发布的跨仓库门禁;
- 不在候选版本中塞入半成品的条件删除契约;
- 不在缺少专项收敛测试的前提下重写继承的站点复制状态寄存器。
延期事项,以及为什么严重度不同
| 事项 | 分类 | 20260903 发布决定(及后续状态) |
|---|---|---|
| 条件删除 #10 | 继承的 S3 功能缺口;影响假定服务端会强制校验 If-Match / 单对象 ETag 的调用方 |
0903 发布说明中明确标注;单对象条件删除已随 20260916(#145)交付,批量 ETag 条件删除仍待实现 |
| 多站点配置删除 #77 | 桶策略、SSE、标签与配额的跨站点删除收敛缺陷;CORS 已使用独立的修复寄存器 | 0903 作为多站点删除配置时的部署注意事项;已随 20260916 修复 |
ListMultipartUploads #79 |
继承的列表格式一致性缺口 | 作为已知问题记录,不影响常规分片上传的数据完整性 |
联邦 CopyObject #99、#100 |
旧版联邦后端的校验和与内联小对象兼容缺口 | 0903 仅影响特定联邦特性;已随 20260916 修复 |
| ILM 迁移 PR #60 与通用 SSE 议题 #61 | 新功能需求 | 不属于本次发布的安全与稳定性范围 |
证据
完整验收树
全量本地验收基于提交 ebac0ca73bbf251b070bb6df4d8005015841f901:
- 完整的
cmd与internal测试套件通过; - 完整的
cmd并发竞态套件(-race)耗时 365.448 秒,全部通过; - 静态检查(lint):0 告警;
- 品牌重命名/兼容性守卫与生成文件检查通过;
govulncheck扫描无可达漏洞;- 六种
make verify部署拓扑验收:174 PASS / 0 FAIL。
前两次运行 make verify 在拉取 mcli 时因网络代理与环境准备中断,并非测试用例失败。最终通过的运行使用了本地固定校验和的 mcli 二进制,保留访问 GitHub 的出站代理、对 localhost 绕过代理,并将 GNU 工具链置于 PATH 首位。
验收后的候选
全量验收之后唯一的代码改动是 84e1580a4:在按需重载桶元数据成功后清除对应的 CORS 加载失败标记。随后的候选合并未改动业务代码,6e112d185 仅更新了 Helm Chart 发布元数据与文档。最终候选提交通过了以下门禁:
git diff --check;- CORS 与 Object Lock 定向
go test -race; - 品牌重命名与兼容性守卫(
rebrand-guard); - 生成文件一致性检查;
- 静态检查(lint)0 告警;
- Helm Chart 语法检查、默认与可选模板渲染、Chart 打包,以及七类资源的旧版升级身份守卫。
Go、No-Go 与发布门禁
代码结论:GO
两轮复审确认的代码缺陷在候选版本中均已闭合。各项修复直接作用于对应不变量所在的模块边界,保留的防御机制均有可复现的故障反例支撑。
生产发布门禁
在正式发布 RELEASE.2026-09-03T13-18-01Z 前,候选版本依次通过了七道发布门禁:
- 候选提交推送至远端并完成代码审查;
- 远端 CI 与测试发布流水线在最新提交上全部通过;
- 预发布标签精确指向经审查的 Chart 7.0.2、Server 0903 与 Client 0903 代码树;
- 构建产物、校验和、SBOM、构建证明(attestation)与签名 RPM 包全部通过校验;
- 正式发布与容器镜像流水线完成 classic 与 distroless 两种镜像的构建与推送;
- 匿名下载与镜像拉取验证通过;
- 基于正式标签更新发布说明并完成文档站部署。
20260903 部署注意事项
升级至 20260903 时需满足以下部署前提:
- 在同一维护窗口内完成分布式集群所有节点的同步升级;
- 在启用桶级 CORS 配置前,先升级站点复制组内的全部成员;
- 核对并更新涉及
s3:DeleteObjectVersion及用户/组状态启停权限分离的 IAM 策略。
重启与读回验证(2026-09-11,#116)
后续的 #116 专项验收(2026-09-11 执行)测量了集群在有界重启窗口内的写入确认与读回一致性,验证方法涵盖四个要点:
- 确认台账。 每次返回成功的 PUT 都写入带版本的唯一对象键,并在收到响应时立即记录
VersionId、字节数与 SHA-256。后续读回按精确的VersionId获取对象并同时校验三项指标,防止早期确认的写入被后续写入静默覆盖。 - 读回时机。 数据探针(canary)写入成功后,分别在 15 秒、30 秒和 60 秒经由所有对端节点周期性重读;最终检查同时覆盖单节点中断期间写入的对象及其重新加入集群后写入的对象。
- 禁用重试掩盖。 每次探针操作设置 60 秒硬超时(涵盖连接建立、请求发送、响应体读取与等待间隔),并关闭客户端 SDK 自动重试,防止恢复窗口内的短暂失败被客户端重试掩盖。
- 容器拓扑。 单机运行四个拥有独立网络标识的 Linux/arm64 容器,每节点挂载一块盘(EC 2+2);
tmpfs卷由占位容器在整个停机窗口内保持挂载;四个节点以 10 秒宽限期并行优雅停止,随后并行启动。
管理接口就绪不代表数据面就绪。 原始测试记录对比了 0806、0903 与当时的 main 修复构建。main 构建在重启后 2.461 秒通过管理接口检查,但随后又经过 14.489 秒才完成首次数据探针读写。在该启动窗口内,admin/health 探针无法反映数据面状态,滚动升级脚本必须验证实际的数据面读写,而不能依赖固定时长的等待。
后续各项修复的交付状态见组件与源码矩阵。