CVE-2026-94603 lets a container image switch off the sandbox you asked Podman to apply. From Podman 4.4.0 until the fixes on 29 September 2026, podman run treated any image carrying the annotation io.podman.annotations.checkpoint.runtime.name as a saved container checkpoint, restored it, and silently ignored your command-line restrictions. --cap-drop=ALL did nothing, and the image's own checkpoint state chose the capabilities and sandbox settings. The fix in Podman 5.8.8 and 6.1.3 removes the feature. Rootless Podman is not affected.
This post is for anyone who runs Podman as root: servers, CI runners, system Quadlet units, or a rootful Podman machine on a laptop. It explains how the bug works, who is exposed, how to check your version, and a script that checks images for the annotation before you run them.
The short answer
If you run rootful Podman between 4.4.0 and 5.8.7, or 6.0.0 to 6.1.2, upgrade to 5.8.8, 6.1.3 or later. Until you can, don't run images you don't trust, and reject any image whose manifest has an annotation starting with io.podman.annotations.checkpoint.. Podman's own advisory gives that as the workaround and notes that Podman can't do the check for you. The issue was found by ADA Logics using Anthropic's Claude models.
| CVE | CVE-2026-94603, GHSA-2cvf-wqm6-wr9g |
| Affected | Podman 4.4.0 to before 5.8.8, and 6.0.0 to before 6.1.3, rootful only |
| Fixed | 5.8.8 and 6.1.3 (both released 29 September 2026); CVE record published 13 October |
| Severity | CNA score CVSS 3.1 10.0; Red Hat rates it Important, CVSS 8.6 |
| Credit | ADA Logics, using Anthropic's Claude models |
How a container image became a checkpoint
Podman can checkpoint a running container with CRIU, saving its memory, its state and its full configuration, and restore it later. Podman 4.4.0, released on 1 February 2023, added a way to distribute those checkpoints through an OCI registry and run them with podman run. To recognise them, podman run looked for the checkpoint runtime annotation in the image's manifest. If it was there, Podman called the restore path instead of creating a new container.
The problem is that a checkpoint isn't an image. A checkpoint contains the entire container configuration, including its capabilities, security options and sandbox settings, and restoring it recreates that container exactly. The advisory says that on detecting a checkpoint, podman run "will silently ignore all user input regarding how the container should be created". Its example is podman run --cap-drop=ALL, which on a checkpoint image keeps whatever capability set the checkpoint specifies, "which could include all capabilities".
So the image author, not the person running the image, decided how it would be sandboxed. Anyone who could get you to run an image, through a public registry, a compromised base image, a typo in an image name or a CI job that pulls from a shared namespace, could ship one that runs with far more privilege than your command line asked for.
The check itself was a few lines. In the code removed by the fix, ContainerRun inspected the image and looked for the annotation:
_, isCheckpointImage := imgData.Annotations[define.CheckpointAnnotationRuntimeName]
if isCheckpointImage {
// ... ic.ContainerRestore(ctx, []string{resolvedImageName}, restoreOptions)
Nothing else about the request was considered. An ordinary podman run with explicit hardening flags went down the restore path because of one key in a manifest the image author controls.
The fix: removing the feature
The maintainers didn't try to make checkpoint images safe inside podman run. They reverted the 2022 commit that added the feature, in the main, v6.1 and v5.8 branches. The revert's commit message says checkpoints "must be explicitly requested by the user to ensure they are used safely", and the release notes for 5.8.8 and 6.1.3 list the removal as a breaking change.
If you relied on podman run <checkpoint-image> to move containers between hosts, that workflow is gone. Restoring a checkpoint is now something you ask for explicitly with podman container restore, which is where the decision belongs: the person restoring a checkpoint is choosing to trust its configuration.
Who is exposed
- Rootful Podman only. The advisory and the CVE record both say rootless containers aren't affected, because checkpoint and restore aren't available in that mode. That covers most developer laptops, since
podman machineVMs are rootless unless created with--rootful. - Servers and CI. Anything that runs
sudo podman run, system-wide Quadlet units under/etc/containers/systemd, and CI runners using rootful Podman are in scope if they run images they didn't build. - Red Hat products. Red Hat's CVE page lists Podman in RHEL 8, 9 and 10, OpenShift Container Platform 4, Podman Desktop and several other products as affected, with no released fix for RHEL at the time of writing. Red Hat has shipped fixes for its Hardened Images and Quay 3.12.
Red Hat's score is lower than the CNA's for a fair reason: exploitation needs someone to run the attacker's image. Red Hat's statement says environments that run only vetted images from trusted registries "remain at reduced operational risk". That's true, and it's also why this belongs in a supply-chain review: the attack works through exactly the images you pull.
Check your version and your distribution
$ podman --version
Upstream fixed versions are 5.8.8 and 6.1.3. Distribution packages lag, and on 14 October 2026 the status was mixed:
| Distribution | Status on 14 October 2026 |
|---|---|
| Fedora 43 to 46 | new podman builds (5.8.9, 6.1.4) in progress in Bodhi; check the package changelog for the revert |
| Debian 13 (trixie), 14 (forky) | vulnerable; fixed in unstable |
| Ubuntu 26.04 LTS | listed as affected, no fix yet |
| RHEL 8, 9, 10 | affected, no erratum yet |
Check your own distribution's tracker before relying on this table, and remember that a distribution may backport the revert without changing the upstream version number.
Check images before you run them
The annotation lives in the image's OCI manifest. Podman reads it from the manifest's annotations field (in containers/common, libimage sets Annotations from the OCI manifest), so you can check it without pulling or running anything. This script reads manifests straight from a registry, or from a local OCI layout, follows multi-architecture indexes, and exits non-zero if any manifest carries a checkpoint annotation. It uses only the standard library and anonymous registry tokens, so it works on public images as is.
#!/usr/bin/env python3
"""Flag container images that Podman 4.4.0 to 5.8.7 / 6.0.0 to 6.1.2 would treat as checkpoints
(CVE-2026-94603): any manifest annotation starting with io.podman.annotations.checkpoint.
Reads manifests only, nothing is pulled or run. Standard library only.
Usage: python3 find_checkpoint_images.py registry/repo:tag ... | oci:/path/to/layout"""
import json, os, re, sys, urllib.error, urllib.request
PREFIX = "io.podman.annotations.checkpoint."
ACCEPT = ", ".join([
"application/vnd.oci.image.index.v1+json",
"application/vnd.oci.image.manifest.v1+json",
"application/vnd.docker.distribution.manifest.list.v2+json",
"application/vnd.docker.distribution.manifest.v2+json",
])
def parse(ref):
name, at, digest = ref.partition("@")
tag = "latest"
if not at and ":" in name.rsplit("/", 1)[-1]:
name, tag = name.rsplit(":", 1)
first = name.split("/")[0]
if "/" not in name or ("." not in first and ":" not in first and first != "localhost"):
name = "docker.io/" + (name if "/" in name else "library/" + name)
host, repo = name.split("/", 1)
return ("registry-1.docker.io" if host == "docker.io" else host), repo, (digest if at else tag)
def registry_fetch(host, repo, reference, token=None):
req = urllib.request.Request(f"https://{host}/v2/{repo}/manifests/{reference}", headers={"Accept": ACCEPT})
if token:
req.add_header("Authorization", f"Bearer {token}")
try:
with urllib.request.urlopen(req, timeout=30) as r:
return json.load(r), token
except urllib.error.HTTPError as e:
challenge = e.headers.get("WWW-Authenticate", "")
if e.code != 401 or token or not challenge.startswith("Bearer"):
raise
params = dict(re.findall(r'(\w+)="([^"]*)"', challenge))
url = f"{params['realm']}?service={params.get('service', '')}&scope=repository:{repo}:pull"
with urllib.request.urlopen(url, timeout=30) as r:
body = json.load(r)
return registry_fetch(host, repo, reference, body.get("token") or body.get("access_token"))
def manifests(ref):
"""Yield (where, manifest) for an index and every manifest it points to."""
if ref.startswith("oci:"):
layout = ref[4:]
blob = lambda d: json.load(open(os.path.join(layout, "blobs", *d.split(":"))))
index = json.load(open(os.path.join(layout, "index.json")))
yield "index.json", index
for m in index.get("manifests", []):
yield m["digest"][:19], blob(m["digest"])
return
host, repo, reference = parse(ref)
top, token = registry_fetch(host, repo, reference)
yield reference, top
for m in top.get("manifests", []):
sub, token = registry_fetch(host, repo, m["digest"], token)
yield m["digest"][:19], sub
flagged = 0
for ref in sys.argv[1:]:
hits = []
for where, m in manifests(ref):
hits += [f"{where}: {k}={v}" for k, v in (m.get("annotations") or {}).items() if k.startswith(PREFIX)]
print(("CHECKPOINT " if hits else "ok ") + ref)
for h in hits:
print(" " + h)
flagged += bool(hits)
sys.exit(1 if flagged else 0)
We ran it against three public images and a local OCI layout. We made the layout by exporting alpine:3.22 with skopeo copy and adding the two checkpoint annotations to its manifest:
$ python3 find_checkpoint_images.py alpine:3.22 quay.io/podman/stable:latest ghcr.io/home-assistant/home-assistant:stable oci:./podman-ckpt
ok alpine:3.22
ok quay.io/podman/stable:latest
ok ghcr.io/home-assistant/home-assistant:stable
CHECKPOINT oci:./podman-ckpt
sha256:c20292a540ff: io.podman.annotations.checkpoint.runtime.name=crun
sha256:c20292a540ff: io.podman.annotations.checkpoint.name=demo
$ echo $?
1
A check like this belongs where images enter your environment: in the CI job before podman run, in an admission step for images pulled from outside, or as a policy on your internal registry. Once Podman is patched the annotation is harmless, but scanning for it also tells you whether anyone has been publishing checkpoint images into the registries you use.
The design lesson
This bug wasn't a parsing mistake or a memory error. It was a design decision that let data from the image decide how the image was executed. Container runtimes draw a line between what the image provides (files, a default command, metadata) and what the operator decides (privileges, mounts, the sandbox). The checkpoint feature crossed it, because a checkpoint is a different kind of object that happens to use the same packaging.
The same question is worth asking of any tool that runs artifacts it pulls from somewhere else, whether that's a container runtime, a package manager's install hooks or a CI system that reads configuration from the repository it builds. Which fields in the artifact can change how it runs, and would the person running it notice? Podman's answer, more than three and a half years after the feature shipped, was to remove the path entirely. That's the right call when the safe version of a feature needs the user to opt in anyway.
The fair view
The practical risk is narrower than a 10.0 score suggests. Rootless Podman, which is the default for many users, isn't affected, and an attacker needs you to run their image as root. Teams that build their own images and pull only from registries they control are in a much better position than ones running arbitrary images in CI. For background on how rootless and rootful modes differ, see our Podman and Docker security comparison.
But "run an image someone else built" is what container platforms are for, and --cap-drop=ALL is exactly the kind of flag people add when they don't fully trust an image. A bug that quietly ignores that flag deserves a prompt upgrade.