SILO Server 20260903 发布前复审
这是 SILO 20260903 背后的发布前长期工程记录:为什么不能直接接受早期“所有问题都已解决”的结论,独立复审发现了什么,修复如何收窄,以及复审时距离生产发布还差哪些门禁。链接的发布说明记录了之后的正式交付结果。
结论:
6e112d1856d4f3655f30fc81ee47e9f43d50d8f3源码候选在代码层面可以 GO 到远端复审;生产发布仍是有条件 GO,必须等待远端 CI、Test Release、tag 与构件校验、签名、容器发布及公开 pull 验证完成。
基线:RELEASE.2026-08-06T00-00-00Z,提交3be10fcc1a44f6620ded0bd303461f9d688cca23。
范围: SILO Server 行为及其内嵌/锁定的运行时组件。文档、独立 Console、mcli、软件仓库、镜像与线上站点是彼此独立的交付物。
为什么需要第二轮审查
第一轮实现有很强的测试结果,也解决了大多数报告缺陷。但“已经全部就绪”的结论仍然过宽:它把绿灯测试与干净工作树当成了所有安全不变量均已闭合的证明。
对抗性复审换了一组问题:
- 同一不变量能否被另一种合法 wire representation 绕过?
- 预鉴权 fast path 是否仍会做 I/O 或获得状态?
- metadata 存在但加载失败时会怎样?
- 两条各自正确的 read-modify-write path 是否共享同一 serialization boundary?
- request sanitization 是否保留了全部 SigV4 streaming state?
- 验证声明说的是最终树,还是此前某棵树?
- 一个复杂机制是在保护已复现故障,还是只保护假想未来?
这轮审查在第一次“ready”之后继续找到了真实缺陷。正确做法不是推翻全部既有工作,而是把每个结论收窄到具体不变量与具体源码树。
分领域复审结果
| 领域 | 对抗性发现 | 最终解决方案 | 状态 |
|---|---|---|---|
| 桶元数据 | 独立配置锁会丢失共享 .metadata.bin 记录中的更新(#102) |
一个有界 metadata.lock 包围所有整记录 writer、migration、import、adoption、healing;复制只应用变化字段,避免陈旧整记录替换 |
候选已关闭 |
| 桶创建 | ForceCreate 与站点 adoption 可能用默认值覆盖既有配置 |
保留既有记录,只更新 creation/adoption 状态;增加 clobber regression test | 候选已关闭 |
| Object Lock | 将 lock 文档字节与一个 canonical XML 比较,会漏掉带 Default Retention rule 的合法配置 | 先 parse Object Lock,再从解析后的 enabled 状态推导 versioning 不变量;验证更新、读回、磁盘重载 | 候选已关闭 |
| 预鉴权 CORS | 任意 path segment 会触发 metadata read 与 cache growth | CORS lookup 只读 resident metadata,不进行 object-layer I/O | 候选已关闭 |
| CORS 启动期 | metadata 尚未初始化时,非驻留名字可能回落全局 CORS | 保留显式 fail-closed startup state | 候选已关闭 |
| CORS 加载失败 | 忘记一个真实桶曾加载失败,会让它与不存在的桶无法区分,使预签名请求落到全局 fallback | 维护有界 failed-bucket set;成功加载、删除、refresh 等路径清除;这些桶保持 fail-closed | 候选已关闭 |
| CORS 恢复 | 按需 GetConfig 成功重载最初没有清除 load-failure bit |
最后一行修复 84e1580a4,加定向 race 覆盖 |
候选已关闭 |
| 复制信任 | 多个 handler 把客户端可控内部 header 的存在当作特权 | 先鉴权;要求精确 marker 与 s3:ReplicateObject / s3:ReplicateDelete;用私有 context decision;之后再清洗 |
候选已关闭 |
| Streaming upload | 清洗后的 request clone 起初没有共享原 trailer map | 保留 trailer map,使迟到的 streaming checksum 可见 | 候选已关闭 |
| Snowball | request-wide trust bit 可能在解压 entry 间泄漏 | 每个 entry 独立推导 trust,并在 worker 间保留请求默认值 | 候选已关闭 |
| SSE-C | 零字节读取与 GetObjectAttributes 可跳过客户密钥认证 |
要求成功解封 key;真正授权的 replica 走独立例外 | 候选已关闭 |
| 删除授权 | 显式版本删除检查普通 delete action,而不是要求 s3:DeleteObjectVersion |
对齐单删与批删授权;复制删除保留 s3:ReplicateDelete;保留 auth/audit context |
候选已关闭 |
| 管理授权 | 用户/组状态变更始终检查 enable action | 检查与目标状态匹配的 action | 候选已关闭 |
| Checksum | Multipart/copy 路径漏字段、接受非法组合,或在错误 representation 上计算 | 补齐算法/类型校验、服务端 part 计算、联邦传递、AWS 错误、CopyObject transform 顺序 | 候选已关闭 |
| 发布证据 | 完整验收最初描述的是之后仍发生变化的树 | 分别记录 ebac0ca73 的完整验收与当前树的定向门禁 |
证据缺陷已关闭 |
定义候选的四条不变量
信任只在鉴权后推导一次
看起来像内部字段的 header 仍然是客户端输入。请求必须保持原始签名形态,先通过现有 authentication path;随后 handler 才能组合:
- 唯一且精确的 replication marker;
- 非匿名、已认证身份;
- 对目标 resource 的
s3:ReplicateObject或s3:ReplicateDelete; - 在更窄的 replica-only 语义中所需的 replica status。
结果存入私有 request context。header stripping 是给旧 consumer 的纵深防御,不是 authority 来源。
顺序很重要,因为 SigV4 可能签了这些 header。鉴权前清洗会让合法复制返回 SignatureDoesNotMatch。sanitized clone 还必须共享 request trailer:trailer 在初始 header parse 之后到达,承载 streaming checksum。
完整接收端模型见 鉴权前不做 I/O,Header 不产生权限。
共享记录只有一个写边界
Policy、lifecycle、SSE、tags、quota、replication、Object Lock、versioning、CORS 是逻辑字段,却是同一 bucket record 的物理成员。每字段 mutex 无法保护整记录 read-modify-write。
选定修复比引入数据库或通用 transaction layer 更小:
锁不覆盖对象数据 I/O,只限于一次 bucket-metadata 操作。Migration 与 healing 同样参与,因为它们也会替换整条记录。Replication receiver 只 merge 变化字段,避免远端旧 snapshot 擦除无关本地状态。
失败是一种状态,不等于不存在
CORS hot path 必须区分四种状态:
| 状态 | 结果 |
|---|---|
| Metadata system 尚未初始化 | 不返回 CORS header |
| 已知真实桶,但 metadata load 失败 | 不返回 CORS header |
| Resident bucket 且有桶级 CORS 文档 | 评估该文档 |
| 无 resident metadata,也没有已知失败 | 使用服务端全局 fallback |
第二行解释了为什么 failed-bucket set 在简化审查后仍然保留。预签名 URL 已经通过自身签名获得授权,无需 bucket-policy evaluation;此时 bucket CORS 文档就是 browser-origin boundary。丢掉失败 bit 并使用宽松全局 fallback,会削弱该边界。
集合只由真实 bucket load attempt 产生,因此有界;生命周期通过两个 helper 维护。成功 load、remove、stale-bucket cleanup、refresh、reset、concurrent load 都有测试。
Object Lock 看语义,不看文本
任何解析有效且 enabled 的 Object Lock 配置都意味着 versioning。XML whitespace、element order 与 Default Retention rule 不改变语义。因此 normalization 必须发生在 parse 之后,而不是将 bytes 与某一个 canonical document 比较。
最终 versioning record 是纯 Enabled;suspended state 与 exclude-prefix 扩展和 lock 不变量冲突,会在 update、read-back、reload 时移除。
复杂度审计
发布前审查专门查找 over-design、重复、没有 threat model 的过度防御,以及陈旧兼容 machinery。
因保护已复现故障而保留
- 一把 metadata lock: deterministic cross-type lost-update 测试已经复现数据丢失。
- CORS tombstone: 没有 tombstone,站点复制无法区分删除与“从未观察到”。
- CORS load-failure state: 预签名 URL 给出了已认证且不经 policy 的具体反例。
- 两级 replication trust: 普通 replication 与 replica ciphertext/SSE 语义并不使用完全相同的 wire shape。
- 鉴权后清洗: SigV4 之前清洗会破坏合法签名请求。
- 多池/null-version 对抗测试: 单池 happy path 无法覆盖它们抓到的状态选择故障。
删除或收窄的复杂度
- CORS failure-set mutation 收口到
noteLoadFailure与clearLoadFailure。 - Replication import 只应用变化字段,不复制陈旧的完整记录。
- 删除过期 encryption helper、死 event-target function 与废弃 handler branch。
- Compatibility guard 不再枚举每个 exported source symbol,只保护真实 served route 与冻结的 wire/config surface。
- 删除旧
wait_pipelint exemption;用gomodguard_v2替代弃用配置。 - Dynamic timeout 测试不再从 parallel package 调用全局
rand.Seed。 - 服务端从临时
silo-go分叉回到已审查的上游兼容minio-gorevision。
刻意没有引入
- 没有通用 metadata transaction framework;
- 没有第二套 CORS cache 或无界 negative cache;
- 没有新增公开“trusted replication”请求 header;
- 没有让服务端发布依赖未来 Console 或文档发布的跨仓库 gate;
- 没有把半套 conditional-delete contract 塞进候选;
- 没有在缺少专用 convergence test 时重写全部继承的 site-replication register。
延期事项,以及为什么严重度不同
| 事项 | 分类 | 发布决定 |
|---|---|---|
| 条件删除 #10 | 继承的 S3 缺失功能;只对假定服务器会执行未支持 If-Match / per-object ETag 的调用方危险 |
显著记录;不合并不完整 PR,也不发布只有单对象的一半合同 |
| 多站点配置删除 #77 | policy/SSE/tags/quota 的继承收敛缺陷;CORS 使用独立已修 register | 不是单站点 blocker;依赖这些多站点删除的用户需要附加部署条件 |
ListMultipartUploads #79 |
继承的 listing conformance gap | 已知问题;不是普通 multipart workflow 的数据完整性 blocker |
联邦 CopyObject #99、#100 |
旧后端 checksum/inline-object 缺口 | 阻塞受影响 feature 的使用,不阻塞通用 server 发布 |
| ILM relocation PR #60 与 broad SSE issue #61 | 新能力请求 | 不属于本版本安全边界 |
“继承”不等于无害,而是说明缺陷不是本变更集引入,应按公开 release contract 评估。若某个部署依赖受影响路径,即使通用版本仍是 conditional GO,该部署也有自己的 stop condition。
证据
完整验收树
完整本地验收对应 ebac0ca73bbf251b070bb6df4d8005015841f901:
- 完整
cmd与internal套件; - 完整
cmdrace:365.448 秒,通过; - lint:0 issue;
- rebrand/compatibility 与 generated-file guard;
govulncheck无 reachable vulnerability;- 六种
make verify部署形态:174 PASS / 0 FAIL。
前两次 make verify 在获取 mcli 时遇到环境/准备失败,并非测试失败。成功运行使用本地 checksum-pinned mcli,保留到 GitHub 的 outbound proxy,对 localhost 绕过代理,并把 GNU userland tool 放在 PATH 前部。这个区别属于证据,不应隐藏。
验收后的候选
完整运行之后唯一代码修改是 84e1580a4:metadata 按需重载成功后清除一个 CORS failure-state bit。候选 merge 不改代码;6e112d185 只修改 Helm 发布元数据与文档。在最终候选上以下门禁通过:
git diff --check;- CORS 与 Object Lock 定向
go test -race; - rebrand guard;
- generated-file check;
- lint 0 issue。
- Helm lint、默认与可选 render、chart package,以及七资源旧版升级身份守卫。
证据强度与一行状态迁移修复相称,但推送后的树仍必须运行远端 CI 与 release workflow。
Go、No-Go 与剩余门禁归属
代码结论:GO
两轮复审确认的代码缺陷在候选中均已解决。修复落在对应不变量所在层;保留的复杂度都有已复现反例支撑。
生产结论:有条件 GO
以下全部成为事实前,不能把服务端称为“已发布”:
- 候选提交推送并完成 review;
- 远端 CI 与 Test Release 在 pushed head 通过;
- 预定 tag 指向已审查的 chart 7.0.2、Server 0903、Client 0903 release tree;
- Draft artifact、checksum、SBOM、attestation、签名 RPM 全部通过校验;
- finalize 与 Docker release 发布 classic、distroless 两种镜像;
- 匿名下载与 pull 测试通过;
- 发布说明用 tagged fact 更新,文档站部署完成。
第 1~6 项任一失败都是 release blocker;本地绿灯无法替代它们。
部署特定停止条件
即使版本成功发布,以下条件无法满足的运维方也应延后升级:
- 无法在同一维护操作中更新分布式集群全部节点;
- 无法在使用桶级 CORS 前更新站点复制组全部成员;
- 无法调整
s3:DeleteObjectVersion与 status-action separation 相关 IAM 策略; - 业务依赖 #10、#77、#79、#99 或 #100 路径,却不能显式接受对应已知限制。
最终结论有意比“所有问题都已修好”更窄:经审候选已经可以进入发布机器;剩余限制全部显式;生产发布由可验证构件把关,而不是由信心把关。