From ba3475ffe37461f06188159633d0e4fba194f2cf Mon Sep 17 00:00:00 2001 From: Dhaval Kapil Date: Wed, 7 Oct 2026 15:37:13 -0700 Subject: [PATCH] [sandbox-hardening] Block Launch Services in the macOS local sandbox Launch Services can start applications outside the calling process's sandbox, allowing sandboxed commands to bypass workspace restrictions. Deny lsopen in the macOS profile shared by ordinary and PTY execution. Add portable profile coverage and a native regression test that checks Launch Services is allowed on the host and denied through session.exec. Test plan: - Focused sandbox checks: 10 passed, 2 native tests skipped. - Formatting, lint, Mypy, and Pyright passed. - Parallel suite: 12085 passed, 66 skipped, 1 existing failure also reproduced on unmodified main; the serial suite was not reached. - Native macOS enforcement and the app-launch reproduction remain pending. --- src/agents/sandbox/sandboxes/unix_local.py | 2 ++ tests/sandbox/test_runtime.py | 34 ++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/agents/sandbox/sandboxes/unix_local.py b/src/agents/sandbox/sandboxes/unix_local.py index b20c6edb78..d69d3b0096 100644 --- a/src/agents/sandbox/sandboxes/unix_local.py +++ b/src/agents/sandbox/sandboxes/unix_local.py @@ -988,6 +988,8 @@ def _literal(path: Path | str) -> str: [ "(version 1)", "(allow default)", + # Launch Services can start applications outside this process's sandbox. + "(deny lsopen)", deny_rules, *allow_rules, ] diff --git a/tests/sandbox/test_runtime.py b/tests/sandbox/test_runtime.py index d0bd145a4b..ced237680e 100644 --- a/tests/sandbox/test_runtime.py +++ b/tests/sandbox/test_runtime.py @@ -1,6 +1,7 @@ from __future__ import annotations import asyncio +import ctypes import io import json import logging @@ -5122,6 +5123,38 @@ async def test_unix_local_exec_confines_commands_to_workspace_root() -> None: shutil.rmtree(workspace_root, ignore_errors=True) +@pytest.mark.asyncio +@pytest.mark.requires_native_macos_sandbox +@pytest.mark.skipif( + sys.platform != "darwin" or shutil.which("sandbox-exec") is None, + reason="sandbox-exec is only available on macOS when installed", +) +async def test_unix_local_exec_denies_launch_services(tmp_path: Path) -> None: + # Query the OS decision without depending on a GUI session or launching a host app. + sandbox_check = ctypes.CDLL("/usr/lib/libsandbox.1.dylib").sandbox_check + sandbox_check.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_int] + sandbox_check.restype = ctypes.c_int + assert sandbox_check(os.getpid(), b"lsopen", 0) == 0 + + check_launch_services = """ +import ctypes +import os + +sandbox_check = ctypes.CDLL("/usr/lib/libsandbox.1.dylib").sandbox_check +sandbox_check.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_int] +sandbox_check.restype = ctypes.c_int +print(sandbox_check(os.getpid(), b"lsopen", 0)) +""" + client = UnixLocalSandboxClient(inherit_host_environment=False) + async with await client.create(manifest=Manifest(root=str(tmp_path / "workspace"))) as session: + result = await session.exec( + sys.executable, "-I", "-c", check_launch_services, shell=False, timeout=10 + ) + + assert result.ok(), result.stderr.decode("utf-8", errors="replace") + assert int(result.stdout.strip()) > 0 + + @pytest.mark.asyncio async def test_unix_local_exec_rejects_when_confinement_is_unavailable( monkeypatch: pytest.MonkeyPatch, @@ -5265,6 +5298,7 @@ def _fake_which(name: str, path: str | None = None) -> str | None: profile = command[2] assert command[:2] == ["/usr/bin/sandbox-exec", "-p"] + assert "(deny lsopen)" in profile assert '(allow file-read-data file-read-metadata (subpath "/opt/homebrew"))' in profile assert '(allow file-read-data file-read-metadata (subpath "/usr/local"))' in profile assert (