diff --git a/src/vercel/_internal/workflow/py_sandbox.py b/src/vercel/_internal/workflow/py_sandbox.py index 829568c..926fe99 100644 --- a/src/vercel/_internal/workflow/py_sandbox.py +++ b/src/vercel/_internal/workflow/py_sandbox.py @@ -26,6 +26,8 @@ class SandboxRestrictionError(RuntimeError): """Raised when workflow code calls a non-deterministic function.""" +# TODO: We should have a more proper proxy that blocks __call__ and +# returns proxied members but otherwise looks the same. def _restricted(name: str) -> Callable[..., NoReturn]: def _raise(*_args: Any, **_kwargs: Any) -> NoReturn: raise SandboxRestrictionError( @@ -288,7 +290,12 @@ def _current_task(loop: Any = None) -> Any: "fsdecode", "fsencode", "fspath", + # These are deterministic enough if the functions that change + # them are blocked... + "getenv", + "getcwd", "_get_exports_list", + "PathLike", environ=os.environ.copy(), allow_if=str.isupper, drops=["fork", "register_at_fork"], @@ -488,6 +495,8 @@ def __getattr__(self, name: str) -> Any: and name not in policy.allowed and not (name.startswith("__") and name.endswith("__")) and not (policy.allow_if is not None and policy.allow_if(name)) + # Only restrict callables (which will include classes). + and callable(policy.resolve_attr(name, real)) ): # Return a restricted callable instead of raising immediately. # This allows module init code like ``from os import urandom`` diff --git a/tests/unit/test_py_sandbox.py b/tests/unit/test_py_sandbox.py index 0fa88ea..a79d63a 100644 --- a/tests/unit/test_py_sandbox.py +++ b/tests/unit/test_py_sandbox.py @@ -136,8 +136,9 @@ def test_os_fspath_allowed(self): def test_os_constants_allowed(self): _run_in_sandbox("import os; _ = os.O_RDONLY") - def test_os_getcwd_blocked(self): - _raises_in_sandbox("import os; os.getcwd()") + def test_os_getcwd_allowed(self): + ns = _run_in_sandbox("import os; result = os.getcwd()") + assert isinstance(ns["result"], str) def test_os_listdir_blocked(self): _raises_in_sandbox("import os; os.listdir('.')") @@ -874,6 +875,14 @@ def test_math_works(self): ns = _run_in_sandbox("import math; result = math.sqrt(16)") assert ns["result"] == 4.0 + def test_shutil_works(self): + _run_in_sandbox("import os; lurr = os.supports_dir_fd") + _run_in_sandbox("import shutil") + + def test_pathlib_works(self): + ns = _run_in_sandbox("import pathlib; result = isinstance(0, pathlib.Path)") + assert not ns["result"] + def test_collections_counter(self): ns = _run_in_sandbox("from collections import Counter; result = dict(Counter('aabbc'))") assert ns["result"] == {"a": 2, "b": 2, "c": 1}