runc CVE-2019-5736: Containers Are Not a Security Boundary
On Monday the runc maintainers disclosed a container breakout, and the subject line of the announcement didn’t leave much room for hope: it applied to all versions. If you run Docker, containerd, CRI-O, or Kubernetes, you are almost certainly running runc underneath, because it’s the small program that actually sets up and starts Linux containers. The big vendors and cloud providers have all posted advisories since then, so I went through them to pull together what the flaw allows, who is exposed, and what’s worth doing beyond installing the update.
I’m sticking to the runc announcement, Docker’s release notes and announcement, Red Hat’s advisory, the Kubernetes project, and the bulletins from AWS, Google Cloud, and Microsoft Azure.
What the runc maintainers said
The announcement on the oss-security list came from Aleksa Sarai, one of the runc maintainers, and credits Adam Iwaniuk and Borys Popławski with finding the bug. It says the vulnerability lets a malicious container overwrite the host runc binary and gain root-level code execution on the host, with minimal user interaction. Someone has to run a command as root inside a container, and it doesn’t matter whether the attacker controls that command, in one of two situations: creating a new container from an image the attacker controls, or attaching with docker exec to an existing container the attacker previously had write access to.
So the realistic scenarios are an untrusted image you pulled and ran, or a container an attacker already had write access to that somebody later attaches to with docker exec. The CVE is CVE-2019-5736, and the maintainers scored it 7.2 under CVSSv3. NVD’s description traces the root cause to file descriptor mishandling related to /proc/self/exe.
The announcement also says LXC had a similar flaw and pushed a fix co-developed with the runc team, and it goes as far as saying most container runtimes are probably vulnerable unless they took unusual mitigations ahead of time. It also states that the maintainers’ own exploit code will be published publicly seven days after the patch date, which puts it at February 18.
How it was fixed
The fix in runc changes runc so that when it enters a container, it runs from a copy of its own binary instead of exposing the real one on the host. The commit message explains that the copy is made with memfd_create(2), with an O_TMPFILE fallback, and that the tradeoff is losing page cache sharing for the runc binary. The practical takeaway is that the real host binary is no longer exposed to the container.
That copy has side effects you might notice. Google’s bulletin warns that the new runc uses more memory and that containers with very low memory limits, under 16MB, may need more.
Which versions fix it
Docker’s 18.09 release notes list 18.09.2, dated February 11, as updating runc for CVE-2019-5736, and they add that Ubuntu 14.04 users on a 3.13 kernel need to move to a supported 4.x kernel. Docker’s announcement the next day gives the full set of fixed versions: 18.09.2 or 18.06.3 for Docker Engine Community, and 18.09.2, 18.03.1-ee-6, or 17.06.2-ee-19 for Docker Engine Enterprise. It also points Docker Desktop users on Mac and Windows to an update.
If you get Docker from your distribution rather than from Docker, the version numbers are different. Red Hat’s advisory rates the issue Important and says the docker package bundles its own copy of runc, so docker users need to update the docker package, while people using CRI-O, podman, or anything else that depends on runc need to update the runc package. Both come from the RHEL 7 Extras channel. On Red Hat’s side that means RHSA-2019:0304 for docker and RHSA-2019:0303 for runc.
What SELinux, AppArmor, and user namespaces change
This is where the advisories get interesting, because the answer depends on configuration more than product.
The runc announcement says the flaw is not blocked by the default AppArmor policy. It’s also not blocked by the default SELinux policy on Fedora, though that applies only to the moby-engine package, because container processes there run as container_runtime_t. Fedora’s docker package and podman are protected, the announcement says, because they run container processes as container_t. And it says the flaw is blocked by correct use of user namespaces, meaning setups where host root isn’t mapped into the container’s user namespace.
Red Hat goes further for its own platform. Its advisory says SELinux in targeted enforcing mode, which is the RHEL 7 default, completely prevents exploitation, and that OpenShift Online and OpenShift Dedicated aren’t vulnerable for that reason. If you’ve ever set SELinux to permissive to make a container problem go away, this is the week to set it back. Red Hat also notes that with enforcing mode on, blocked attempts show up as AVC denials in the audit log, while warning that you can’t rely on logs from a machine where an attacker already got root.
The Kubernetes project’s post adds the simplest condition of all: the exploit needs UID 0 inside the container. If the process in the container isn’t root, or is something you trust, the vulnerability doesn’t apply.
What the cloud providers said
The AWS bulletin says no customer action is needed except for the services it lists. For Amazon Linux there’s an updated Docker package, and for ECS and EKS there are new optimized AMIs, with AWS recommending that you replace existing container instances and worker nodes with ones launched from the latest AMI. Fargate has an updated platform version 1.3, and AWS says services should be redeployed so tasks move onto it.
Google’s GKE bulletin says only Ubuntu nodes are affected and nodes running Container-Optimized OS are not. To fix Ubuntu nodes you upgrade the master first, then the nodes. It also says containers not running as root are unaffected.
Microsoft posted an Azure update on February 13 saying it built a new version of the Moby container runtime with the fix for AKS. To pick it up you upgrade your cluster, and Microsoft says any upgrade works, since the upgrade replaces existing nodes with new ones running the patched runtime.
What I’d do this week
Patch first. Update Docker to one of the fixed versions, or update your distribution’s docker or runc packages, and if you run containerd or CRI-O directly, make sure the runc they call is the patched one. On AWS, GKE, and AKS, do the node replacement or cluster upgrade that each provider describes, because a notice that a new AMI exists doesn’t patch anything by itself.
Then look at what you actually run. The Kubernetes post calls attacker-controlled images, such as unvetted images from public repositories, the most common source of risk. My advice is to build your own images or vet the ones you use, and pin them by digest so the image you vetted is the image you get.
Stop running containers as root where you can. Docker, Google, and the Kubernetes project all point to it for this bug, and it helps against plenty of others. In a Dockerfile that’s a USER line. In Kubernetes it’s a security context on the pod, and the Kubernetes post suggests enforcing it cluster-wide with a PodSecurityPolicy that requires MustRunAsNonRoot.
spec:
securityContext:
runAsUser: 1000
runAsNonRoot: true
For containers that really do need root inside, look at user namespaces. Docker’s userns-remap documentation describes mapping root in the container to an unprivileged range of UIDs on the host, and setting the value to default has Docker create a dockremap user for you.
{
"userns-remap": "default"
}
It isn’t free. The docs say it’s best enabled on a new installation, because existing images and containers get hidden once it’s on. It’s also incompatible with sharing the host’s PID or network namespace, and --privileged only works if you turn remapping off for that container with --userns=host. Keep SELinux enforcing on hosts that have it, too. Just don’t count on the default AppArmor profile to save you from this one, because the runc announcement says it doesn’t.
Containers were never a wall
The uncomfortable lesson here isn’t really about runc. A container is a set of kernel features wrapped around ordinary processes on a shared host, and the tooling that manages it runs as root on that host. The Kubernetes post puts the stakes plainly: an attacker who gets root on the host has access to the server and every other container on it. One bug in one privileged component, and the thing that looked like a boundary turns out to be a partition.
That doesn’t make containers useless for security. It means they’re one layer among several. If you’re running code you don’t trust next to code you care about, especially on a multi-tenant host where customers or teams share nodes, treat the container as a speed bump. Put non-root users, user namespaces, and mandatory access control underneath it, and separate workloads you really don’t trust onto their own nodes or VMs. And get the patch on before February 18.