From 204cf24b867558f12ce7c5decf1c5acf92a70606 Mon Sep 17 00:00:00 2001 From: Jackmeson1 <130693642+Jackmeson1@users.noreply.github.com> Date: Mon, 26 Oct 2026 18:21:08 -0700 Subject: [PATCH] Restrict time.clock_gettime and clock_gettime_ns in the workflow sandbox The runtime-only restrictions on the time module block time(), monotonic(), perf_counter(), process_time() and the other clock readers, but not clock_gettime() or clock_gettime_ns(), so workflow code could read the clocks through them without the sandbox raising. Add both to the restricted set. Calls while the workflow module is imported stay allowed, as for the other time entries. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019TTGAv6fe859ebbCLLfLHP Signed-off-by: Jackmeson1 <130693642+Jackmeson1@users.noreply.github.com> --- .../breaking-changes/punctual-platypus.md | 1 + .../worker/workflow_sandbox/_restrictions.py | 4 +++ .../worker/workflow_sandbox/test_importer.py | 29 +++++++++++++++++++ tests/worker/workflow_sandbox/test_runner.py | 7 +++++ 4 files changed, 41 insertions(+) create mode 100644 changelog/breaking-changes/punctual-platypus.md 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(