# REPRO-2026-00248: containerd CRI checkpoint import RCE ## Summary Status: published Severity: critical Type: security Confidence: high ## Identifiers REPRO ID: REPRO-2026-00248 CVE: CVE-2026-50195 ## Package Name: Unknown Ecosystem: Unknown Affected: Unknown Fixed: Unknown ## Root Cause # Root Cause Analysis Report ## Summary CVE-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`. ## Impact - **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`. - **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. - **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. ## Impact Parity - **Disclosed/claimed maximum impact:** Code execution via CRI checkpoint import API by causing the host to pull and run an attacker image. - **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`. - **Parity:** `full`. - **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. ## Root Cause The 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: 1. Reads `config.RootfsImageRef` from the checkpoint metadata. 2. Calls `c.client.GetImage(ctx, config.RootfsImageRef)` and, if missing, `c.client.Pull(ctx, config.RootfsImageRef)`, which pulls the attacker-selected image reference. 3. Parses `config.RootfsImageName` as a valid tag. 4. Reads the pulled image service record for `config.RootfsImageRef`, rewrites `tagImage.Name = config.RootfsImageName`, and calls `c.client.ImageService().Create(ctx, tagImage)`. That 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. The 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. ## Reproduction Steps 1. Run `bundle/repro/reproduction_steps.sh` from the bundle root (the script is self-contained and uses `PRUVA_ROOT` if set). 2. The script: - 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`. - Downloads official containerd v2.2.2 and v2.2.5 release binaries, plus `runc` and `crictl`. - Builds two privileged runner containers that run real containerd CRI services. - 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`. - For v2.2.5, repeats the same flow as a fixed negative control. 3. Expected evidence: - 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`. - 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. ## Evidence Primary proof artifacts: - `bundle/logs/exploit_evidence.txt` — concise vulnerable/fixed verdict summary. - `bundle/logs/vulnerable_v222_exploit_evidence.txt` — full vulnerable CRI flow and code execution marker. - `bundle/logs/fixed_v225_exploit_evidence.txt` — fixed negative control. - `bundle/logs/vulnerable_v222_containerd.log` and `bundle/logs/fixed_v225_containerd.log` — containerd service logs. - `bundle/logs/vulnerable_v222_ctr_images.txt` and `bundle/logs/fixed_v225_ctr_images.txt` — image store evidence. - `bundle/repro/runtime_manifest.json` — runtime manifest with `entrypoint_kind=api_remote`, service/health/target true, and artifact list. Key excerpts from the successful vulnerable run: ```text containerd: containerd github.com/containerd/containerd/v2 v2.2.2 301b2dac98f15c27117da5c8af12118a041a31d9 checkpoint config.dump: {"rootfsImageRef":"localhost:5000/attacker:latest","rootfsImageName":"localhost:5000/victim:stable", ...} POST-CHECKPOINT: attacker image entries = 1 POST-CHECKPOINT: poisoned victim tag entries = 1 localhost:5000/attacker:latest ... sha256:25ff2e0f0b209d91423507e05f4a369614a1014f46b6bceea502fd24fe7d289f localhost:5000/victim:stable ... sha256:25ff2e0f0b209d91423507e05f4a369614a1014f46b6bceea502fd24fe7d289f StartContainer rc=0 PRUVA_CODE_EXEC_FROM_ATTACKER_IMAGE uid=0 gid=0 image=attacker VERDICT: VULN_CODE_EXEC_CONFIRMED ``` Key excerpts from the fixed negative control: ```text containerd: containerd github.com/containerd/containerd/v2 v2.2.5 e53c7c1516c3b2bff98eb76f1f4117477e6f4e66 POST-CHECKPOINT: attacker image entries = 1 POST-CHECKPOINT: poisoned victim tag entries = 0 creating 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 VERDICT: FIXED_BLOCKED - fixed containerd did not create the victim tag and no attacker code executed ``` Environment details captured: - Official containerd v2.2.2 and v2.2.5 Linux amd64 release binaries are downloaded by the script. - `crictl` drives the real CRI gRPC endpoint at `/run/containerd/containerd.sock` inside the privileged runner. - The runtime manifest confirms `service_started=true`, `healthcheck_passed=true`, and `target_path_reached=true`. ## Recommendations / Next Steps - 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. - Preserve the fixed behavior that prevents checkpoint metadata from creating arbitrary local image tags for pulled base images. - 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. - Consider additional validation of checkpoint metadata provenance and consistency between image name/reference fields before any pull or restore action. ## Additional Notes - The final `bundle/repro/reproduction_steps.sh` was executed successfully twice consecutively in this run. - 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. - 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. ## Reproduction Details Reproduced: 2026-07-06T08:37:00.360Z Duration: 2363 seconds Tool calls: 315 Turns: Unknown Handoffs: 3 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00248 pruva-verify CVE-2026-50195 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00248&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00248/artifacts/bundle/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-50195 ## Artifacts - bundle/repro/reproduction_steps.sh (reproduction_script, 22799 bytes) - bundle/repro/rca_report.md (analysis, 8785 bytes) - bundle/vuln_variant/reproduction_steps.sh (reproduction_script, 22327 bytes) - bundle/vuln_variant/rca_report.md (analysis, 10378 bytes) - bundle/artifact_promotion_manifest.json (other, 11632 bytes) - bundle/vuln_variant/root_cause_equivalence.json (other, 1627 bytes) - bundle/repro/runtime_manifest.json (other, 1435 bytes) - bundle/repro/validation_verdict.json (other, 821 bytes) - bundle/logs/exploit_evidence.txt (other, 752 bytes) - bundle/logs/containerd.log (log, 16801 bytes) - bundle/logs/attacker_marker.txt (other, 136 bytes) - bundle/logs/crictl_ps.txt (other, 374 bytes) - bundle/logs/vulnerable_v222_exploit_evidence.txt (other, 5150 bytes) - bundle/logs/fixed_v225_exploit_evidence.txt (other, 4262 bytes) - bundle/logs/vulnerable_v222_exploit_stdout.log (log, 5020 bytes) - bundle/logs/vulnerable_v222_containerd.log (log, 18165 bytes) - bundle/logs/vulnerable_v222_ctr_images.txt (other, 1704 bytes) - bundle/logs/vulnerable_v222_crictl_ps.txt (other, 575 bytes) - bundle/logs/vulnerable_v222_podlogs.txt (other, 145 bytes) - bundle/logs/fixed_v225_exploit_stdout.log (log, 4132 bytes) - bundle/logs/fixed_v225_containerd.log (log, 17388 bytes) - bundle/logs/fixed_v225_ctr_images.txt (other, 1420 bytes) - bundle/logs/fixed_v225_crictl_ps.txt (other, 374 bytes) - bundle/logs/source/patch_check.txt (other, 3531 bytes) - bundle/logs/reproduction_steps.log (log, 9108 bytes) - bundle/logs/vuln_variant/digest_variant_summary.txt (other, 1017 bytes) - bundle/logs/vuln_variant/vulnerable_v222_digest_variant_evidence.txt (other, 5131 bytes) - bundle/logs/vuln_variant/fixed_v225_digest_variant_evidence.txt (other, 4564 bytes) - bundle/vuln_variant/variant_manifest.json (other, 4144 bytes) - bundle/vuln_variant/validation_verdict.json (other, 3063 bytes) - bundle/vuln_variant/patch_analysis.md (documentation, 6717 bytes) - bundle/vuln_variant/runtime_manifest.json (other, 966 bytes) - bundle/logs/vuln_variant/reproduction_steps.log (log, 8747 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00248 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00248/artifacts/bundle/repro/reproduction_steps.sh - Web: https://pruva.dev/r/REPRO-2026-00248 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://pruva.dev