Description
When resolving an image by digest, dockerResolver.Resolve() tries HEAD /v2/<repo>/manifests/<digest> first, then falls back to HEAD /v2/<repo>/blobs/<digest> if the manifests endpoint fails. If the manifests request fails due to a transient error (timeout, connection reset, 5xx, etc.), the blobs fallback can silently succeed — but returns Content-Type: application/octet-stream instead of the actual manifest media type.
This wrong media type is cached permanently in containerd's metadata store. The image record becomes poisoned: containerd no longer recognizes the content as a manifest list/index, cannot resolve platforms or child manifests, and never self-heals. The only recovery is manual deletion of the image and content records.
Steps to reproduce the issue
- Set up a CNCF Distribution v3 registry
- Push a multi-platform image (manifest list / OCI index)
- Configure containerd to pull from this registry
- Simulate a transient failure on
HEAD /v2/<repo>/manifests/<digest> (e.g., inject a network timeout or 503 response)
- Trigger a pull of the image by digest
- Observe:
ctr -n k8s.io images ls shows application/octet-stream media type and only the manifest list JSON size (e.g., 513 bytes) instead of the full image size
- The image cannot be used — no platforms are resolved
Describe the results you received and expected
When successfully reproduced, the issue would cause containerd cache pollution that it will indefinitely not be able to run an image.
k8s events
Normal Pulled 67s (x6 over 116s) kubelet spec.containers{my-container}: Container image "my-image:<my-tag>@<my-sha>" already present on machine
Warning Failed 13s (x10 over 116s) kubelet spec.containers{my-container}: Error: failed to create containerd container: error unpacking image: unexpected media type application/octet-stream for <my-sha>: not found
containerd state
ctr -n k8s.io images ls | grep my-image
my-image:<my-tag>@<my-sha> application/octet-stream <my-sha> 513.0 B - io.cri-containerd.image=managed
Expected Behavior
It is debatable if CNCF distribution project should instead return the real Content-Type instead of application/octet-stream at GET/HEAD /blobs endpoint. But containerd should be robust enough to never get into indefinite bad state because of the GET /blobs fallback.
What version of containerd are you using?
2.1.5
Any other relevant information
Suspected root cause
resolver.go Resolve()
if dgst != "" {
paths = append(paths, []string{"manifests", dgst.String()})
// fallback to blobs on not found.
paths = append(paths, []string{"blobs", dgst.String()})
}
The comment even says "fallback to blobs on not found" — but the code actually falls back on any error, not just 404. The outer loop still tries all paths, and a transient failure on /manifests/ still causes the fallback to /blobs/, which returns application/octet-stream.
Issue exists in 2.1.5 but should be reproduciable on main.
Show configuration if it is related to CRI plugin.
No response
Description
When resolving an image by digest,
dockerResolver.Resolve()triesHEAD /v2/<repo>/manifests/<digest>first, then falls back toHEAD /v2/<repo>/blobs/<digest>if the manifests endpoint fails. If the manifests request fails due to a transient error (timeout, connection reset, 5xx, etc.), the blobs fallback can silently succeed — but returnsContent-Type: application/octet-streaminstead of the actual manifest media type.This wrong media type is cached permanently in containerd's metadata store. The image record becomes poisoned: containerd no longer recognizes the content as a manifest list/index, cannot resolve platforms or child manifests, and never self-heals. The only recovery is manual deletion of the image and content records.
Steps to reproduce the issue
HEAD /v2/<repo>/manifests/<digest>(e.g., inject a network timeout or 503 response)ctr -n k8s.io images lsshowsapplication/octet-streammedia type and only the manifest list JSON size (e.g., 513 bytes) instead of the full image sizeDescribe the results you received and expected
When successfully reproduced, the issue would cause containerd cache pollution that it will indefinitely not be able to run an image.
k8s events
containerd state
Expected Behavior
It is debatable if CNCF distribution project should instead return the real
Content-Typeinstead ofapplication/octet-streamatGET/HEAD /blobsendpoint. But containerd should be robust enough to never get into indefinite bad state because of theGET /blobsfallback.What version of containerd are you using?
2.1.5
Any other relevant information
Suspected root cause
resolver.goResolve()The comment even says "fallback to blobs on not found" — but the code actually falls back on any error, not just 404. The outer loop still tries all paths, and a transient failure on
/manifests/still causes the fallback to/blobs/, which returnsapplication/octet-stream.Issue exists in
2.1.5but should be reproduciable onmain.Show configuration if it is related to CRI plugin.
No response