{"repro_id":"REPRO-2026-00248","version":6,"title":"containerd CRI checkpoint import RCE","repro_type":"security","status":"published","severity":"critical","description":"containerd before 2.3.2, 2.2.5, and 2.1.9 fails to validate image references in a CRI checkpoint image's configuration. An attacker can craft a checkpoint image that pulls arbitrary images during import, leading to remote code execution on the host. Reproduce: build containerd from a vulnerable version, create a malicious checkpoint image with an attacker-controlled image reference, and use the CRI checkpoint import API to import it. The host will pull and run the attacker image.","root_cause":"# Root Cause Analysis Report\n\n## Summary\n\nCVE-2026-50195 is a containerd CRI checkpoint import vulnerability in which metadata inside an attacker-supplied checkpoint archive is trusted during `CreateContainer`. In vulnerable containerd v2.2.2, the checkpoint `config.dump` fields `rootfsImageRef` and `rootfsImageName` are used to pull an attacker-controlled image and then create an arbitrary local image tag for it. The reproduction proves the real CRI path by importing a malicious checkpoint archive through the containerd CRI gRPC socket, poisoning `localhost:5000/victim:stable` so it points to `localhost:5000/attacker:latest`, and then creating and starting a normal victim container from the poisoned tag. The victim container runs the attacker image command and prints `PRUVA_CODE_EXEC_FROM_ATTACKER_IMAGE uid=0 gid=0 image=attacker`.\n\n## Impact\n\n- **Package/component affected:** `github.com/containerd/containerd/v2`, specifically the CRI plugin's checkpoint import path in `internal/cri/server/container_checkpoint_linux.go` reached from `CreateContainer`.\n- **Affected versions:** containerd versions before the patched releases 2.3.2, 2.2.5, and 2.1.9. The runtime proof uses vulnerable v2.2.2 and fixed negative control v2.2.5.\n- **Risk level and consequences:** Critical. An attacker who can submit a crafted CRI checkpoint image/archive can poison a node's local image cache by forcing an arbitrary local tag to resolve to attacker-controlled image content. A subsequent pod/container using that local tag with an already-present/no-pull workflow can execute attacker-controlled code under the victim container identity.\n\n## Impact Parity\n\n- **Disclosed/claimed maximum impact:** Code execution via CRI checkpoint import API by causing the host to pull and run an attacker image.\n- **Reproduced impact from this run:** Code execution. The vulnerable run used the real CRI endpoint to import the crafted checkpoint, created the poisoned `victim:stable` tag, then successfully invoked `StartContainer` for a normal victim container. The victim container executed the attacker image command and produced the marker `PRUVA_CODE_EXEC_FROM_ATTACKER_IMAGE uid=0 gid=0 image=attacker`.\n- **Parity:** `full`.\n- **Not demonstrated:** The proof runs in an isolated privileged Docker runner rather than a full Kubernetes cluster/kubelet. It nevertheless exercises the production containerd CRI gRPC service and CRI container lifecycle (`runp`, `create`, `start`) with official containerd release binaries.\n\n## Root Cause\n\nThe vulnerable checkpoint import implementation reads untrusted checkpoint metadata from `config.dump` and uses it as authoritative image identity. In v2.2.2, `CRImportCheckpoint` does the following:\n\n1. Reads `config.RootfsImageRef` from the checkpoint metadata.\n2. Calls `c.client.GetImage(ctx, config.RootfsImageRef)` and, if missing, `c.client.Pull(ctx, config.RootfsImageRef)`, which pulls the attacker-selected image reference.\n3. Parses `config.RootfsImageName` as a valid tag.\n4. Reads the pulled image service record for `config.RootfsImageRef`, rewrites `tagImage.Name = config.RootfsImageName`, and calls `c.client.ImageService().Create(ctx, tagImage)`.\n\nThat final tag creation step lets the checkpoint archive bind attacker image content to an arbitrary local tag. The fixed v2.2.5 code removes the vulnerable retagging block; in the negative control, the attacker image is still pulled for the checkpoint restore path, but the arbitrary `victim:stable` local tag is not created, so the later victim `CreateContainer` fails with `not found` instead of running attacker code.\n\nThe script records source evidence in `bundle/logs/source/patch_check.txt`, including the vulnerable v2.2.2 tag creation block and the fixed v2.2.5 corresponding block where it is absent.\n\n## Reproduction Steps\n\n1. Run `bundle/repro/reproduction_steps.sh` from the bundle root (the script is self-contained and uses `PRUVA_ROOT` if set).\n2. The script:\n   - Starts a local Docker registry and builds/pushes three images: pause, a legitimate `victim:stable`, and an attacker image whose `CMD` prints `PRUVA_CODE_EXEC_FROM_ATTACKER_IMAGE`.\n   - Downloads official containerd v2.2.2 and v2.2.5 release binaries, plus `runc` and `crictl`.\n   - Builds two privileged runner containers that run real containerd CRI services.\n   - For v2.2.2, creates a pod sandbox, imports a malicious checkpoint archive through `crictl create` where `rootfsImageRef=localhost:5000/attacker:latest` and `rootfsImageName=localhost:5000/victim:stable`, then creates and starts a normal victim container from `victim:stable`.\n   - For v2.2.5, repeats the same flow as a fixed negative control.\n3. Expected evidence:\n   - Vulnerable run: `POST-CHECKPOINT: poisoned victim tag entries = 1`, `StartContainer rc=0`, and victim logs contain `PRUVA_CODE_EXEC_FROM_ATTACKER_IMAGE uid=0 gid=0 image=attacker`.\n   - Fixed run: `POST-CHECKPOINT: poisoned victim tag entries = 0`, victim `CreateContainer` fails with `failed to resolve image \"localhost:5000/victim:stable\": not found`, and no attacker marker appears.\n\n## Evidence\n\nPrimary proof artifacts:\n\n- `bundle/logs/exploit_evidence.txt` — concise vulnerable/fixed verdict summary.\n- `bundle/logs/vulnerable_v222_exploit_evidence.txt` — full vulnerable CRI flow and code execution marker.\n- `bundle/logs/fixed_v225_exploit_evidence.txt` — fixed negative control.\n- `bundle/logs/vulnerable_v222_containerd.log` and `bundle/logs/fixed_v225_containerd.log` — containerd service logs.\n- `bundle/logs/vulnerable_v222_ctr_images.txt` and `bundle/logs/fixed_v225_ctr_images.txt` — image store evidence.\n- `bundle/repro/runtime_manifest.json` — runtime manifest with `entrypoint_kind=api_remote`, service/health/target true, and artifact list.\n\nKey excerpts from the successful vulnerable run:\n\n```text\ncontainerd: containerd github.com/containerd/containerd/v2 v2.2.2 301b2dac98f15c27117da5c8af12118a041a31d9\ncheckpoint config.dump: {\"rootfsImageRef\":\"localhost:5000/attacker:latest\",\"rootfsImageName\":\"localhost:5000/victim:stable\", ...}\nPOST-CHECKPOINT: attacker image entries = 1\nPOST-CHECKPOINT: poisoned victim tag entries = 1\nlocalhost:5000/attacker:latest ... sha256:25ff2e0f0b209d91423507e05f4a369614a1014f46b6bceea502fd24fe7d289f\nlocalhost:5000/victim:stable   ... sha256:25ff2e0f0b209d91423507e05f4a369614a1014f46b6bceea502fd24fe7d289f\nStartContainer rc=0\nPRUVA_CODE_EXEC_FROM_ATTACKER_IMAGE uid=0 gid=0 image=attacker\nVERDICT: VULN_CODE_EXEC_CONFIRMED\n```\n\nKey excerpts from the fixed negative control:\n\n```text\ncontainerd: containerd github.com/containerd/containerd/v2 v2.2.5 e53c7c1516c3b2bff98eb76f1f4117477e6f4e66\nPOST-CHECKPOINT: attacker image entries = 1\nPOST-CHECKPOINT: poisoned victim tag entries = 0\ncreating container: rpc error: code = NotFound desc = failed to check if this is a checkpoint image: failed to resolve image \"localhost:5000/victim:stable\": not found\nVERDICT: FIXED_BLOCKED - fixed containerd did not create the victim tag and no attacker code executed\n```\n\nEnvironment details captured:\n\n- Official containerd v2.2.2 and v2.2.5 Linux amd64 release binaries are downloaded by the script.\n- `crictl` drives the real CRI gRPC endpoint at `/run/containerd/containerd.sock` inside the privileged runner.\n- The runtime manifest confirms `service_started=true`, `healthcheck_passed=true`, and `target_path_reached=true`.\n\n## Recommendations / Next Steps\n\n- Upgrade containerd to one of the patched versions: 2.3.2 or later, 2.2.5 or later on the 2.2 branch, or 2.1.9 or later on the 2.1 branch.\n- Preserve the fixed behavior that prevents checkpoint metadata from creating arbitrary local image tags for pulled base images.\n- Add regression tests around CRI checkpoint import where `rootfsImageRef` and `rootfsImageName` intentionally differ. The expected result should be that checkpoint import does not create or overwrite the arbitrary local tag, and a later normal container using that tag cannot resolve to attacker content.\n- Consider additional validation of checkpoint metadata provenance and consistency between image name/reference fields before any pull or restore action.\n\n## Additional Notes\n\n- The final `bundle/repro/reproduction_steps.sh` was executed successfully twice consecutively in this run.\n- The proof uses an isolated Docker network and local registry, so it is repeatable and does not depend on an external attacker registry after initial base-image/tool downloads.\n- The fixed v2.2.5 negative control still pulls the attacker base image for checkpoint restore, but it no longer creates the arbitrary victim tag. This distinction matches the reproduced exploit chain and is why the subsequent victim `CreateContainer`/`StartContainer` cannot execute attacker code on the fixed build.\n","cve_id":"CVE-2026-50195","cwe_id":"CWE-345 (Insufficient Verification of Data Authenticity)","reproduced_at":"2026-07-06T08:37:00.360225+00:00","duration_secs":2363.0,"tool_calls":315,"handoffs":3,"total_cost_usd":10.444727019999998,"agent_costs":{"hypothesis_generator":0.0124644,"judge":0.02627665,"repro":7.074883429999997,"support":0.08978154,"vuln_variant":3.241321000000001},"cost_breakdown":{"hypothesis_generator":{"accounts/fireworks/models/glm-5p2":0.0124644},"judge":{"gpt-5.4-mini":0.02627665},"repro":{"accounts/fireworks/routers/glm-5p2-fast":3.662840429999999,"gpt-5.5":3.412043},"support":{"accounts/fireworks/routers/glm-5p2-fast":0.08978154},"vuln_variant":{"gpt-5.5":3.241321000000001}},"quality":{"confidence":"high","idempotent_verified":false,"community_verifications":0},"environment":{"sandbox_image":"ghcr.io/n3mes1s/pruva-sandbox@sha256:8096b2518d6022e13d68f885c3b8ded6b4fe607098b1a1ccbfb99abc004d1dc1"},"published_at":"2026-07-06T08:37:28.410090+00:00","retracted":false,"artifacts":[{"path":"bundle/repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":22799,"category":"reproduction_script"},{"path":"bundle/repro/rca_report.md","filename":"rca_report.md","size":8785,"category":"analysis"},{"path":"bundle/vuln_variant/reproduction_steps.sh","filename":"reproduction_steps.sh","size":22327,"category":"reproduction_script"},{"path":"bundle/vuln_variant/rca_report.md","filename":"rca_report.md","size":10378,"category":"analysis"},{"path":"bundle/artifact_promotion_manifest.json","filename":"artifact_promotion_manifest.json","size":11632,"category":"other"},{"path":"bundle/vuln_variant/root_cause_equivalence.json","filename":"root_cause_equivalence.json","size":1627,"category":"other"},{"path":"bundle/repro/runtime_manifest.json","filename":"runtime_manifest.json","size":1435,"category":"other"},{"path":"bundle/repro/validation_verdict.json","filename":"validation_verdict.json","size":821,"category":"other"},{"path":"bundle/logs/exploit_evidence.txt","filename":"exploit_evidence.txt","size":752,"category":"other"},{"path":"bundle/logs/containerd.log","filename":"containerd.log","size":16801,"category":"log"},{"path":"bundle/logs/attacker_marker.txt","filename":"attacker_marker.txt","size":136,"category":"other"},{"path":"bundle/logs/crictl_ps.txt","filename":"crictl_ps.txt","size":374,"category":"other"},{"path":"bundle/logs/vulnerable_v222_exploit_evidence.txt","filename":"vulnerable_v222_exploit_evidence.txt","size":5150,"category":"other"},{"path":"bundle/logs/fixed_v225_exploit_evidence.txt","filename":"fixed_v225_exploit_evidence.txt","size":4262,"category":"other"},{"path":"bundle/logs/vulnerable_v222_exploit_stdout.log","filename":"vulnerable_v222_exploit_stdout.log","size":5020,"category":"log"},{"path":"bundle/logs/vulnerable_v222_containerd.log","filename":"vulnerable_v222_containerd.log","size":18165,"category":"log"},{"path":"bundle/logs/vulnerable_v222_ctr_images.txt","filename":"vulnerable_v222_ctr_images.txt","size":1704,"category":"other"},{"path":"bundle/logs/vulnerable_v222_crictl_ps.txt","filename":"vulnerable_v222_crictl_ps.txt","size":575,"category":"other"},{"path":"bundle/logs/vulnerable_v222_podlogs.txt","filename":"vulnerable_v222_podlogs.txt","size":145,"category":"other"},{"path":"bundle/logs/fixed_v225_exploit_stdout.log","filename":"fixed_v225_exploit_stdout.log","size":4132,"category":"log"},{"path":"bundle/logs/fixed_v225_containerd.log","filename":"fixed_v225_containerd.log","size":17388,"category":"log"},{"path":"bundle/logs/fixed_v225_ctr_images.txt","filename":"fixed_v225_ctr_images.txt","size":1420,"category":"other"},{"path":"bundle/logs/fixed_v225_crictl_ps.txt","filename":"fixed_v225_crictl_ps.txt","size":374,"category":"other"},{"path":"bundle/logs/source/patch_check.txt","filename":"patch_check.txt","size":3531,"category":"other"},{"path":"bundle/logs/reproduction_steps.log","filename":"reproduction_steps.log","size":9108,"category":"log"},{"path":"bundle/logs/vuln_variant/digest_variant_summary.txt","filename":"digest_variant_summary.txt","size":1017,"category":"other"},{"path":"bundle/logs/vuln_variant/vulnerable_v222_digest_variant_evidence.txt","filename":"vulnerable_v222_digest_variant_evidence.txt","size":5131,"category":"other"},{"path":"bundle/logs/vuln_variant/fixed_v225_digest_variant_evidence.txt","filename":"fixed_v225_digest_variant_evidence.txt","size":4564,"category":"other"},{"path":"bundle/vuln_variant/variant_manifest.json","filename":"variant_manifest.json","size":4144,"category":"other"},{"path":"bundle/vuln_variant/validation_verdict.json","filename":"validation_verdict.json","size":3063,"category":"other"},{"path":"bundle/vuln_variant/patch_analysis.md","filename":"patch_analysis.md","size":6717,"category":"documentation"},{"path":"bundle/vuln_variant/runtime_manifest.json","filename":"runtime_manifest.json","size":966,"category":"other"},{"path":"bundle/logs/vuln_variant/reproduction_steps.log","filename":"reproduction_steps.log","size":8747,"category":"log"}]}