March 20, 2024·5 min read·engineeringdockervolumesstorage

Docker Engine 26 mounts selected paths inside named volumes

Engine 26 adds volume-subpath mounts with explicit handling for symlinks and directory replacement races. The implementation explains why selecting a directory needs more than string validation.

Mounting one directory inside a named volume sounds like a pathname convenience. The difficult part is ensuring that the directory checked by the daemon remains the object mounted into the container. Another process can change a path between validation and use, and a symlink can direct resolution outside the intended volume. A useful implementation has to preserve the relationship between the volume and the selected object throughout mounting.

Docker Engine 26.0.0, released on March 20, adds a Subpath field to volume options and a corresponding CLI option. This makes part of a volume directly mountable at a container destination. The implementation's handling of filesystem references is the more interesting part of the change.

I prefer Docker configuration that makes the relationship between the artifact and its environment visible. Selecting a volume and a relative path in the mount declaration expresses that relationship directly. It also puts responsibility for resolving the selected path in the engine, where the actual mount occurs.

The volume name and selected path remain separate

The CLI change in PR #4331 adds volume-subpath to the long mount syntax. Its parser assigns the supplied value to the volume options' Subpath field. The volume source continues to identify the volume, while the new option identifies a path inside it.

For example, after a volume contains a directory called reports, the selected directory can be mounted as follows:

docker run --rm \
  --mount type=volume,src=shared-data,dst=/data,volume-subpath=reports \
  alpine ls /data

The example assumes that shared-data already contains reports. The CLI change includes a test for a nonexistent subpath and expects failure. It does not interpret the selected name as a request to create a directory during this mount operation.

Separating source identity from the selected path avoids making a slash in a volume name carry another meaning. The declaration has one field for the managed volume and another for resolution within it. A reader can identify the intended root without reconstructing how the client split a compound string.

The same CLI test covers both directory and file mounts. It populates a volume, then selects a subdirectory for a directory listing and a file for reading. That establishes a more precise capability than the common description of mounting a subdirectory. The selected object can also be an individual file.

Relative paths constrain the request

The tests reject an absolute path with an error explaining that the subpath must be relative and inside the volume. An absolute filesystem path would make the volume root ambiguous. The new field instead describes a selection relative to an already identified storage object.

This is a constraint on the request, but it cannot by itself prove confinement. A relative name can still include a symlink whose target lies elsewhere. Even a name that resolved safely during an initial check can be replaced before mounting.

Paweł Gronowski's engine implementation in PR #45687 explicitly supports symlinks while preventing them from escaping the base volume directory. Its description also identifies protection against time-of-check to time-of-use attacks on Linux and Windows. Those requirements explain why the feature involves a safe-path implementation alongside the API field.

A simplistic check that a string begins with the volume's path would establish very little about the object eventually used. Filesystem resolution applies to each component, and components can change. The implementation therefore has to constrain resolution and preserve the selected result until the mount is established.

Linux mounts through an opened reference

The Linux design opens subpath components relative to the base volume directory and checks them against escape. The implementation description identifies openat for walking those components. It then mounts the final descriptor through /proc/self/fd/ at a temporary mountpoint owned by the daemon.

That temporary mountpoint becomes the source passed to the container runtime. The engine removes it after container startup. The selected filesystem object is therefore represented by an opened reference and a daemon-controlled mount during the handoff, rather than by a user-controlled pathname that the runtime must resolve again.

The distinction matters because checking a name and subsequently reopening the name are separate operations. If another process replaces the directory between them, the second resolution can select a different object. Retaining the opened reference keeps the handoff tied to the result that passed the confinement checks.

Windows uses another mechanism to preserve the same property. The PR description says it locks path components before checking them and releases those locks once mounting has completed. Preventing replacement during that interval keeps the checked path associated with the mounted source.

The two implementations share a requirement without pretending that their operating systems expose identical filesystem operations. This is the part of the feature I find most useful to study. A small configuration addition can require substantial platform-specific work to preserve an ordinary user's expectation about which files become visible.

Tests describe the client and daemon contract

The CLI integration test skips daemons with an API older than 1.45. That condition makes the feature's dependency explicit. Recognizing a new option locally is not enough when the server receiving the request cannot provide its behavior.

The successful directory case checks the contents visible at the destination. The successful file case reads the selected file. Failure cases cover an absolute path and a missing selection. Together, these cases establish both useful behavior and limits without treating a successfully parsed command as proof of a working mount.

The engine PR also names its integration coverage for volume subdirectories. Its central security requirement remains that supported symlinks cannot escape the volume directory and that concurrent replacement cannot alter the mount source after checking. Those properties are what make the option suitable for selecting less than the full volume.

The new selection does not create another independently managed volume. It chooses an object within the existing one and mounts that object at the requested destination. Consequently, the configuration still needs a clear storage layout inside the source volume. The mount declaration now records the part of that layout a container consumes, and the engine preserves that selection during the transition into the container.