Docker container escapes through leaked runtime file descriptors
January's runc and BuildKit fixes address different ways that container execution and image building can reach beyond their intended permissions.
A container runtime can enter the right mount namespace and still start a process with access to the host filesystem. The runc vulnerability disclosed today depends on a reference that survives that transition. An internal file descriptor points at a host directory, and a working-directory change can follow it after the runtime has established the container's filesystem. The runtime assumed that entering the namespace made the subsequent directory change safe.
The runc advisory for CVE-2024-21626 explains both the leaked descriptor and the missing verification. Its account is more useful than treating every container escape as a failure of the same mechanism. Here, filesystem isolation depends on which references the runtime retains while constructing the process, as well as which namespace the process joins.
I value Docker's repeatable build files and transferable images. A small final image with a static Go binary remains my preferred packaging approach. This disclosure concerns the software that starts that image and executes its build steps, so reducing the contents of the application image does not resolve it.
A directory reference survives the namespace transition
The affected runc versions include 1.1.11 and earlier. According to its maintainers, internal descriptors leaked into runc init, including a handle to the host's /sys/fs/cgroup. A container configuration could select a working directory through the process's descriptor directory. The particular descriptor number depends on the order in which runc opens files, so the number in an example is not a stable interface.
The attack does not require an obviously suspicious directory in the image configuration. An image can contain a symlink from an ordinary-looking directory to the descriptor reference. Resolving that directory during startup then gives the new process a working directory in the host filesystem. The process can reach files through that retained reference even though startup has already entered the container's mount namespace.
The missing check matters independently of the original leak. Before this fix, runc did not verify that the selected working directory remained inside the container after calling chdir. Its implementation assumed that the earlier pivot_root made reaching an external directory impossible. A descriptor retained from an earlier stage invalidated that assumption.
This is a useful distinction when reviewing isolation code. A pathname is interpreted using a process's filesystem state, but an existing descriptor already identifies an opened object. Establishing a new root does not establish that every reference involved in later setup originated inside that root. The advisory demonstrates that failure with a concrete startup sequence, rather than with a need for an unusually privileged application.
An existing container can participate in the attack
The same underlying problem also affects runc exec. In that case, an attacker who can modify files inside an existing container can replace a directory that an administrative operation will use as its working directory. A later exec operation follows the replacement symlink, and the new process acquires a working directory outside the intended filesystem.
There are conditions on this attack. The advisory says that runc exec defaults to /, which cannot be replaced by this symlink technique. Exploitation of the working-directory variant requires use of --cwd and knowledge of the selected path. Whether replacing that path also prevents the requested executable from resolving depends on the relationship between the directory and executable paths.
Once the new container binary executes, the attacker can use its process working-directory reference to access the host filesystem. The maintainers therefore distinguish this scenario from persuading somebody to run a newly supplied malicious image. An attacker with access to a running container may be able to wait for an otherwise reasonable administrative exec operation.
That distinction changes how I read image-trust advice. Restricting image sources addresses the malicious-image path, but it does not describe every attack in the advisory. A running container in which an attacker can alter executable content or directory paths is also relevant to the runtime's security assumptions.
Host executable replacement changes the impact
The advisory also describes variants that use the leaked reference when selecting the executable itself. A host executable can run as the container process, after which a process executable reference permits replacement of that host binary. Subsequent execution by a privileged host user can give the attacker control of the host.
The maintainers connect this technique to CVE-2019-5736. Both attacks use a process's executable reference to modify a host binary. This defect provides another route to a host executable through descriptors retained during startup. It is therefore insufficient to describe the disclosure only as unintended read access to host files.
The advisory's severity discussion also resists a simple ranking of practical danger. The malicious-image executable-overwrite variant has the highest listed score, but the maintainers consider the existing-container variants particularly concerning. Those variants need less conspicuous interaction than convincing an operator to run a new attacker-supplied image. The prerequisite for each attack is more informative than one score for the entire family.
The patch removes several assumptions
Runc 1.1.12 applies multiple protections. It checks the final working directory using the behavior of os.Getwd when the directory lies outside the current namespace root. The advisory identifies an ENOENT result as the condition that detects this invalid state. That check blocks the directory-based attacks even if another descriptor leak exists.
The patch also closes internal runc descriptors immediately before the final execve. This prevents an executable argument from using a descriptor that should have remained private to runtime setup. The implementation has to account for descriptors used internally by the Go runtime, which makes indiscriminate closure more complicated than simply iterating over every open number.
Separate changes fix the known leaks and mark non-standard descriptors close-on-exec before executing runc init. The cgroup directory handle receives O_CLOEXEC, and the release backports a fix for leaked Go file objects. These protections operate at different stages. Closing the known leak addresses today's trigger, while verifying the directory and limiting inherited references constrain what another leak could do.
The 1.1.12 release is the runtime update identified by the advisory. Updating application packages inside an image cannot apply these changes to the runc executable used by its host. The component being replaced must match the component that performs the vulnerable setup.
BuildKit has separate mount and permission defects
The coordinated disclosure also includes BuildKit vulnerabilities. CVE-2024-23651 involves parallel malicious build steps sharing cache mounts with subpaths. A race can make host files accessible to a build container. The vulnerable operation belongs to the builder's handling of shared mount state, so the trust decision begins when it accepts the Dockerfile.
CVE-2024-23652 concerns cleanup of empty mountpoint files. A malicious frontend or Dockerfile using RUN --mount can cause that cleanup to delete a file outside the container. Cleanup therefore has its own filesystem permissions to enforce after the build operation has finished using the mount.
A further defect, CVE-2024-23653, affects the interactive container API. That API could request elevated privileges without the required entitlement validation. Normally, security.insecure must be enabled in the daemon configuration and allowed by the user submitting the build. The failure bypasses that agreement at a separate execution entry point.
All three advisories identify BuildKit 0.12.5 as the patched release. Their workarounds emphasize trusted frontends and Dockerfiles. A frontend image can be selected by the Dockerfile's syntax directive, which means reviewing only the application source leaves another executable build input unexamined. The builder's permission model includes the program that translates the Dockerfile into build operations.
The update belongs to the builder and runtime
Docker's January 31 advisory coordinates the affected components and their updates. Engine releases 25.0.2 and 24.0.9 are part of that response, with different coverage between the branches. Docker's announcement schedules Desktop 4.27.1 for February 1. Desktop users therefore have a forthcoming packaged update on the date of this disclosure.
The runtime advisory gives narrowly scoped workarounds. Using / as the working directory blocks its directory-based variants, and trusting images addresses the malicious-image variants. It says there is no practical workaround for the existing-container executable-overwrite variant other than avoiding exec operations. Those limits explain why one configuration change cannot substitute for replacing the affected runtime.
For a build system, the corresponding distinction is between the final application artifact and the machinery that creates it. A minimal final stage does not remove earlier build execution, cache mounts, or frontend requests. The patches belong wherever those operations actually run, including a separate builder when the local Docker client submits work elsewhere.