import os import shlex import subprocess import sys from pathlib import Path import pytest HELPER_SOURCE = ( Path(__file__).resolve().parents[2] / "deploy" / "scripts" / "libexec" / "left4me-overlay" ) def _setup_instance(root: Path, name: str = "alpha", lowerdirs: list[str] | None = None) -> None: """Create the on-disk shape the helper expects.""" (root / "installation").mkdir(parents=True, exist_ok=True) (root / "overlays" / "workshop").mkdir(parents=True, exist_ok=True) if lowerdirs is None: lowerdirs = [str(root / "overlays" / "workshop"), str(root / "installation")] inst_dir = root / "instances" / name inst_dir.mkdir(parents=True) (inst_dir / "instance.env").write_text( f"L4D2_PORT=27015\nL4D2_ARGS=-tickrate 100\nL4D2_LOWERDIRS={':'.join(lowerdirs)}\n" ) runtime = root / "runtime" / name (runtime / "upper").mkdir(parents=True) (runtime / "work").mkdir(parents=True) (runtime / "merged").mkdir(parents=True) def _run(args: list[str], root: Path, extra_env: dict[str, str] | None = None) -> subprocess.CompletedProcess: env = { **os.environ, "LEFT4ME_ROOT": str(root), "LEFT4ME_OVERLAY_PRINT_ONLY": "1", } if extra_env: env.update(extra_env) return subprocess.run( [sys.executable, str(HELPER_SOURCE), *args], env=env, capture_output=True, text=True, ) def test_mount_prints_expected_command(tmp_path: Path) -> None: """The helper invokes /bin/mount directly. nsenter into PID 1's mount namespace happens at the systemd Exec line (see the unit file), so by the time the helper runs, the syscall already lands in the right namespace. """ _setup_instance(tmp_path) result = _run(["mount", "alpha"], tmp_path) assert result.returncode == 0, result.stderr parts = shlex.split(result.stdout.strip()) assert parts[0] == "/bin/mount" assert parts[1:3] == ["-t", "overlay"] assert parts[3] == "overlay" assert parts[4] == "-o" options = parts[5] assert f"upperdir={tmp_path}/runtime/alpha/upper" in options assert f"workdir={tmp_path}/runtime/alpha/work" in options assert f"lowerdir={tmp_path}/overlays/workshop:{tmp_path}/installation" in options assert parts[6] == str(tmp_path / "runtime" / "alpha" / "merged") def test_umount_prints_expected_command(tmp_path: Path) -> None: """Same as the mount path: helper invokes /bin/umount directly, relying on the unit-level nsenter to put it in PID 1's namespace. """ _setup_instance(tmp_path) result = _run(["umount", "alpha"], tmp_path) assert result.returncode == 0, result.stderr parts = shlex.split(result.stdout.strip()) assert parts == [ "/bin/umount", str(tmp_path / "runtime" / "alpha" / "merged"), ] @pytest.mark.parametrize("bad_name", ["..", "../escape", "FOO", "foo bar", "foo/bar", ""]) def test_rejects_bad_instance_name(tmp_path: Path, bad_name: str) -> None: result = _run(["mount", bad_name], tmp_path) assert result.returncode != 0 assert "invalid instance name" in result.stderr or "usage:" in result.stderr def test_rejects_lowerdir_outside_allowlist(tmp_path: Path) -> None: _setup_instance(tmp_path, lowerdirs=["/etc"]) result = _run(["mount", "alpha"], tmp_path) assert result.returncode != 0 assert "outside the permitted roots" in result.stderr def test_rejects_lowerdir_traversal(tmp_path: Path) -> None: # An overlay subdirectory whose path uses .. to escape the overlays root. _setup_instance(tmp_path, lowerdirs=[str(tmp_path / "overlays" / "..") + "/etc"]) result = _run(["mount", "alpha"], tmp_path) assert result.returncode != 0 assert "outside the permitted roots" in result.stderr or "path does not exist" in result.stderr def test_rejects_lowerdir_symlink_escape(tmp_path: Path) -> None: _setup_instance(tmp_path) sneaky = tmp_path / "overlays" / "sneaky" os.symlink("/etc", sneaky) # rewrite instance.env to point at the symlink inst_env = tmp_path / "instances" / "alpha" / "instance.env" inst_env.write_text(f"L4D2_LOWERDIRS={sneaky}\n") result = _run(["mount", "alpha"], tmp_path) assert result.returncode != 0 assert "outside the permitted roots" in result.stderr def test_rejects_missing_instance_env(tmp_path: Path) -> None: (tmp_path / "instances" / "alpha").mkdir(parents=True) runtime = tmp_path / "runtime" / "alpha" (runtime / "upper").mkdir(parents=True) (runtime / "work").mkdir(parents=True) (runtime / "merged").mkdir(parents=True) result = _run(["mount", "alpha"], tmp_path) assert result.returncode != 0 assert "instance.env not found" in result.stderr def test_rejects_lowerdir_count_over_cap(tmp_path: Path) -> None: (tmp_path / "installation").mkdir() many = [str(tmp_path / "installation")] * 501 _setup_instance(tmp_path, lowerdirs=many) result = _run(["mount", "alpha"], tmp_path) assert result.returncode != 0 assert "501 entries" in result.stderr def test_rejects_empty_lowerdir_entry(tmp_path: Path) -> None: (tmp_path / "installation").mkdir() _setup_instance( tmp_path, lowerdirs=[str(tmp_path / "installation"), "", str(tmp_path / "installation")], ) result = _run(["mount", "alpha"], tmp_path) assert result.returncode != 0 assert "empty entry" in result.stderr @pytest.mark.skipif(sys.platform != "linux", reason="user.* xattrs are Linux-only") def test_rejects_upperdir_with_fuseoverlayfs_xattr(tmp_path: Path) -> None: _setup_instance(tmp_path) tainted = tmp_path / "runtime" / "alpha" / "upper" / "deleted-thing" tainted.write_bytes(b"") try: os.setxattr(tainted, "user.fuseoverlayfs.opaque", b"y") except OSError: pytest.skip("filesystem doesn't support user.* xattrs") result = _run(["mount", "alpha"], tmp_path) assert result.returncode != 0 assert "fuse-overlayfs xattr" in result.stderr