research /

Containerd CDI Annotation Smuggling Grafts Host Devices Into Pods

Restoring an attacker-authored containerd checkpoint smuggles a host device on a GPU node, mounts, env, and OCI hooks into a pod that requested nothing.

A live Kubernetes pod can ask for no device at all. No GPU in the spec, no quota hit, no device plugin allocation. Then a checkpoint image shows up with an annotation, and containerd treats that checkpoint metadata like the kubelet’s own request.

On a GPU node, that can mean device nodes, library mounts, environment, and OCI hooks landing in a container that never requested them. The pod did not ask for a host device. The checkpoint did. containerd believed the checkpoint.

This is CVE-2026-53492. I found it in the same checkpoint restore pass as the other containerd bugs, using the agent workflow I wrote about separately. This was the corner of the surface nobody else reported.

CVE-2026-53492 · ghsa-33vj-92qq-66hc

CDI assumes the writer is the kubelet

Kubernetes attaches host devices (GPUs as a prime example) through the Container Device Interface. The NVIDIA GPU Operator or a DRA driver drops CDI specs into /etc/cdi and /var/run/cdi; each names a device and the device nodes, library mounts, env, and hooks containerd should splice into a container that wants it. When a pod requests one, the kubelet writes a cdi.k8s.io/ annotation, and containerd’s spec build parses that prefix, resolves the name against the on-node specs, and injects the edits. It trusts the prefix wherever it appears and never asks who set the annotation. That holds as long as only the live kubelet request can write into that annotation map.

That last sentence is the whole vulnerability. CDI is safe enough when the kubelet is the writer. Checkpoint restore quietly swaps in a different writer.

Restore replaces the author

Checkpoint restore breaks that assumption. A CRIU checkpoint ships as an OCI image carrying a status.dump blob — a snapshot of the container at checkpoint time, annotations included. When CreateContainer sees a checkpoint, it hands off to CRImportCheckpoint in container_checkpoint_linux.go, which decodes status.dump and assigns the whole annotation map onto the live config:

Checkpoint restore replaces the live kubelet annotation map with checkpoint status.dump annotations, and containerd's CDI injector turns those values into device requests.

// CRImportCheckpoint, paraphrased
status := decodeStatusDump(checkpointImage)        // entirely attacker-controlled
meta.Config.Annotations = status.GetAnnotations()  // whole map, verbatim
createContainer(ctx, meta, /* restore */ true)     // spec build runs as normal

The annotations the kubelet actually sent are thrown away. restore: true does not disable CDI; it mostly steers status bookkeeping. So the normal spec build still runs, WithCDI still hands annotations to cdi.ParseAnnotations, and any cdi.k8s.io/* key still becomes a device request.

That is the same pattern as the other bugs where trusted and untrusted paths meet in one state object, and the later reader has no memory of which path wrote the value.

The injection happens at create time, upstream of CRIU, so the bug fires even on a node with no criu binary.

Why the severity depends on the node

A smuggled annotation is not magic. It only names a device that already exists in CDI specs on the node. But on a fleet with NVIDIA GPU Operator or DRA specs installed, those are exactly the names containerd is prepared to inject. A tenant who asked for no GPU and consumed no quota can still end up with host device access and the operator-provided hooks that come with it.

That is why I read it as conditional but severe. The bug does not create a device out of thin air; it bypasses the allocation path meant to decide who gets one.

Disclosure

Reported through containerd’s private advisory flow, published Jun 18 2026 with patches available.

One of four containerd CVEs from the same research pass and one of three rooted in CRImportCheckpoint re-trusting checkpoint metadata before CRIU runs alongside the RootfsImageName tag-poisoning and the container.log symlink-read issue. The rig and method are written up in Thinking Outside the Containerd.


← all research