test: handed tmp_path and staged workspaces to slot uid in Linux root tests
This commit is contained in:
@@ -41,6 +41,40 @@ def _ensure_dashboard_bundle() -> None:
|
|||||||
reset_index_cache()
|
reset_index_cache()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _open_tmp_path_for_slot_traversal(tmp_path: Path) -> None:
|
||||||
|
"""Grant traverse (`+x`) on tmp_path's root-owned ancestors so slot
|
||||||
|
subprocesses can reach the workspace.
|
||||||
|
|
||||||
|
pytest's default ``tmp_path`` lives under ``/tmp/pytest-of-<user>/`` with
|
||||||
|
mode ``0700``. On macOS dev that's irrelevant (no slot subprocess ever
|
||||||
|
drops uid). On Linux+root the slot UID (e.g. 2001) is non-zero and
|
||||||
|
every directory between ``/`` and the workspace needs at least the
|
||||||
|
`o+x` bit or the slot's stat fails with EACCES. Adds `o+x` (NOT `o+r`)
|
||||||
|
so directory contents stay private; only path-traversal is allowed.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import stat
|
||||||
|
|
||||||
|
if platform.system() != "Linux" or os.geteuid() != 0:
|
||||||
|
return
|
||||||
|
cursor = tmp_path.resolve()
|
||||||
|
while cursor != cursor.parent:
|
||||||
|
try:
|
||||||
|
st = cursor.stat()
|
||||||
|
except FileNotFoundError:
|
||||||
|
break
|
||||||
|
if not stat.S_ISDIR(st.st_mode):
|
||||||
|
break
|
||||||
|
if not (st.st_mode & 0o001):
|
||||||
|
try:
|
||||||
|
cursor.chmod(st.st_mode | 0o001)
|
||||||
|
except PermissionError:
|
||||||
|
break
|
||||||
|
cursor = cursor.parent
|
||||||
|
|
||||||
def _baseline_env(tmp_path: Path) -> dict[str, str]:
|
def _baseline_env(tmp_path: Path) -> dict[str, str]:
|
||||||
return {
|
return {
|
||||||
# Orchestrator-mode: no PAT in this container; talk to gh-proxy instead.
|
# Orchestrator-mode: no PAT in this container; talk to gh-proxy instead.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import platform
|
||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
@@ -762,6 +763,13 @@ async def test_git_push_passes_slot_uid_to_git_push(
|
|||||||
|
|
||||||
branch = "farm/abc/slot"
|
branch = "farm/abc/slot"
|
||||||
repo_dir, head = _stage_workspace(proxy_settings, upstream_repo, "octo/widget", 1, branch)
|
repo_dir, head = _stage_workspace(proxy_settings, upstream_repo, "octo/widget", 1, branch)
|
||||||
|
# The push handler reads the origin URL as the slot uid. On Linux+root
|
||||||
|
# the staged workspace is root-owned; hand it to slot 2001 so the
|
||||||
|
# subprocess can stat it. On macOS dev this is a no-op (slot identity
|
||||||
|
# is never activated).
|
||||||
|
if platform.system() == "Linux" and os.geteuid() == 0:
|
||||||
|
for path in [repo_dir.parent, repo_dir, *repo_dir.rglob("*")]:
|
||||||
|
os.chown(path, 2001, 2001, follow_symlinks=False)
|
||||||
captured: dict[str, object] = {}
|
captured: dict[str, object] = {}
|
||||||
|
|
||||||
def fake_git_push(path: Path, **kwargs: object) -> PushResult:
|
def fake_git_push(path: Path, **kwargs: object) -> PushResult:
|
||||||
|
|||||||
+31
-5
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import platform
|
||||||
import signal
|
import signal
|
||||||
import stat
|
import stat
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -148,6 +149,13 @@ def test_rename_workspace_branch_refreshes_shared_metadata(tmp_path: Path, monke
|
|||||||
repo_full_name="octo/widget",
|
repo_full_name="octo/widget",
|
||||||
issue_number=1,
|
issue_number=1,
|
||||||
)
|
)
|
||||||
|
# On Linux+root the rename runs `git branch -m` as the slot uid (2004),
|
||||||
|
# so the worktree needs to be readable by that uid before the call.
|
||||||
|
# On macOS dev `_slot_permissions_active` returns False and this
|
||||||
|
# whole block is a no-op.
|
||||||
|
if platform.system() == "Linux" and os.geteuid() == 0:
|
||||||
|
for path in [root, repo_dir, *repo_dir.rglob("*")]:
|
||||||
|
os.chown(path, 2004, 2004, follow_symlinks=False)
|
||||||
calls: list[tuple[Path, int | None]] = []
|
calls: list[tuple[Path, int | None]] = []
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
"robomp.sandbox._share_git_metadata_with_slots",
|
"robomp.sandbox._share_git_metadata_with_slots",
|
||||||
@@ -688,12 +696,20 @@ def test_ensure_workspace_refreshes_permissions_for_retry_slot_and_session(
|
|||||||
) -> None:
|
) -> None:
|
||||||
chowns: list[tuple[Path, int | None]] = []
|
chowns: list[tuple[Path, int | None]] = []
|
||||||
shared: list[tuple[Path, int | None]] = []
|
shared: list[tuple[Path, int | None]] = []
|
||||||
|
real_chown = _chown_workspace
|
||||||
|
real_share = _share_git_metadata_with_slots
|
||||||
|
|
||||||
monkeypatch.setattr("robomp.sandbox._chown_workspace", lambda root, slot_uid: chowns.append((root, slot_uid)))
|
def record_chown(root: Path, slot_uid: int | None) -> None:
|
||||||
monkeypatch.setattr(
|
chowns.append((root, slot_uid))
|
||||||
"robomp.sandbox._share_git_metadata_with_slots",
|
# Delegate so subsequent slot-identity git ops can stat the tree.
|
||||||
lambda repo_dir, slot_uid: shared.append((repo_dir, slot_uid)),
|
real_chown(root, slot_uid)
|
||||||
)
|
|
||||||
|
def record_share(repo_dir: Path, slot_uid: int | None) -> None:
|
||||||
|
shared.append((repo_dir, slot_uid))
|
||||||
|
real_share(repo_dir, slot_uid)
|
||||||
|
|
||||||
|
monkeypatch.setattr("robomp.sandbox._chown_workspace", record_chown)
|
||||||
|
monkeypatch.setattr("robomp.sandbox._share_git_metadata_with_slots", record_share)
|
||||||
|
|
||||||
mgr = SandboxManager(tmp_path / "workspaces")
|
mgr = SandboxManager(tmp_path / "workspaces")
|
||||||
ws1 = mgr.ensure_workspace(
|
ws1 = mgr.ensure_workspace(
|
||||||
@@ -826,9 +842,14 @@ def test_ensure_workspace_invokes_slot_chown(
|
|||||||
tmp_path: Path, upstream_repo: Path, monkeypatch: pytest.MonkeyPatch
|
tmp_path: Path, upstream_repo: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
) -> None:
|
) -> None:
|
||||||
calls: list[tuple[Path, int | None]] = []
|
calls: list[tuple[Path, int | None]] = []
|
||||||
|
real_chown = _chown_workspace
|
||||||
|
|
||||||
def record_chown(ws_root: Path, slot_uid: int | None) -> None:
|
def record_chown(ws_root: Path, slot_uid: int | None) -> None:
|
||||||
calls.append((ws_root, slot_uid))
|
calls.append((ws_root, slot_uid))
|
||||||
|
# Delegate to the real chown so the subsequent `git config` as the
|
||||||
|
# slot can stat the tree. On macOS dev (uid != 0) the real chown is
|
||||||
|
# itself a no-op; on Linux+root in CI it hands the tree to the slot.
|
||||||
|
real_chown(ws_root, slot_uid)
|
||||||
|
|
||||||
monkeypatch.setattr("robomp.sandbox._chown_workspace", record_chown)
|
monkeypatch.setattr("robomp.sandbox._chown_workspace", record_chown)
|
||||||
mgr = SandboxManager(tmp_path / "workspaces")
|
mgr = SandboxManager(tmp_path / "workspaces")
|
||||||
@@ -852,6 +873,7 @@ def test_ensure_workspace_provisions_and_slot_owns_runtime_dirs(
|
|||||||
) -> None:
|
) -> None:
|
||||||
owned: dict[Path, tuple[int, int]] = {}
|
owned: dict[Path, tuple[int, int]] = {}
|
||||||
runtime_paths: list[Path] = []
|
runtime_paths: list[Path] = []
|
||||||
|
real_chown = _chown_workspace
|
||||||
|
|
||||||
def record_chown(ws_root: Path, slot_uid: int | None) -> None:
|
def record_chown(ws_root: Path, slot_uid: int | None) -> None:
|
||||||
assert slot_uid is not None
|
assert slot_uid is not None
|
||||||
@@ -869,6 +891,10 @@ def test_ensure_workspace_provisions_and_slot_owns_runtime_dirs(
|
|||||||
for path in paths:
|
for path in paths:
|
||||||
assert path.is_dir()
|
assert path.is_dir()
|
||||||
owned[path] = (slot_uid, slot_uid)
|
owned[path] = (slot_uid, slot_uid)
|
||||||
|
# Same rationale as test_ensure_workspace_invokes_slot_chown: hand
|
||||||
|
# the tree to the slot so the subsequent `git config` works under
|
||||||
|
# real slot permissions in CI.
|
||||||
|
real_chown(ws_root, slot_uid)
|
||||||
|
|
||||||
monkeypatch.setattr("robomp.sandbox._chown_workspace", record_chown)
|
monkeypatch.setattr("robomp.sandbox._chown_workspace", record_chown)
|
||||||
mgr = SandboxManager(tmp_path / "workspaces")
|
mgr = SandboxManager(tmp_path / "workspaces")
|
||||||
|
|||||||
Reference in New Issue
Block a user