# -*- coding: utf-8 -*- # pylint: disable=unused-argument """Unit tests for Linux Landlock sandbox and probe_sandbox_support.""" from __future__ import annotations import sys from unittest.mock import MagicMock, mock_open, patch import pytest from pineagents.sandbox import ( MountSpec, SandboxCapability, SandboxConfig, SandboxMode, create_sandbox, probe_sandbox_support, ) from pineagents.sandbox.config import ( _probe_linux_landlock, _probe_macos_seatbelt, detect_platform_mode, ) # os.uname is Linux/macOS only; skip entirely on Windows. pytestmark = pytest.mark.skipif( sys.platform == "win32", reason=( "Linux sandbox tests require os.uname which is unavailable on Windows" ), ) # ============================================================================ # probe_sandbox_support() — platform routing # ============================================================================ class TestProbeSandboxSupport: """Test probe_sandbox_support() for each platform.""" @pytest.fixture(autouse=True) def _clear_probe_cache(self): """Clear lru_cache so each test starts fresh.""" probe_sandbox_support.cache_clear() yield probe_sandbox_support.cache_clear() @patch("sys.platform", "darwin") @patch("shutil.which", return_value="/usr/bin/sandbox-exec") def test_darwin_delegates_to_seatbelt(self, mock_which): result = probe_sandbox_support() assert result.supported is True assert result.mode == SandboxMode.SEATBELT @patch("sys.platform", "linux") @patch("shutil.which", return_value=None) # bwrap not found @patch("os.uname") def test_linux_delegates_to_landlock(self, mock_uname, mock_which): # bwrap not found → falls through to Landlock → kernel too old mock_uname.return_value = MagicMock(release="4.0.0") result = probe_sandbox_support() # Should go through _probe_linux_landlock and fail on kernel version assert result.mode == SandboxMode.NONE assert "4.0" in result.reason @patch("sys.platform", "win32") @patch("pineagents.sandbox.config._probe_windows") def test_windows_calls_windows_probe(self, mock_probe): mock_probe.return_value = SandboxCapability( supported=False, mode=SandboxMode.NONE, reason="Not running on Windows", ) result = probe_sandbox_support() mock_probe.assert_called_once() assert result.supported is False @patch("sys.platform", "freebsd13") def test_unknown_platform_returns_unsupported(self): result = probe_sandbox_support() assert result.supported is False assert result.mode == SandboxMode.NONE # ============================================================================ # _probe_linux_landlock() — detailed Linux detection # ============================================================================ class TestProbeLinuxLandlock: """Test Linux Landlock probe logic with various kernel/LSM scenarios.""" @patch("os.uname") def test_kernel_too_old(self, mock_uname): mock_uname.return_value = MagicMock(release="5.10.112-generic") result = _probe_linux_landlock() assert result.supported is False assert "5.10" in result.reason assert "< 5.13" in result.reason assert result.mode == SandboxMode.NONE @patch("os.uname") def test_kernel_version_parse_error(self, mock_uname): mock_uname.return_value = MagicMock(release="invalid-kernel") result = _probe_linux_landlock() assert result.supported is False assert "Cannot parse" in result.reason @patch("builtins.open", mock_open(read_data="capability,yama,apparmor")) @patch("os.uname") def test_landlock_not_in_lsm(self, mock_uname): mock_uname.return_value = MagicMock(release="5.15.0-125-generic") result = _probe_linux_landlock() assert result.supported is False assert "not in LSM list" in result.reason @patch("builtins.open", side_effect=OSError("Permission denied")) @patch("os.uname") def test_lsm_file_unreadable(self, mock_uname, mock_file): mock_uname.return_value = MagicMock(release="5.15.0-125-generic") result = _probe_linux_landlock() assert result.supported is False assert "Cannot read" in result.reason @patch("platform.machine", return_value="x86_64") @patch( "builtins.open", mock_open(read_data="lockdown,capability,landlock,yama,apparmor"), ) @patch("os.uname") def test_landlock_supported_abi_v4(self, mock_uname, mock_machine): mock_uname.return_value = MagicMock(release="6.7.0-generic") # Mock ctypes.CDLL to return a fake libc with syscall returning 4 mock_libc = MagicMock() mock_libc.syscall.return_value = 4 # ABI v4 with ( patch("ctypes.CDLL", return_value=mock_libc), patch( "ctypes.util.find_library", return_value="libc.so.6", ), ): result = _probe_linux_landlock() assert result.supported is True assert result.mode == SandboxMode.LANDLOCK assert result.landlock_abi_version == 4 assert "ABI v4" in result.reason @patch("platform.machine", return_value="x86_64") @patch("builtins.open", mock_open(read_data="landlock,capability")) @patch("os.uname") def test_landlock_syscall_fails(self, mock_uname, mock_machine): mock_uname.return_value = MagicMock(release="5.15.0-generic") mock_libc = MagicMock() mock_libc.syscall.return_value = -1 # failure with ( patch("ctypes.CDLL", return_value=mock_libc), patch( "ctypes.util.find_library", return_value="libc.so.6", ), patch("ctypes.get_errno", return_value=38), ): result = _probe_linux_landlock() assert result.supported is False assert "syscall failed" in result.reason # ============================================================================ # _probe_macos_seatbelt() # ============================================================================ class TestProbeMacosSeatbelt: """Test macOS Seatbelt probe.""" @patch("shutil.which", return_value="/usr/bin/sandbox-exec") def test_sandbox_exec_available(self, mock_which): result = _probe_macos_seatbelt() assert result.supported is True assert result.mode == SandboxMode.SEATBELT @patch("shutil.which", return_value=None) def test_sandbox_exec_not_found(self, mock_which): result = _probe_macos_seatbelt() assert result.supported is False assert result.mode == SandboxMode.NONE # ============================================================================ # detect_platform_mode() — integration with probe # ============================================================================ class TestDetectPlatformMode: """Test that detect_platform_mode uses probe results.""" @patch("pineagents.sandbox.config.probe_sandbox_support") def test_returns_probe_mode(self, mock_probe): mock_probe.return_value = SandboxCapability( supported=True, mode=SandboxMode.LANDLOCK, reason="ok", landlock_abi_version=3, ) assert detect_platform_mode() == SandboxMode.LANDLOCK @patch("pineagents.sandbox.config.probe_sandbox_support") def test_returns_none_when_unsupported(self, mock_probe): mock_probe.return_value = SandboxCapability( supported=False, mode=SandboxMode.NONE, reason="too old", ) assert detect_platform_mode() == SandboxMode.NONE # ============================================================================ # LinuxSandbox._generate_sandbox_script — rule compilation # ============================================================================ class TestLinuxSandboxRuleCompilation: """Test that Landlock rules are correctly generated from SandboxConfig.""" def test_basic_workspace_mount(self): from pineagents.sandbox.linux_sandbox import _generate_sandbox_script config = SandboxConfig( mode=SandboxMode.LANDLOCK, workspace_dir="/home/user/project", mounts=[MountSpec(path="/home/user/project", writable=True)], ) script = _generate_sandbox_script( config, "echo hello", "/home/user/project", 1, ) # Should contain the workspace path with write access assert "/home/user/project" in script assert "add_path" in script assert "exec" in script.lower() def test_readonly_mount(self): from pineagents.sandbox.linux_sandbox import ( _FS_READ_ACCESS, _FS_WRITE_ACCESS, _generate_sandbox_script, ) config = SandboxConfig( mode=SandboxMode.LANDLOCK, workspace_dir="/home/user/project", mounts=[ MountSpec(path="/home/user/project", writable=True), MountSpec(path="/opt/data", writable=False), ], ) with patch("os.path.exists", return_value=True): script = _generate_sandbox_script( config, "ls", "/home/user/project", 1, ) # /opt/data should be in the script with read-only access assert "/opt/data" in script def test_deny_paths_excluded(self): from pineagents.sandbox.linux_sandbox import _generate_sandbox_script config = SandboxConfig( mode=SandboxMode.LANDLOCK, workspace_dir="/home/user/project", mounts=[MountSpec(path="/home/user/project", writable=True)], allow_read_all=False, deny_paths=["/home/user/.ssh"], ) script = _generate_sandbox_script( config, "ls", "/home/user/project", 1, ) # deny_paths should NOT appear as an add_path call # (the path should not be granted access) lines_with_ssh = [ line for line in script.split("\n") if ".ssh" in line and "add_path" in line ] assert len(lines_with_ssh) == 0 def test_executable_false(self): from pineagents.sandbox.linux_sandbox import ( _FS_EXEC_ACCESS, _generate_sandbox_script, ) config = SandboxConfig( mode=SandboxMode.LANDLOCK, workspace_dir="/home/user/project", mounts=[ MountSpec( path="/home/user/project", writable=True, executable=True, ), MountSpec( path="/tmp/untrusted", writable=True, executable=False, ), ], ) script = _generate_sandbox_script( config, "ls", "/home/user/project", 1, ) # /tmp/untrusted should NOT have EXEC access bit # Find the line with /tmp/untrusted for line in script.split("\n"): if "/tmp/untrusted" in line and "add_path" in line: # The hex access mask should NOT include EXEC (0x1) assert ( f"0x{_FS_EXEC_ACCESS:x}" not in line or "0x1" not in line.split(",")[1] ) break # ============================================================================ # Governance: sandbox_available=False when the platform cannot sandbox # ============================================================================ class TestGovernanceSandboxUnavailable: """Probe degradation: sandbox_available is False when unsupported. (A shell SANDBOX_FALLBACK then runs unsandboxed via ALLOW in ``assert_policy``; that behavior is covered in test_policy.py.) """ def test_sandbox_fallback_becomes_ask(self): """When the platform cannot sandbox, ``sandbox_available`` is False.""" cap = SandboxCapability( supported=False, mode=SandboxMode.NONE, reason="Kernel 5.10 < 5.13, Landlock unavailable", ) from pineagents.governance.resource_governor import ResourceGovernor governor = ResourceGovernor(workspace_dir="/tmp/test_ws") # Mock policy loading to avoid filesystem operations with ( patch( "pineagents.governance.resource_governor.load_governance_policy", ) as mock_load, patch("pathlib.Path.mkdir"), patch( "pineagents.governance.resource_governor.probe_sandbox_support", return_value=cap, ), ): mock_policy = MagicMock() mock_load.return_value = mock_policy governor.start() assert governor.sandbox_available is False assert ( governor.sandbox_capability.reason == "Kernel 5.10 < 5.13, Landlock unavailable" ) # ============================================================================ # Platform compatibility guard — cross-platform downgrade # ============================================================================ class TestCreateSandboxLandlockDowngrade: """Test that LANDLOCK mode downgrades on non-linux platforms.""" @patch("pineagents.sandbox.config.sys") @patch( "pineagents.sandbox.config.detect_platform_mode", return_value=SandboxMode.NONE, ) def test_landlock_mode_on_darwin_downgrades(self, mock_detect, mock_sys): """LANDLOCK on macOS downgrades to platform default.""" from pineagents.sandbox.local_sandbox import NoneSandbox mock_sys.platform = "darwin" config = SandboxConfig( mode=SandboxMode.LANDLOCK, workspace_dir="/tmp/ws", ) sb = create_sandbox(config) assert isinstance(sb, NoneSandbox) mock_detect.assert_called_once()