This bug exemplifies why I look for state transitions and not just dangerous calls in code reviews. There is no direct escape or one-line sink that screams “critical.” Instead just one tenant writing a name into a global image map and another tenant implicitly trusting that name later.
An attacker with permissions to create pods can use a crafted checkpoint image to force containerd to pull a malicious image and assign it an arbitrary local tag. The next pod on that node requesting that same tag (think busybox:latest for example) with IfNotPresent runs the attacker image instead of the requested container and under its own service account, secrets, environment, and volumes.
I found this combing the CRI checkpoint import path with the same agent rig behind the four containerd CVEs and reported it independently. Other teams hit the same gadget around the same time, which is part of the convergent checkpoint bug story.
CVE-2026-50195 · ghsa-cvxm-645q-p574
The cache is a trust boundary
containerd maps human image names to digests in a node-wide image store, shared by every Kubernetes namespace through containerd’s single k8s.io namespace with no per-tenant partition of names. For any non-:latest tag the default pull policy is IfNotPresent. Kubelet asks whether containerd already holds an image by that name, and if so runs it without contacting the registry. Whoever writes a name into that table decides what every later IfNotPresent reference to it runs.
That is fine when the table is populated by pulls from the registry the workload named. It is not fine when an unrelated tenant can mint an entry.
The checkpoint names both sides
A checkpoint ships as an OCI image whose layers carry a CRIU process dump and config.dump runc metadata describing the original container. To restore, kubelet runs the checkpoint image and CRImportCheckpoint unpacks it.
Two config.dump fields matter here with rootfsImageRef, the digest containerd fetches, and rootfsImageName, the human tag containerd creates in the image store pointing at that digest. Both come from the checkpoint artifact. The only check on the name is that it parses as a reference.
// internal/cri/server/container_checkpoint_linux.go — CRImportCheckpoint, paraphrased
img, _ := c.LocalOrPullImage(ctx, config.RootfsImageRef) // fetch the digest the checkpoint names
if _, err := reference.Parse(config.RootfsImageName); err != nil {
return err // only a *syntax* check on the name
}
tagImage := images.Image{Name: config.RootfsImageName, Target: img.Target()}
c.client.ImageService().Create(ctx, tagImage) // live tag in the shared k8s.io store
The tag is created before the restored process starts so it lands even on a node with no criu binary where restore is guaranteed to fail. Existing tags aren’t overwritten (Create returns AlreadyExists) so this targets tags absent on the node which is exactly the set IfNotPresent would otherwise pull.
The entire bug in a nutshell is write now, read later, across different tenants sharing node state. Two pods never need to interact, and an attacker can piggyback on default behavior around the most popular images in the Kubernetes ecosystem.
Why it matters
A tenant who can run checkpoint images gets code execution in a victim’s context. The process is the attacker’s digest but the surrounding identity is the victim’s pod: service-account token, secrets, env, volumes, and whatever RBAC that workload has. Inherit the environment and identity, then run attacker code.
Disclosure & credit
Reported through containerd’s private advisory flow, published Jun 18 2026 with patches available.
It was one of four containerd findings from the same agent rig — Thinking Outside the Containerd — and two siblings reach the same CRImportCheckpoint path: the CDI annotation smuggling issue and the container.log symlink read.