From 62e55f7738922f90c56761072f9c9e4423150484 Mon Sep 17 00:00:00 2001 From: echobt <154886644+echobt@users.noreply.github.com> Date: Wed, 29 Jul 2026 22:14:31 +0000 Subject: [PATCH] fix(master): refuse tmpfs options Swarm cannot express The Swarm backend translated a docker-run tmpfs spec by keeping size= and discarding everything else. The own_runner job container installs the miner agent into /tmp/.local under a read-only rootfs, so its spec carries exec; tmpfs is noexec unless told otherwise. Routing that workload through Swarm therefore produced a noexec /tmp, where the agent loads no shared object and fails with "failed to map segment from shared object" -- a symptom that took hours to trace back to a dropped mount flag. Swarm cannot express the flag at all: against Docker 29.2.1 a bare exec is rejected as a non key=value field and tmpfs-options is an unknown option, so only tmpfs-size and tmpfs-mode survive. Translate those two, accept the flags Swarm already applies by default, and refuse anything else -- exec loudest of all -- rather than dropping it and failing far from the cause. --- src/base/master/swarm_backend.py | 43 ++++++++++++++++++++++++--- tests/unit/test_swarm_tmpfs_exec.py | 46 +++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 4 deletions(-) create mode 100644 tests/unit/test_swarm_tmpfs_exec.py diff --git a/src/base/master/swarm_backend.py b/src/base/master/swarm_backend.py index 3c84923f8..a7bde47bd 100644 --- a/src/base/master/swarm_backend.py +++ b/src/base/master/swarm_backend.py @@ -1606,15 +1606,50 @@ def _readonly_mount_arg(source: str, target: str) -> str: return f"type={mount_type},source={source},destination={target},readonly" +#: tmpfs options Docker Swarm already applies by default, so honouring them +#: needs no translation. ``noexec`` belongs here because tmpfs is noexec unless +#: told otherwise -- which is exactly why ``exec`` below cannot be dropped. +_SWARM_TMPFS_DEFAULT_OPTIONS = frozenset({"rw", "noexec", "nosuid", "nodev"}) + + def _tmpfs_mount_arg(value: str) -> str: + """Translate a ``docker run`` tmpfs spec into a Swarm ``--mount`` argument. + + Swarm's ``--mount type=tmpfs`` expresses only ``tmpfs-size`` and + ``tmpfs-mode`` (verified against Docker 29.2.1: a bare ``exec`` is rejected + as a non key=value field, and ``tmpfs-options`` is an unknown option). Any + option that cannot be translated is refused rather than dropped: a silently + discarded ``exec`` yields a noexec ``/tmp``, so anything installed there + loads no shared object and the failure surfaces far from its cause. + """ + path, separator, options = value.partition(":") if not path.startswith("/"): raise DockerOrchestrationError(f"tmpfs mount must be absolute: {path}") arg = f"type=tmpfs,destination={path}" - if separator: - for option in options.split(","): - if option.startswith("size="): - arg += f",tmpfs-size={option.removeprefix('size=')}" + if not separator: + return arg + for raw_option in options.split(","): + option = raw_option.strip() + if not option or option in _SWARM_TMPFS_DEFAULT_OPTIONS: + continue + if option.startswith("size="): + arg += f",tmpfs-size={option.removeprefix('size=')}" + elif option.startswith("mode="): + arg += f",tmpfs-mode={option.removeprefix('mode=')}" + elif option == "exec": + raise DockerOrchestrationError( + f"tmpfs mount {path} requests exec, which Docker Swarm cannot " + "express: --mount type=tmpfs supports only tmpfs-size and " + "tmpfs-mode. Dropping the flag would mount the path noexec and " + "break loading anything installed there. Run this workload on " + "the broker backend instead." + ) + else: + raise DockerOrchestrationError( + f"tmpfs mount {path} carries option {option!r} that Docker " + "Swarm cannot express; refusing rather than dropping it." + ) return arg diff --git a/tests/unit/test_swarm_tmpfs_exec.py b/tests/unit/test_swarm_tmpfs_exec.py new file mode 100644 index 000000000..d6a9f27c0 --- /dev/null +++ b/tests/unit/test_swarm_tmpfs_exec.py @@ -0,0 +1,46 @@ +"""Swarm tmpfs translation must never silently drop execution semantics. + +The own_runner job container installs the miner agent into ``/tmp/.local`` under +a read-only rootfs, so its tmpfs spec carries ``exec``. Docker Swarm's +``--mount type=tmpfs`` cannot express that flag (verified against Docker +29.2.1: ``exec`` is rejected as a non key=value field and ``tmpfs-options`` is +an unknown option), and tmpfs defaults to ``noexec``. Dropping the flag on the +way through therefore produces a mount that loads no shared object -- the exact +production failure that cost hours to diagnose. Refuse instead. +""" + +from __future__ import annotations + +import pytest + +from base.master.docker_orchestrator import DockerOrchestrationError +from base.master.swarm_backend import _tmpfs_mount_arg + + +def test_exec_is_refused_rather_than_silently_dropped() -> None: + with pytest.raises(DockerOrchestrationError) as excinfo: + _tmpfs_mount_arg("/tmp:rw,nosuid,exec,size=2g") + message = str(excinfo.value) + assert "exec" in message + assert "/tmp" in message + + +def test_default_noexec_spec_still_translates() -> None: + arg = _tmpfs_mount_arg("/tmp:rw,noexec,nosuid,size=512m") + assert "type=tmpfs" in arg + assert "destination=/tmp" in arg + assert "tmpfs-size=512m" in arg + + +def test_spec_without_size_translates() -> None: + assert _tmpfs_mount_arg("/tmp:rw,nosuid,nodev") == "type=tmpfs,destination=/tmp" + + +def test_unknown_option_is_refused_rather_than_dropped() -> None: + with pytest.raises(DockerOrchestrationError): + _tmpfs_mount_arg("/tmp:rw,totally-unknown-option") + + +def test_relative_path_still_refused() -> None: + with pytest.raises(DockerOrchestrationError): + _tmpfs_mount_arg("tmp:size=1m")