fix(replication): preserve normalized replica metadata

Restore only the six replication-specific metadata fields after trust
validation, so streaming uploads retain their actual content encoding and
Snowball entries do not inherit ordinary metadata from the outer archive.

Include helper, authenticated PUT/COPY/multipart and Snowball regressions,
plus the R7 investigation, actual Opus 5 consensus and local verification.
The production change is based on PR #187 by Mikhail Khadarenka.

Co-authored-by: Mikhail Khadarenka <chodorenko@gmail.com>
Signed-off-by: Feng Ruohang <rh@vonng.com>
This commit is contained in:
Feng Ruohang
2026-09-16 00:00:17 +08:00
parent 9ebe81c1b3
commit 4fcdf37ce6
17 changed files with 1104 additions and 24 deletions
@@ -0,0 +1,28 @@
本轮只做回应核验,未重复独立审计;无 shell,未计算哈希、未运行任何测试。
# R7 v1 第二轮确认(Opus 5)
## 1. 版本与哈希
- 冻结方案:`docs/investigations/r7/plan-v1.md`,任务声明 SHA-256 **`7af5705ebbfb0a375956d38dba059095dc16e24558290b1bd35a6ce85b9e1f96`**。
- 处置附录:`docs/investigations/r7/plan-v1.dispositions.md`,任务声明 SHA-256 **`d32d30f8a420f904da6ee039f0461d6281e7da44bd805d416b4970d071f0af32`**。
- 两个哈希均按任务声明采纳,本会话无法计算,校验仍属调用方责任。产品基线 `9ebe81c1b3611f9cc73e676b5b741c2be62c467a`,工作区确认未改产品代码(`docs/investigations/r7/` 为 untracked)。生产补丁仍读 `pr187.diff`,作为提议代码而非我运行的证据。
## 2. 三个争议点的裁定
**N6:我错了,撤回。** `cmd/erasure-server-pool-tags_test.go:258-264` 确有 `type tagTestCapacityDisk struct{ StorageAPI }`,其 `DiskInfo` 把 `Total=Free, Used=0`,并已在同文件 `:131` 被 `TestReplicaWritesPreserveTagOrdering` 使用。我上轮按 “adapter” 字面 grep 漏掉了该类型,所以 plan 第 56 行 “repository already used 的临时容量适配器” 属实。撤回 N6 的事实判断,保留其被接受的部分:临时 `capacity-test-utils_test.go` 不进交付 diff,原始 507 与适配过程需记录。补充一句非阻塞:若正式回归仍需容量适配,直接复用同包内已有类型即可(不算临时旁路),产品容量策略不得改动。
**N1:接受纠正。** `object-handlers.go:2740` 的 `rawReplica` 是整包级判定,`:2788-2791` 对任何缺 `ReplicateObject` 的条目直接 `ErrAccessDenied` 并中止该条目;因此我建议的“同一 REPLICA 包内 trusted 与 untrusted 条目对比”在机制上不可能成立。Codex 的做法正确:同一 tar 分别发 ordinary 与 replica 两次请求,比较条目元数据(排除 replica 状态/时间戳/ETag)。实质结论不变且已被证据坐实——补丁前 `:2835-2838` 的恢复分支会把外层 `supportedHeaders` + 用户元数据整体灌进仅含 storage class 的 `metadata`(`:2802-2804`),补丁后只剩六个映射;`http-baseline-v2.log:195-201` 的 `Erasure/no-pax` 正是外层 `aws-chunked` 泄漏。可选增补(非必须):再加一条 untrusted-marker 归档对照。
**N5:接受收窄措辞。** 源码只能支撑到:`bucket-replication.go:987-997` 的逐字符串比较 → `replicationActionForTarget:1131` → 仅在 `replicateObjectToTarget:1598` 的复制任务里求值,且成功后置 `Completed`、不自我重排队。所以“每次 heal/resync/重放对账都会再次选中 `replicateMetadata`” 成立,“不间断热循环” 我上轮说过头,撤回该措辞。补救顺序“先确认并修权威源版本、再协调副本”,以及把重复元数据复制/不一致记为已知影响,均予保留。
## 3. 其余处置确认
- **N2 已用实测兑现**:`http-baseline-v2.log` 是 handler 级证据,单盘 `ErasureSD` 与 16 盘 `Erasure` 均覆盖 put/copy-replace/multipart,失败精确落在 `replica/bare`(persisted/GET/HEAD 均为 `aws-chunked`,期望空)与 `replica/mixed`(`aws-chunked,gzip` vs `gzip`),ordinary、untrusted-marker、gzip、unauthorized-replica 全通过;连同 Snowball 8 例,64 叶 = 44 通过 / 20 失败,与附录计数一致。护栏诉求已满足(该日志由本会话之外产生,我只读未跑)。
- **N3 / N7 / N8 / N9**:接受无异议。N3 额外要求断言 XML 码为 `AccessDenied` 以区分签名失败,正确。
- **N4**:接受,且两条期望值与代码一致——`handler-utils.go:357-368` 按 `,` 精确等值比较,故 `"aws-chunked, gzip" → " gzip"`(保留前导空格)、`"gzip, aws-chunked" → "gzip, aws-chunked"`。仅作现状记录,不得宣传为本次修复。
- **最终验收范围补充**:同意。无双站点调度器/重启/网络故障验收时只报告本地接收链路与既有 SSE 结论;存量修复只出设计文件,不授权扫描或改写现网对象。
## 4. 结论
**APPROVE。阻塞问题 0。** plan-v1 + 本处置附录构成的 v1 组合可直接进入实现,无需 v2;N1/N5 采用 Codex 的修正表述,N6 以我撤回告结。实现时请把 N1(无 PAX 行为变化)、N4(空格现状)、N5(收敛风险与补救顺序)落为断言或文档,并确保临时容量适配文件不出现在交付 diff 中。
@@ -0,0 +1,36 @@
{
"requested_model": "claude-opus-5",
"requested_effort": "max",
"cli_version": "2.1.270",
"baseline": "9ebe81c1b3611f9cc73e676b5b741c2be62c467a",
"plan": "plan-v1.md",
"plan_sha256": "7af5705ebbfb0a375956d38dba059095dc16e24558290b1bd35a6ce85b9e1f96",
"addendum": "plan-v1.dispositions.md",
"addendum_sha256": "d32d30f8a420f904da6ee039f0461d6281e7da44bd805d416b4970d071f0af32",
"status": "completed",
"started_at": "2026-09-15T15:51:25.514239+00:00",
"raw_output": "/Users/vonng/tmp/silo-r7-20260915-ad51/opus-v1-confirmation.jsonl",
"stderr": "/Users/vonng/tmp/silo-r7-20260915-ad51/opus-v1-confirmation.stderr.log",
"command": "claude --print --model claude-opus-5 --effort max --safe-mode --permission-mode plan --tools Read,Grep,Glob --strict-mcp-config --no-session-persistence --output-format stream-json --verbose --add-dir /Users/vonng/tmp/silo-r7-20260915-ad51",
"actual_assistant_models": [
"claude-opus-5"
],
"subtype": "success",
"is_error": false,
"duration_ms": 57910,
"num_turns": 18,
"session_id": "9b4140a8-140f-4d94-9a59-9983346709fd",
"raw_sha256": "2e2db44411a0b367607b73a2f7fe38c5125f49294f496d603c8ed4975c549bba",
"review_sha256": "a471ca03b6e68d77d01dede8c6d1a8f989bceed9f7eeb99fc36634c025eb3541",
"verdict": "APPROVE",
"blocking_findings": 0,
"completed_at": "2026-09-15T15:53:09.806502+00:00",
"auxiliary_model_ids": [
"claude-haiku-4-5-20251001",
"claude-opus-5"
],
"observed_tool_attempts": [
"Grep",
"Read"
]
}
@@ -0,0 +1,7 @@
This is round 2 of the actual R7 Opus review discussion. Product baseline remains 9ebe81c1b3611f9cc73e676b5b741c2be62c467a and NO product code has been changed. Your first actual review is /Users/vonng/.codex/worktrees/ad51/silo/docs/investigations/r7/review/opus-v1.md. It approved v1 with 9 nonblocking notes and zero blockers.
Please read the SAME frozen plan /Users/vonng/.codex/worktrees/ad51/silo/docs/investigations/r7/plan-v1.md (SHA-256 7af5705ebbfb0a375956d38dba059095dc16e24558290b1bd35a6ce85b9e1f96) and Codex's numbered replies / binding acceptance addendum /Users/vonng/.codex/worktrees/ad51/silo/docs/investigations/r7/plan-v1.dispositions.md (SHA-256 d32d30f8a420f904da6ee039f0461d6281e7da44bd805d416b4970d071f0af32). The plan+addendum is the exact v1 consensus bundle; production patch remains /Users/vonng/tmp/silo-r7-20260915-ad51/pr187.diff.
Focus this round on replies, not repeating the whole independent audit. Check the factual disagreement N6 by reading cmd/erasure-server-pool-tags_test.go lines 258-265 (tagTestCapacityDisk does exist; Grep for adapter missed it). Check N1's parity testing correction: a single REPLICA archive with an unauthorized entry rejects that entry, so equal ordinary vs replica archive uploads is the correct comparison. Check N5's narrower statement about repeated reconciliation opportunities rather than claiming an automatic continuous hot loop. N2 is now backed by /Users/vonng/tmp/silo-r7-20260915-ad51/http-baseline-v2.log. The other notes are accepted and will become assertions/docs.
Return a concise Chinese confirmation that (a) explicitly names both hashes, (b) agrees/disagrees with each disputed point N1/N5/N6 and remaining dispositions, and (c) states APPROVE or REQUEST_CHANGES and whether there are any blockers to implementing this exact v1 bundle. Do not simulate tests. No shell is available: hash checking remains the caller's responsibility. Do not attempt Write or write a Claude plan file; tools are restricted to Read/Grep/Glob and your review is the final text.
+51
View File
@@ -0,0 +1,51 @@
Write 工具在本会话被禁用,因此评审直接以回复形式交付(本次任务本就是只读评审,未改动任何文件、未运行任何测试)。
---
# R7 修复方案独立评审
## 1. 审阅版本与哈希
- **审阅对象**:`/Users/vonng/.codex/worktrees/ad51/silo/docs/investigations/r7/plan-v1.md`,72 行,标题 `R7 plan v1: preserve normalized replica object metadata`(`docs/investigations/r7/` 在 git status 中为 untracked)。
- **任务给定 SHA-256**:`7af5705ebbfb0a375956d38dba059095dc16e24558290b1bd35a6ce85b9e1f96`。本会话只读且无 shell,**我未重新计算该哈希**;逐字审阅的是该路径当前内容。
- 基线 `9ebe81c1b3611f9cc73e676b5b741c2be62c467a`、引入提交 `56fa63bf…`、PR #187 的 OPEN/head 状态均按任务声明采纳,**未用 git/gh 复核**。
- `pr187.diff` 被当作“提议的代码”阅读,不视为已运行的测试;`baseline.log` 由本会话之外产生,我只读未跑。
- **性质**:源码与方案验证,非我执行的测试。
## 2. 结论
**APPROVE_WITH_NONBLOCKING_NOTES**
## 3. 阻塞问题
**无阻塞问题。** 以下为逐项核验依据。
**最小补丁充分性(充分)**:恢复辅助函数是 replica 路径上唯一能把未归一化的 ordinary 头写进对象元数据的入口。`putOptsFromHeaders`/`getDefaultOpts`(`cmd/object-api-options.go:388-477`)只读 SSE 与 source-* 时间戳;`completeMultipartOpts:542-548` 只取 actual-object-size 与 ssec-crc。五处调用点(`object-handlers.go:1149/2282/2836/2869`、`object-multipart-handlers.go:246`)全部被覆盖。
**信任边界(未改变)**:`evaluateReplicationTrust`(`cmd/replication-trust.go:78-90`)要求已认证主体 + 精确单值 `true` 标记 + `s3:ReplicateObject`,`REPLICA` 声明无权限直接 403;Snowball 走等价的 per-entry 内联判定(`object-handlers.go:2784-2793`)。补丁不前移恢复点、不让头部本身产生信任。
**调用链(方案描述与代码一致)**:PUT 在签名校验后评估信任、元数据在 `:2199` 已归一化;COPY REPLACE 用 `extractMetadataFromReq`、COPY 保留源语义(`:1143-1156`、`:1411`、`:1801`);多段初始化在 sanitization 之后提取(`object-multipart-handlers.go:179/233`);**分片与完成确实不需要恢复**——分片从 `mi.UserDefined` 取加密状态(`:885-886`、`:957-987`),完成从 `completeMultipartOpts` 取两个字段。
**六个映射与空标记**:补丁遍历 `replicationToInternalHeaders`(`handler-utils.go:106-114`),与基线遍历 `supportedHeaders` 的键集完全相同,且六→六为单射,故 map 迭代顺序无关;空值 multipart 标记按 key 存在性消费(`internal/crypto/metadata.go:27`),`strings.Join([]string{""}, ",")==""` 行为与基线一致。
**归一化与冗余用户元数据**:基线恢复分支会把刚被 `extractMetadata`(`:218-241`)删除的 `X-Amz-Meta-X-Amz-Unencrypted-Content-Length/-Md5`(`internal/http/headers.go:138-139`,GHSA-76wf-9vgp-pj7w)按原始大小写写回,补丁一并消除。
**额外独立验证(支持方案的关键事实)**:本仓库固定的 minio-go(`go.mod:71` → `pkg/signer/utils.go:70-87 setAwsChunkedContentEncoding`)**保留调用方已设编码**并生成 `aws-chunked` 或 `aws-chunked,gzip`(无空格)。因此方案声明的 `aws-chunked→无`、`aws-chunked,gzip→gzip` 与真实复制线路一致,修复后目标端存储值将等于源端 `objInfo.ContentEncoding`(`bucket-replication.go:838`),读路径 `erasure-metadata.go:138` → `api-headers.go:129-131` 也成立。
## 4. 非阻塞意见
1. **Snowball 无 PAX 条目的行为变化必须显式承认并加断言**。证据:`object-handlers.go:2802-2842` 的 `metadata` 只有 storage class 与压缩键,基线恢复会把外层 tar 请求的 content-type / `x-amz-meta-*` / cache-control 复制进每个 entry;补丁后不再复制,与普通 Snowball(`:2874-2877` 分支从不做 `extractMetadata`)一致。我同意这个选择,但它超出“只去掉 aws-chunked”。最小修正:在 §Proposed patch 第 6 条写明“trusted replica 无 PAX 条目不再继承外层归档 ordinary 元数据”,并在 §Verification 4 增加断言:同一 tar 中 trusted 与 untrusted 无 PAX 条目的 UserDefined(除 replica 状态/时间戳/ETag 外)相等;同时注明“外层请求的六个字段仍套用到所有条目”是既有且有意保留的行为。
2. **回归护栏应绑定 handler 级用例**。`baseline_test.go` 只覆盖 helper、绕过信任门;真正会退化的是调用点。最小修正:§Verification 7 的“基线必须失败”至少绑定一条 HTTP 用例(trusted replica streaming PUT → `GetObjectInfo().ContentEncoding`)。
3. **流式签名测试必须在签名前注入 replication 头**。`newTestStreamingSignedCustomEncodingRequest`(`test-utils_test.go:817-834`)先 Set 编码再签名;若签名后再加 `x-amz-bucket-replication-status`,得到的是 403 SignatureDoesNotMatch,容易被误读成“未恢复元数据”。最小修正:在 §Verification 2/3 补一句,并要求区分签名失败与权限拒绝。
4. **精确 token 裁剪的空格限制未被测试固定**。`handler-utils.go:357-368` 按 `,` 分割做精确等值比较,`"gzip, aws-chunked"` 不会被裁剪。同意不改语法;最小修正:§Verification 1 增加两条“记录现状”的断言用例。
5. **已污染对象的收敛性风险应进入补救段**。`bucket-replication.go:987-997` 用源端 `ContentEncoding` 与目标端元数据比较,而 minio-go 在已含 `aws-chunked` token 时直接早退(`utils.go:78-80`):源端仍为 `aws-chunked` 的历史对象在目标端修复后将永久 mismatch 并反复触发 metadata 复制。最小修正:补救顺序明确为“先修源端版本、再 reconcile 副本”,并把该 churn 列为已知影响与监控项。
6. **“仓库已有的容量适配器”与现状不符**:`cmd/` 下不存在此类测试旁路(`object-api-utils.go:1302 hasSpaceFor` 无测试开关;`cmd` 内 grep `adapter` 仅命中 `object-api-interface.go:325` 注释)。最小修正:改写为“显式记录所用临时本地改动并保证其不进入交付 diff”。
7. **map 迭代等价性建议写入方案**(第 3 条补一句“顺序无关性来自单射映射”),避免未来新增映射时被误改;`nv` 对重复大小写头仍是“最后写入者胜”,与基线相同,不是本次引入。
8. **GHSA 冗余键回写的修复建议在根因段显式成句**;并注意 `putReplicationOpts`(`bucket-replication.go:797-813`)不像 `getCopyObjMetadata`(`:733-735`)那样过滤这两个键,故历史对象可能确实携带它们,修复后目标端会与普通 PUT 一样丢弃。
9. **POST 表单路径的不对称**:`bucket-handlers.go:1260-1266` 直接调用 `extractMetadataFromMime`,本就不做裁剪/默认 content-type/冗余键删除;补丁未改变它,建议在方案中补半句,避免未来“顺手统一”而改变 POST 行为。
## 5. 是否同意同一 v1 方案进入实现
**同意。** 该 v1 方案的根因、信任边界、调用链与补丁范围均与当前基线源码相符,不存在阻塞缺陷;上述 9 条均为记录/测试/文档层面的处置,不需要改动补丁本体。在 Codex 逐条记录处置(尤其第 1、2、5 条)后,**同一 v1 方案可直接进入实现,无需 v2**。
最后重申两点事实边界:PR #187 diff 中的测试改动是提议代码而非已运行证据;`baseline.log` 的失败断言是既有缺陷的证据,非实现验证——两者都不是我在本会话中运行的测试。
@@ -0,0 +1,37 @@
{
"requested_model": "claude-opus-5",
"requested_effort": "max",
"cli_version": "2.1.270",
"baseline": "9ebe81c1b3611f9cc73e676b5b741c2be62c467a",
"plan": "plan-v1.md",
"plan_sha256": "7af5705ebbfb0a375956d38dba059095dc16e24558290b1bd35a6ce85b9e1f96",
"status": "completed",
"started_at": "2026-09-15T15:44:49.861232+00:00",
"raw_output": "/Users/vonng/tmp/silo-r7-20260915-ad51/opus-v1.jsonl",
"stderr": "/Users/vonng/tmp/silo-r7-20260915-ad51/opus-v1.stderr.log",
"command": "claude --print --model claude-opus-5 --effort max --safe-mode --permission-mode plan --tools Read,Grep,Glob --strict-mcp-config --no-session-persistence --output-format stream-json --verbose --add-dir /Users/vonng/tmp/silo-r7-20260915-ad51",
"actual_assistant_models": [
"claude-opus-5"
],
"observed_tool_attempts": [
"Glob",
"Grep",
"Read",
"Write"
],
"subtype": "success",
"is_error": false,
"duration_ms": 309465,
"num_turns": 62,
"session_id": "10f1e9b3-7120-4a1f-94d9-e53745849cd1",
"raw_sha256": "b749be2acbec45b978907849c741e1f71994b0ad4e13d5b5204f429c9ba408b3",
"review_sha256": "d832e461291f0ec32455ade97c9bf9cd02d31812642a4b9254dd5e06ab1cc82b",
"verdict": "APPROVE_WITH_NONBLOCKING_NOTES",
"blocking_findings": 0,
"completed_at": "2026-09-15T15:51:03.390714+00:00",
"auxiliary_model_ids": [
"claude-haiku-4-5-20251001",
"claude-opus-5"
],
"note": "Actual reviewer assistant messages all identify claude-opus-5. CLI plan mode attempted Write to a Claude plan artifact, but Write was disabled; final review was returned as text. No product changes."
}
@@ -0,0 +1,12 @@
Independently review the R7 repair plan. Do not edit files or run implementation. The user requires real Opus 5.0 discussion and explicit consensus before product code changes. Disagree when evidence warrants it; do not assume PR author claims are tests we ran.
Repository baseline: 9ebe81c1b3611f9cc73e676b5b741c2be62c467a.
Plan v1: /Users/vonng/.codex/worktrees/ad51/silo/docs/investigations/r7/plan-v1.md
Plan SHA-256: 7af5705ebbfb0a375956d38dba059095dc16e24558290b1bd35a6ce85b9e1f96
Current upstream PR snapshot and proposed production patch: /Users/vonng/tmp/silo-r7-20260915-ad51/pr187.json and /Users/vonng/tmp/silo-r7-20260915-ad51/pr187.diff
Direct current-baseline helper reproduction: /Users/vonng/tmp/silo-r7-20260915-ad51/baseline_test.go, /Users/vonng/tmp/silo-r7-20260915-ad51/baseline.log (expected assertions fail).
Read the full plan and then independently inspect relevant current code including cmd/handler-utils.go, cmd/replication-trust.go, all five restoration call sites in cmd/object-handlers.go and cmd/object-multipart-handlers.go, existing test fixtures and SSE replication consumers. The Snowball no-PAX caller has NOT already performed generic ordinary metadata extraction; explicitly evaluate the proposed behavior there.
Check: minimal patch sufficiency; ordinary and trusted/replica trust boundary; PUT/COPY/multipart/parts/completion/Snowball call chains; six SSE-only mappings and empty multipart marker/checksum; normalization and removed unsafe ordinary user metadata; correct actual HTTP tests; stored-object remedial design risks. Do not expand this into independent R4/R5 issues unless this proposed fix depends on them.
Return a review in Chinese with (1) the reviewed version and exact hash, (2) verdict APPROVE / APPROVE_WITH_NONBLOCKING_NOTES / REQUEST_CHANGES, (3) each blocking issue with severity, exact code/plan evidence and smallest correction, (4) separately numbered nonblocking notes, (5) explicit whether you agree this SAME v1 plan can proceed to implementation after Codex records its dispositions. If no blocking issues, say so. Your review is source/plan validation, not actual tests run by you.