diff --git a/changelog/breaking-changes/punctual-platypus.md b/changelog/breaking-changes/punctual-platypus.md new file mode 100644 index 000000000..8492768c7 --- /dev/null +++ b/changelog/breaking-changes/punctual-platypus.md @@ -0,0 +1 @@ +The workflow sandbox now restricts `time.clock_gettime()` and `time.clock_gettime_ns()` at runtime, like `time.time()` and `time.monotonic()`. Calling them from workflow code raises `RestrictedWorkflowAccessError`. diff --git a/temporalio/worker/workflow_sandbox/_restrictions.py b/temporalio/worker/workflow_sandbox/_restrictions.py index 9ca5ad4e4..333ad96eb 100644 --- a/temporalio/worker/workflow_sandbox/_restrictions.py +++ b/temporalio/worker/workflow_sandbox/_restrictions.py @@ -742,6 +742,10 @@ def _public_callables(parent: Any, *, exclude: set[str] = set()) -> set[str]: use={ "pthread_getcpuclockid", "get_clock_info", + # Not available on every platform (e.g. Windows); matching a + # nonexistent attribute is harmless + "clock_gettime", + "clock_gettime_ns", "localtime", "monotonic", "monotonic_ns", diff --git a/tests/worker/workflow_sandbox/test_importer.py b/tests/worker/workflow_sandbox/test_importer.py index 205a8df3b..4ecaa1144 100644 --- a/tests/worker/workflow_sandbox/test_importer.py +++ b/tests/worker/workflow_sandbox/test_importer.py @@ -4,6 +4,7 @@ import subprocess import sys import textwrap +import time import types from typing import Any @@ -40,6 +41,34 @@ def test_workflow_sandbox_importer_path_in_place_division(): path.exists() +@pytest.mark.parametrize( + ("function", "args"), + [ + ("time", ""), + ("monotonic", ""), + ("clock_gettime", "time.CLOCK_REALTIME"), + ("clock_gettime_ns", "time.CLOCK_REALTIME"), + ], + ids=["time", "monotonic", "clock_gettime", "clock_gettime_ns"], +) +def test_workflow_sandbox_importer_clock_restricted_at_runtime_only( + function: str, args: str +): + if not hasattr(time, function): + pytest.skip(f"time.{function} is not available on this platform") + call = f"time.{function}({args})" + context = RestrictionContext() + with Importer(SandboxRestrictions.default, context).applied(): + import time as sandboxed_time + + # Allowed while the workflow module is being imported + eval(call, {"time": sandboxed_time}) + context.is_runtime = True + with pytest.raises(RestrictedWorkflowAccessError) as err: + eval(call, {"time": sandboxed_time}) + assert err.value.qualified_name == f"time.{function}.__call__" + + @pytest.mark.parametrize("first", ["host", "sandbox"]) def test_workflow_sandbox_importer_pydantic_constraints(first: str): pytest.importorskip("pydantic", minversion="2") diff --git a/tests/worker/workflow_sandbox/test_runner.py b/tests/worker/workflow_sandbox/test_runner.py index d357a6872..55ef6384a 100644 --- a/tests/worker/workflow_sandbox/test_runner.py +++ b/tests/worker/workflow_sandbox/test_runner.py @@ -201,6 +201,13 @@ async def test_workflow_sandbox_restrictions(client: Client): if sys.version_info >= (3, 14): invalid_code_to_check.append("import uuid\nuuid.uuid7()") # type: ignore[reportUnreachable] + # clock_gettime is not available on every platform (e.g. Windows) + if hasattr(time, "clock_gettime"): + invalid_code_to_check += [ + "import time\ntime.clock_gettime(time.CLOCK_REALTIME)", + "import time\ntime.clock_gettime_ns(time.CLOCK_REALTIME)", + ] + for code in invalid_code_to_check: with pytest.raises(WorkflowFailureError) as err: await client.execute_workflow(