Skip to content

fix(r2): return source digest/size for mounted blob HEAD responses#121

Open
mushanyoung wants to merge 1 commit intocloudflare:mainfrom
mushanyoung:fix/r2-mounted-blob-head-metadata
Open

fix(r2): return source digest/size for mounted blob HEAD responses#121
mushanyoung wants to merge 1 commit intocloudflare:mainfrom
mushanyoung:fix/r2-mounted-blob-head-metadata

Conversation

@mushanyoung
Copy link
Copy Markdown

Title

fix(r2): return source digest/size for mounted blob HEAD responses

Summary

Mounted layers in the R2 backend are stored as lightweight "symlink" objects (body like <repo>/blobs/<digest>).
HEAD /v2/:name/blobs/:digest could return metadata from that symlink object instead of the mounted source blob.

This caused invalid blob metadata in HEAD responses for mounted layers, including:

  • incorrect Docker-Content-Digest
  • very small Content-Length values (commonly around path-string length such as 86)

Those headers are consumed by OCI clients during push/pull validation and can surface as unauthorized/manifest-corruption style failures in downstream workflows.

Root Cause

Two pieces combined into the bug:

  1. mountExistingLayer stores a symlink object whose checksum represents the symlink payload bytes (the path string), not the mounted source blob digest.
  2. The blob HEAD route read R2 object metadata directly (env.REGISTRY.head(...)), bypassing symlink resolution logic.

Result: for mounted blobs, HEAD returned symlink metadata instead of source blob metadata.

Changes

1) Persist source metadata on new symlink objects

File: src/registry/r2.ts

  • Added custom metadata keys:
    • X-Serverless-Registry-Symlink-Digest
    • X-Serverless-Registry-Symlink-Size
  • When mounting, write these fields from the source layer response.

2) Resolve symlinks in layerExists (including old symlinks)

File: src/registry/r2.ts

  • Detect symlink objects by:
    • explicit symlink metadata, or
    • checksum mismatch between requested digest and stored object checksum.
  • Fast path (new symlinks): use stored digest/size metadata.
  • Backward compatibility (old symlinks): read symlink body, parse <repo>/blobs/<digest>, then recursively query layerExists on the source target.
  • Added safety fallback for malformed payloads and self-reference.

3) Route blob HEAD through layerExists

File: src/router.ts

  • Updated HEAD /v2/:name+/blobs/:tag to use env.REGISTRY_CLIENT.layerExists(name, tag) for local lookups.
  • Keeps existing remote-registry fallback behavior when local blob does not exist.

4) Add regression assertions

File: test/index.test.ts

  • Extended recursive layer mounting test to assert mounted blob HEAD returns:
    • Docker-Content-Digest == layer digest
    • Content-Length == source blob byte length

Reproduction (before this patch)

  1. Push image to repoA.
  2. Push another image to repoB that mounts the same layer from repoA.
  3. Run HEAD /v2/repoB/blobs/<mounted-layer-digest>.
  4. Observe mismatch:
    • digest/header can reflect symlink object checksum
    • Content-Length can be tiny (e.g. path-length values such as 86)

Behavior After Patch

For mounted blobs, HEAD now returns source blob metadata (digest + size), including for previously-created symlink objects.

Compatibility and Risk

  • Backward compatible with existing symlink objects already present in R2.
  • New symlinks gain metadata for O(1) HEAD resolution.
  • Main behavior change is correctness of blob HEAD metadata for mounted layers.

Test Plan

Executed:

  • npm test -- test/index.test.ts -t "Upload manifests with recursive layer mounting"
  • npm test -- test/index.test.ts

Both pass.

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