From 78491b83c1e221ae6471fc4e94e957f277585177 Mon Sep 17 00:00:00 2001 From: breken-ai <312387581+breken-ai@users.noreply.github.com> Date: Fri, 25 Sep 2026 18:36:50 -0700 Subject: [PATCH] containers: strip any bind mode from run volumes containers.run()/create() derive the container's volume list from the `volumes` bind strings. Only a trailing ":ro" or ":rw" was removed, so a bind such as "/src:/app:z", "/src:/app:ro,Z" or "/src:/app:cached" added "/app:z" (etc.) to Config.Volumes, and the daemon created an extra anonymous volume mounted at that literal path next to the bind. Drop whatever mode follows the destination, keeping Windows drive letters in the destination intact. Assisted-By: Claude Signed-off-by: breken-ai <312387581+breken-ai@users.noreply.github.com> --- docker/models/containers.py | 8 ++++---- tests/unit/models_containers_test.py | 30 ++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/docker/models/containers.py b/docker/models/containers.py index 9c9e92c90f..56d0081c53 100644 --- a/docker/models/containers.py +++ b/docker/models/containers.py @@ -1187,10 +1187,10 @@ def _host_volume_from_bind(bind): bits = rest.split(':', 1) if len(bits) == 1 or bits[1] in ('ro', 'rw'): return drive + bits[0] - elif bits[1].endswith(':ro') or bits[1].endswith(':rw'): - return bits[1][:-3] - else: - return bits[1] + # bits[1] is "dest" or "dest:mode", where mode can be any bind option + # ("ro", "z", "ro,Z", "rw,rprivate", "cached", ...) + dest_drive, dest = ntpath.splitdrive(bits[1]) + return dest_drive + dest.split(':', 1)[0] ExecResult = namedtuple('ExecResult', 'exit_code,output') diff --git a/tests/unit/models_containers_test.py b/tests/unit/models_containers_test.py index 0e2ae341a9..9c2cc27aa3 100644 --- a/tests/unit/models_containers_test.py +++ b/tests/unit/models_containers_test.py @@ -484,6 +484,36 @@ def test_create(self): ) client.api.inspect_container.assert_called_with(FAKE_CONTAINER_ID) + def test_create_volumes_with_bind_options(self): + # Mode options other than a bare "ro"/"rw" (SELinux labels, + # propagation, macOS consistency, ...) must not leak into the + # container's anonymous volume list. + binds = [ + '/tmp:/mnt/z:z', + '/tmp:/mnt/ro-z:ro,Z', + '/tmp:/mnt/rprivate:rw,rprivate', + 'src:/mnt/cached:cached', + 'volumename:/mnt/nocopy:nocopy', + '/tmp:/mnt/ro:ro', + 'C:\\windows\\path:D:\\hello\\world:ro,Z', + ] + client = make_fake_client() + client.containers.create('alpine', volumes=binds) + client.api.create_container.assert_called_with( + image='alpine', + command=None, + volumes=[ + '/mnt/z', + '/mnt/ro-z', + '/mnt/rprivate', + '/mnt/cached', + '/mnt/nocopy', + '/mnt/ro', + 'D:\\hello\\world', + ], + host_config={'Binds': binds, 'NetworkMode': 'default'}, + ) + def test_create_with_image_object(self): client = make_fake_client() image = client.images.get(FAKE_IMAGE_ID)