You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(sandbox): correct sandlock integration semantics and fail loud
The SandlockSandbox wrapper had several latent correctness issues that
could cause it to silently run with weaker isolation than intended, or
drop resource limits on the floor.
Fixes:
* Network policy intent is now explicit. sandlock's net_allow_hosts
uses tri-state semantics (None=unrestricted, []=deny all, [..]=
allowlist) and network-enabled=True previously passed None to a
Sequence[str] field. Rewritten to pass net_connect=["0-65535"]
when network is enabled, or net_allow_hosts=[] to block DNS when
disabled. TCP defaults ([] = deny all) handle the rest.
* stdout/stderr are now str, not bytes. sandlock returns bytes from
Sandbox.run(); PraisonAI's SandboxResult is typed as str. Added a
_decode() helper with errors="replace" so downstream consumers never
see binary artefacts or crash on .lower() / .split().
* max_cpu is now actually passed to the Policy. Previously
limits.cpu_percent was silently ignored.
* execute_file() passes the script by path, not via `python3 -c <code>`.
Large scripts no longer hit ARG_MAX, and the script's parent
directory is added to the Landlock read allowlist via the new
extra_readable parameter on _create_policy.
* Timeout detection is authoritative: we now inspect result.error for
"timed out" rather than heuristically comparing wall-clock duration
against limits.timeout_seconds. A process that happens to finish
just under the timeout no longer gets mis-classified.
* Sandbox handles are now managed via `with ... as sb:` so cleanup
runs on exception.
* fs_readable is filtered to paths that actually exist. Landlock
fails at spawn time if any allowlisted path is missing, so the
hardcoded list (which included /usr/local/lib/python3) caused
sandlock_spawn failures on most hosts. Now we filter with
os.path.isdir before constructing the policy.
Breaking change — silent fallback removed:
SandlockSandbox used to fall back to SubprocessSandbox whenever
landlock_abi_version() < 1, logging only a warning. This violates
the caller's explicit choice of kernel-level isolation: a
SandlockSandbox that isn't actually using Landlock is a security
footgun, and a warning in the logs is not a consent mechanism.
__init__ now raises RuntimeError if Landlock support is missing.
Callers who want graceful degradation should catch ImportError /
RuntimeError and construct SubprocessSandbox explicitly, e.g.:
try:
sb = SandlockSandbox(cfg)
except (ImportError, RuntimeError):
sb = SubprocessSandbox(cfg)
The equivalent fallback branches in execute(), run_command(), and
execute_file() are removed.
Tests updated:
- test_raises_when_landlock_unavailable replaces the two fallback
tests and asserts RuntimeError is raised at construction time.
- test_sandlock_execution_timeout now mocks result.error instead
of patching time.time.
- test_sandlock_execution_failure sets result.error=None explicitly.
- test_policy_creation_with_minimal_limits strengthened to check
max_cpu, the new net_allow_hosts=[] deny-all semantics, and that
net_connect is left unset (defaults to deny-all).
All 10 unit tests pass, including the real-sandlock integration test
(which was failing on baseline because of the /usr/local/lib/python3
hardcoded path).
0 commit comments