Skip to content

fix: keep rewritten CopyObject data and metadata consistent - #86

Merged
Vonng merged 3 commits into
mainfrom
codex/issue-83-copy-null-version-release
Aug 29, 2026
Merged

fix: keep rewritten CopyObject data and metadata consistent#86
Vonng merged 3 commits into
mainfrom
codex/issue-83-copy-null-version-release

Conversation

@Vonng

@Vonng Vonng commented Aug 29, 2026

Copy link
Copy Markdown
Member

Summary

  • mirror the object layer rewrite decision before preserving compression metadata
  • re-encrypt SSE-C key rotations whenever the object layer must write new bytes
  • cover null-version corruption in both compression directions, encrypted rewrites, wrong/equal keys, and predicate cases

Dependency

This PR is intentionally based on codex/server-prerelease-foundation; the destination-key checksum fix must precede these CopyObject changes.

Validation

  • focused normal and race on both object-layer backends
  • independent Claude Opus 5 Max review: GO, no blocking finding
  • combined candidate full CI-tagged suite passes

Refs #83. Never merge the old codex/issue-64 branch; it contains a duplicate of an already-merged federation fix.

Vonng and others added 3 commits August 29, 2026 16:00
CopyObjectHandler recorded the source compression metadata whenever the copy
was metadata-only, on the assumption that the object layer would then leave
the stored bytes alone. That assumption does not hold. Both
erasureServerPools.CopyObject and erasureSets.CopyObject only skip a data
rewrite in three cases, and otherwise fall back to a full PutObject.

The reachable gap is a copy whose source is a null version on a bucket that
gained versioning after the object was written. Neither version ID is set, so
the self-referential version branch is skipped, the data is rewritten as
plaintext, and the preserved compression metadata then described bytes that
no longer exist. A subsequent GET failed with "s2: corrupt input".

Mirror the object layer's decision in copyRewritesObjectData and record the
compression metadata from it, so the metadata always describes whichever
bytes are finally stored. The source version selection that lets a versioned
metadata-only copy add a self-referential version moves next to the same
decision, since both depend on the effective metadata-only value.

Signed-off-by: Feng Ruohang <rh@vonng.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A key rotation rewraps the object key held in metadata; it never re-encrypts
the stored bytes. CopyObjectHandler took that shortcut whenever the request
looked like a same-object SSE-C rotation, on the assumption that the object
layer would then leave the stored bytes alone. That is the same assumption
copyRewritesObjectData() was added to stop making.

When the source is a null version on a bucket that gained versioning after the
object was written, the object layer cannot reference that version and falls
back to PutObject. The reader at that point holds plaintext decrypted with the
old key and no EncryptFn is set, so the destination ends up storing plaintext
under metadata that claims the object is SSE-C encrypted. A subsequent GET
failed with "sio: unsupported version".

Gate the rotation shortcut on the same prediction the compression metadata
already uses. When the object layer stores new object data the rotation falls
through to the regular re-encrypting copy, which decrypts with the old key and
re-encrypts with the new one. The source version selection moves next to the
gate because both decisions need it.

That fallback authenticates the source key through the source decryptor, which
GetObjectNInfo does not build for a zero byte object. Check the key explicitly
before the destination is written, so the gate cannot turn a rotation that the
shortcut rejected with AccessDenied into one that succeeds. The re-encrypting
copy regenerates the encrypted ETag, unlike an in-place rotation; the test
records that difference.

The other three object layer CopyObject callers that set metadataOnly -
PostRestoreObjectHandler, updateRestoreMetadata and batchKeyRotate - address
the same version on both sides and never set Versioned, so they only reach the
two in-place cases already covered by the copyRewritesObjectData table.

Signed-off-by: Feng Ruohang <rh@vonng.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fk3PAD7DHCYzcyegYWqAmt
Exercise the silent compression-on-copy path, compressed SSE-C re-encryption, and the equal-invalid-key error contract on both object-layer backends.

Signed-off-by: Feng Ruohang <rh@vonng.com>
@Vonng
Vonng changed the base branch from codex/server-prerelease-foundation to main August 29, 2026 10:41
@Vonng Vonng closed this Aug 29, 2026
@Vonng Vonng reopened this Aug 29, 2026
@Vonng
Vonng merged commit 2b2e0d2 into main Aug 29, 2026
8 checks passed
@Vonng
Vonng deleted the codex/issue-83-copy-null-version-release branch September 2, 2026 15:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant