Skip to content

Commit 75877e5

Browse files
seedspiritclaude
andauthored
fix(BA-4096): avoid nested bind mount of DO_NOT_STORE_PERSISTENT_FILES_HERE.md (#10944)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 4f38276 commit 75877e5

9 files changed

Lines changed: 143 additions & 12 deletions

File tree

changes/10944.fix.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix session creation failure on macOS with Docker Desktop VirtioFS caused by nested bind mount of `DO_NOT_STORE_PERSISTENT_FILES_HERE.md` by copying the file into the scratch work directory instead.

src/ai/backend/agent/agent.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -653,9 +653,6 @@ def mount_static_binary(
653653
jail_path = None
654654

655655
dotfile_extractor_path = self.resolve_krunner_filepath("runner/extract_dotfiles.py")
656-
persistent_files_warning_doc_path = self.resolve_krunner_filepath(
657-
"runner/DO_NOT_STORE_PERSISTENT_FILES_HERE.md"
658-
)
659656
entrypoint_sh_path = self.resolve_krunner_filepath("runner/entrypoint.sh")
660657

661658
fantompass_path = self.resolve_krunner_filepath("runner/fantompass.py")
@@ -673,11 +670,6 @@ def mount_static_binary(
673670
_mount(MountTypes.BIND, words_json_path, "/opt/kernel/words.json")
674671
if jail_path is not None:
675672
_mount(MountTypes.BIND, jail_path, "/opt/kernel/jail")
676-
_mount(
677-
MountTypes.BIND,
678-
persistent_files_warning_doc_path,
679-
"/home/work/DO_NOT_STORE_PERSISTENT_FILES_HERE.md",
680-
)
681673

682674
_mount(MountTypes.VOLUME, krunner_volume, "/opt/backend.ai")
683675
pylib_path = f"/opt/backend.ai/lib/python{krunner_pyver}/site-packages/"

src/ai/backend/agent/docker/agent.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,11 @@ def _clone_dotfiles() -> None:
463463
zshrc_path = Path(str(files("ai.backend.runner").joinpath(".zshrc")))
464464
vimrc_path = Path(str(files("ai.backend.runner").joinpath(".vimrc")))
465465
tmux_conf_path = Path(str(files("ai.backend.runner").joinpath(".tmux.conf")))
466+
persistent_files_warning_doc_path = Path(
467+
str(
468+
files("ai.backend.runner").joinpath("DO_NOT_STORE_PERSISTENT_FILES_HERE.md")
469+
)
470+
)
466471
jupyter_custom_dir = self.work_dir / ".jupyter" / "custom"
467472
jupyter_custom_dir.mkdir(parents=True, exist_ok=True)
468473
shutil.copy(jupyter_custom_css_path.resolve(), jupyter_custom_dir / "custom.css")
@@ -474,6 +479,10 @@ def _clone_dotfiles() -> None:
474479
shutil.copy(zshrc_path.resolve(), self.work_dir / ".zshrc")
475480
shutil.copy(vimrc_path.resolve(), self.work_dir / ".vimrc")
476481
shutil.copy(tmux_conf_path.resolve(), self.work_dir / ".tmux.conf")
482+
shutil.copy(
483+
persistent_files_warning_doc_path.resolve(),
484+
self.work_dir / "DO_NOT_STORE_PERSISTENT_FILES_HERE.md",
485+
)
477486

478487
def chown_scratch(uid: int | None, gid: int | None) -> None:
479488
paths = [
@@ -485,6 +494,7 @@ def chown_scratch(uid: int | None, gid: int | None) -> None:
485494
self.work_dir / ".zshrc",
486495
self.work_dir / ".vimrc",
487496
self.work_dir / ".tmux.conf",
497+
self.work_dir / "DO_NOT_STORE_PERSISTENT_FILES_HERE.md",
488498
]
489499
self._chown_paths_if_root(paths, uid, gid)
490500

src/ai/backend/agent/stage/kernel_lifecycle/docker/mount/krunner.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,6 @@ def _prepare_default_mounts(self) -> list[Mount]:
117117
self._parse_mount("runner/fantompass.py", "/opt/kernel/fantompass.py"),
118118
self._parse_mount("runner/hash_phrase.py", "/opt/kernel/hash_phrase.py"),
119119
self._parse_mount("runner/words.json", "/opt/kernel/words.json"),
120-
self._parse_mount(
121-
"runner/DO_NOT_STORE_PERSISTENT_FILES_HERE.md",
122-
"/home/work/DO_NOT_STORE_PERSISTENT_FILES_HERE.md",
123-
),
124120
]
125121

126122
def _prepare_musl_mounts(self, runner_info: KernelRunnerInfo) -> list[Mount]:

src/ai/backend/agent/stage/kernel_lifecycle/docker/scratch.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,9 @@ def _clone_func(self, spec: ScratchSpec) -> None:
195195
zshrc_path = Path(str(files("ai.backend.runner").joinpath(".zshrc")))
196196
vimrc_path = Path(str(files("ai.backend.runner").joinpath(".vimrc")))
197197
tmux_conf_path = Path(str(files("ai.backend.runner").joinpath(".tmux.conf")))
198+
persistent_files_warning_doc_path = Path(
199+
str(files("ai.backend.runner").joinpath("DO_NOT_STORE_PERSISTENT_FILES_HERE.md"))
200+
)
198201
jupyter_custom_dir = work_dir / ".jupyter" / "custom"
199202
jupyter_custom_dir.mkdir(parents=True, exist_ok=True)
200203
shutil.copy(jupyter_custom_css_path.resolve(), jupyter_custom_dir / "custom.css")
@@ -206,6 +209,10 @@ def _clone_func(self, spec: ScratchSpec) -> None:
206209
shutil.copy(zshrc_path.resolve(), work_dir / ".zshrc")
207210
shutil.copy(vimrc_path.resolve(), work_dir / ".vimrc")
208211
shutil.copy(tmux_conf_path.resolve(), work_dir / ".tmux.conf")
212+
shutil.copy(
213+
persistent_files_warning_doc_path.resolve(),
214+
work_dir / "DO_NOT_STORE_PERSISTENT_FILES_HERE.md",
215+
)
209216

210217
paths = [
211218
work_dir,
@@ -216,6 +223,7 @@ def _clone_func(self, spec: ScratchSpec) -> None:
216223
work_dir / ".zshrc",
217224
work_dir / ".vimrc",
218225
work_dir / ".tmux.conf",
226+
work_dir / "DO_NOT_STORE_PERSISTENT_FILES_HERE.md",
219227
]
220228
self._chown_paths_if_root(paths, spec.container_config)
221229

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
python_tests()

tests/unit/agent/stage/kernel_lifecycle/docker/mount/__init__.py

Whitespace-only changes.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""Unit tests for KernelRunnerMountProvisioner mount list (BA-4096 regression)."""
2+
3+
from __future__ import annotations
4+
5+
import pytest
6+
7+
from ai.backend.agent.stage.kernel_lifecycle.docker.mount.krunner import (
8+
KernelRunnerMountProvisioner,
9+
)
10+
from ai.backend.common.types import MountPermission
11+
12+
13+
class TestKernelRunnerMountProvisionerDefaultMounts:
14+
@pytest.fixture
15+
def provisioner(self) -> KernelRunnerMountProvisioner:
16+
return KernelRunnerMountProvisioner()
17+
18+
def test_default_mounts_do_not_include_warning_doc(
19+
self,
20+
provisioner: KernelRunnerMountProvisioner,
21+
) -> None:
22+
"""Regression: DO_NOT_STORE_PERSISTENT_FILES_HERE.md must NOT appear
23+
as a bind mount — it is now copied via ScratchProvisioner."""
24+
mounts = provisioner._prepare_default_mounts()
25+
mount_targets = [str(m.target) for m in mounts]
26+
assert "/home/work/DO_NOT_STORE_PERSISTENT_FILES_HERE.md" not in mount_targets
27+
28+
def test_default_mounts_include_expected_entries(
29+
self,
30+
provisioner: KernelRunnerMountProvisioner,
31+
) -> None:
32+
mounts = provisioner._prepare_default_mounts()
33+
mount_targets = [str(m.target) for m in mounts]
34+
assert "/opt/kernel/entrypoint.sh" in mount_targets
35+
assert "/opt/kernel/extract_dotfiles.py" in mount_targets
36+
assert "/opt/kernel/fantompass.py" in mount_targets
37+
assert "/opt/kernel/hash_phrase.py" in mount_targets
38+
assert "/opt/kernel/words.json" in mount_targets
39+
40+
def test_default_mounts_are_read_only(
41+
self,
42+
provisioner: KernelRunnerMountProvisioner,
43+
) -> None:
44+
mounts = provisioner._prepare_default_mounts()
45+
for mount in mounts:
46+
assert mount.permission == MountPermission.READ_ONLY
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
"""Unit tests for ScratchProvisioner warning doc cloning (BA-4096 regression)."""
2+
3+
from __future__ import annotations
4+
5+
from pathlib import Path
6+
from unittest.mock import patch
7+
from uuid import uuid4
8+
9+
import pytest
10+
11+
from ai.backend.agent.stage.kernel_lifecycle.docker.scratch import (
12+
ContainerOwnershipConfig,
13+
ScratchProvisioner,
14+
ScratchSpec,
15+
)
16+
from ai.backend.agent.stage.kernel_lifecycle.docker.utils import ScratchUtil
17+
from ai.backend.common.types import BinarySize, KernelId
18+
19+
_SCRATCH_OS = "ai.backend.agent.stage.kernel_lifecycle.docker.scratch.os"
20+
21+
22+
class TestScratchProvisionerCloneFunc:
23+
@pytest.fixture
24+
def provisioner(self) -> ScratchProvisioner:
25+
return ScratchProvisioner()
26+
27+
@pytest.fixture
28+
def scratch_spec(self, tmp_path: Path) -> ScratchSpec:
29+
kernel_id = KernelId(uuid4())
30+
scratch_root = tmp_path / "scratches"
31+
scratch_root.mkdir()
32+
work_dir = ScratchUtil.work_dir(scratch_root, kernel_id)
33+
work_dir.mkdir(parents=True)
34+
return ScratchSpec(
35+
kernel_id=kernel_id,
36+
container_config=ContainerOwnershipConfig(
37+
kernel_uid=1000,
38+
kernel_gid=1000,
39+
supplementary_gids=set(),
40+
fallback_kernel_uid=1000,
41+
fallback_kernel_gid=1000,
42+
kernel_features=frozenset(),
43+
),
44+
scratch_type="hostdir",
45+
scratch_root=scratch_root,
46+
scratch_size=BinarySize(1024),
47+
)
48+
49+
def test_clone_copies_warning_doc(
50+
self,
51+
provisioner: ScratchProvisioner,
52+
scratch_spec: ScratchSpec,
53+
) -> None:
54+
"""Regression: DO_NOT_STORE_PERSISTENT_FILES_HERE.md must be copied
55+
into work_dir instead of being bind-mounted separately."""
56+
provisioner._clone_func(scratch_spec)
57+
work_dir = ScratchUtil.work_dir(scratch_spec.scratch_root, scratch_spec.kernel_id)
58+
59+
copied = work_dir / "DO_NOT_STORE_PERSISTENT_FILES_HERE.md"
60+
assert copied.exists()
61+
assert copied.stat().st_size > 0
62+
63+
def test_chown_includes_warning_doc(
64+
self,
65+
provisioner: ScratchProvisioner,
66+
scratch_spec: ScratchSpec,
67+
) -> None:
68+
"""When running as root, the warning doc must be chowned like dotfiles."""
69+
with (
70+
patch(f"{_SCRATCH_OS}.geteuid", return_value=0),
71+
patch(f"{_SCRATCH_OS}.chown") as mock_chown,
72+
):
73+
provisioner._clone_func(scratch_spec)
74+
75+
chowned_paths = {Path(call.args[0]) for call in mock_chown.call_args_list}
76+
work_dir = ScratchUtil.work_dir(scratch_spec.scratch_root, scratch_spec.kernel_id)
77+
assert work_dir / "DO_NOT_STORE_PERSISTENT_FILES_HERE.md" in chowned_paths

0 commit comments

Comments
 (0)