How CVE-2019-5736 exposes the host runc executable
Docker 18.09.2 patches a runc escape that can overwrite the host runtime. The fix changes the executable exposed during container entry, including the path used by docker exec.
Entering an existing container can cross a security boundary even when the command being run looks harmless. The runc disclosure for CVE-2019-5736 identifies docker exec into a container previously writable by an attacker as one of two ways to trigger a host compromise. Creating a container from an attacker-controlled image is the other. In both cases the vulnerable runtime can expose the host executable to code inside the container.
Docker's 18.09.2 release notes, dated February 11, identify the runc update as a critical security correction. The impact is administrative privilege on the host. This is a host-runtime patch, which matters because replacing an application image does not replace the runtime binary used to enter that image.
I value Docker's image format because it makes application environments portable. This vulnerability concerns the privileged machinery that starts those environments. A precise account needs to follow that machinery across the container boundary, including the case where the container has already been running.
The executable remains a host object
Aleksa Sarai's disclosure credits Adam Iwaniuk and Borys Poplawski with discovering the vulnerability. It describes a malicious container overwriting the host runc binary and obtaining root-level code execution on the host. The required interaction is running a command as root inside the affected container. The command itself does not have to be attacker-controlled.
That last detail changes how to interpret an apparently routine maintenance operation. An operator might enter a compromised container to inspect it with a familiar command. The advisory's prerequisite concerns the state of the container and the runtime's entry operation, so choosing an innocent command does not remove the issue. Previous attacker write access is enough to make an existing container relevant to the advisory.
The upstream correction explains the object at the center of the problem. /proc/self/exe can point to the important host-side container executable. A path reached through procfs can therefore identify a file outside the application image. Restricting the files intentionally placed in the image does not by itself establish that the entering runtime exposes no host executable.
The patch's approach is to make a separate copy of the runtime executable before continuing the namespace-entry operation. Its comment in nsexec says the process must re-execute when it is not already running a cloned binary. This prevents container processes from reaching the host binary through /proc/self/exe. The protection changes the object behind that reference, rather than depending on the command inside the container to behave differently.
The distinction between a pathname and its referenced object is central here. Seeing a procfs path inside a container does not mean the executable it references originated in the container filesystem. The runtime is in the middle of establishing the process environment. The patch addresses that transition while it still controls which executable the entering process uses.
A copied executable with constrained writes
The patch creates the copy using memfd_create where the build supports it. It copies the original executable into the new file and applies seals that prevent writing, growing or shrinking the file. It also seals the seal configuration itself. The resulting object contains executable bytes, but it is separate from the installed host runtime.
Re-execution uses fexecve on the copied executable's file descriptor. The helper reconstructs argument and environment arrays from /proc/self/cmdline and /proc/self/environ. That is necessary because the correction runs during namespace-entry initialization, where the helper cannot simply receive those values through an ordinary main entry point.
The implementation also detects whether it already runs from a sealed copy. This allows re-execution to complete without repeatedly cloning the executable. The check opens /proc/self/exe and examines its seals on the memfd path. Both making the copy and recognizing it are part of the mechanism, because a successful second execution must continue into namespace setup.
An alternative implementation path in the patch uses O_TMPFILE. The code opens an unnamed temporary file, copies the binary, and reopens it read-only before execution. An accompanying comment explains that retaining a writable descriptor would interfere with executing the file. The patch therefore deals with the file's lifetime and access mode as well as its contents.
Sarai's commit message names a cost of the chosen design. The copied executable loses the page-cache sharing that an overlayfs-based approach could provide. The author describes the copy as less complicated. That is a concrete security engineering tradeoff in the published change, with a simpler isolation mechanism taking precedence over sharing the original executable's cached pages.
The patch runs in nsenter so that the change is transparent to the Go code and benefits libcontainer users. This placement matters for a runtime library used by more than one integration. Fixing only a high-level Docker command would leave other ways of reaching the same lower-level operation to solve the problem independently.
Why default policy is an insufficient description
The disclosure says Docker's default AppArmor policy does not block this attack. It also discusses Fedora SELinux behavior, but qualifies that statement by package and process label. The affected moby-engine case runs container processes as container_runtime_t. The disclosure distinguishes that from Fedora's docker package and podman, which use container_t and receive protection against this exploit.
Those details make an unconditional statement about SELinux misleading. The existence of a mandatory-access-control system does not establish the effective policy for a particular runtime package. The relevant question is which domain the container processes actually use and what that policy permits. Sarai's qualification is more useful than reducing the advisory to a claim that one security product either works or fails everywhere.
Correct use of user namespaces does block the vulnerability according to the disclosure, provided host root is not mapped into the container's user namespace. The condition is part of the mitigation. Merely mentioning user namespaces without describing the mapping would omit the reason that the protection applies.
This is also why the root prerequisite needs to be stated carefully. The advisory requires root execution inside the container for the described paths, while its user-namespace qualification concerns how that identity corresponds to host identity. The word root alone does not describe both sides of the boundary. That identity mapping determines whether the runtime operation supplies the authority the exploit needs.
My preference for a minimal final image remains useful for controlling application contents, but it cannot establish the safety of this transition. The vulnerable component belongs to the host runtime stack. Its executable remains part of every affected container entry operation, even when the application image contains only a static service binary.
Existing containers are part of the exposure
The two disclosed trigger paths differ in how the attacker supplies container state. An attacker-controlled image supplies that state when a new container starts. Prior write access supplies it inside an existing container before an operator uses docker exec. Both paths depend on the runtime operation that subsequently executes a command as root.
That means an image provenance policy addresses only part of the described exposure. A container that originally came from a trusted image can still meet the second prerequisite after an attacker gains write access. Conversely, the advisory does not say that every running container immediately overwrites runc without any further interaction. It describes specific entry operations, and their prerequisites should remain visible when discussing urgency.
The disclosure identifies similar consequences outside Docker. Sarai reports a related LXC vulnerability and a co-developed correction. He also notes that systemd-nspawn appears unaffected because it uses a different attachment method. The comparison supports a narrow architectural explanation. The important common behavior is how a runtime enters a container and handles its executable, not the use of the word container in a product description.
Docker 18.09.2 supplies the relevant runc correction for its users. Its release notes also say Ubuntu 14.04 installations on a 3.13 kernel need a supported Ubuntu 4.x kernel. That compatibility requirement belongs in the account of this release because the patched runtime must run on the host that receives it.
The upstream patch makes the executable reference safe before proceeding with namespace entry. The operator-facing consequence is specific. Both new-container creation and execution inside an existing writable container must use the corrected host runtime. Application image contents and host runtime packaging answer different parts of that requirement.