fix(sandbox): correct sandlock integration semantics and fail loud#1367
fix(sandbox): correct sandlock integration semantics and fail loud#1367congwang-mk wants to merge 2 commits intoMervinPraison:mainfrom
Conversation
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).
Sandbox.run() does not populate result.error on timeout β only the Pipeline path does. String-matching on result.error was therefore unreliable for the common single-sandbox case: a real timeout from Sandbox.run() returns success=False, exit_code=-1, empty stderr, and error=None, which my previous logic mis-classified as FAILED. Switch to the structural signal: sandlock's ExitStatus::Timeout is exposed as exit_code == -1 (see sandlock's _sdk.py around line 1475). This matches how sandlock itself detects pipeline timeouts and works uniformly across Sandbox.run() and any future execution paths. Verified end-to-end with a real forced timeout against real sandlock: status: SandboxStatus.TIMEOUT exit: -1 error: Execution timed out after 1s Test updated to match: mock_timeout_result.exit_code = -1 and error = None (reflecting actual Sandbox.run() behavior).
π WalkthroughWalkthroughThe changes enhance the Sandlock sandbox implementation with runtime Landlock availability validation, extended filesystem allowlisting via Changes
Estimated code review effortπ― 4 (Complex) | β±οΈ ~45 minutes Possibly related PRs
Poem
π₯ Pre-merge checks | β 3β Passed checks (3 passed)
βοΈ Tip: You can configure your own custom pre-merge checks in the settings. β¨ Finishing Touchesπ§ͺ Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@copilot Do a thorough review of this PR. Read ALL existing reviewer comments above from Qodo, Coderabbit, and Gemini first β incorporate their findings. Review areas:
|
There was a problem hiding this comment.
Code Review
This pull request enhances the SandlockSandbox by enforcing Landlock support at instantiation and removing the silent fallback to SubprocessSandbox. Key improvements include refined network policy logic, better timeout detection using sandlock's exit code sentinels, and a more robust execute_file implementation that passes script paths directly to interpreters. Feedback focuses on tightening the filesystem security policy: specifically, ensuring that the sandbox's temporary and working directories are explicitly granted read access (as required by Landlock) and narrowing the scope of extra_readable paths to individual files rather than entire parent directories to prevent unintended data exposure.
| limits=limits, | ||
| env=env, | ||
| working_dir=self._temp_dir, | ||
| extra_readable=[os.path.dirname(abs_path)], |
There was a problem hiding this comment.
Allowlisting the entire parent directory of the script is overly permissive and could expose sensitive files on the host if the script is located outside the sandbox's temporary directory. It is safer to only allowlist the script file itself. Note that this requires updating _create_policy to support files in extra_readable (by using os.path.exists instead of os.path.isdir).
| extra_readable=[os.path.dirname(abs_path)], | |
| extra_readable=[abs_path], |
| allowed_read_paths = [p for p in _candidate_read_paths if os.path.isdir(p)] | ||
| if extra_readable: | ||
| allowed_read_paths.extend( | ||
| p for p in extra_readable if os.path.isdir(p) | ||
| ) |
There was a problem hiding this comment.
The current implementation of _create_policy has two issues regarding the read allowlist:
- It only allows directories in
extra_readabledue to theos.path.isdircheck. This prevents allowlisting individual files, which is necessary for theexecute_filemethod to work securely. - The sandbox's temporary directory (
self._temp_dir) and theworking_dirare added to the writable allowlist but not the readable one. In Landlock, write access does not implicitly grant read access. The sandboxed process will likely fail to read its own working directory or files written to the sandbox unless they are explicitly added tofs_readable.
| allowed_read_paths = [p for p in _candidate_read_paths if os.path.isdir(p)] | |
| if extra_readable: | |
| allowed_read_paths.extend( | |
| p for p in extra_readable if os.path.isdir(p) | |
| ) | |
| allowed_read_paths = [p for p in _candidate_read_paths if os.path.isdir(p)] | |
| if extra_readable: | |
| allowed_read_paths.extend(p for p in extra_readable if os.path.exists(p)) | |
| for p in [self._temp_dir, working_dir]: | |
| if p and os.path.isdir(p) and p not in allowed_read_paths: | |
| allowed_read_paths.append(p) |
Greptile SummaryThis PR hardens the Key changes:
Confidence Score: 4/5Safe to merge after correcting the misleading kernel version in the RuntimeError message; all logic changes are sound. The PR correctly fixes all stated bugs: network policy semantics, bytes/str decoding, max_cpu propagation, file-by-path execution, Landlock path filtering, timeout sentinel, and context-manager cleanup. The breaking change (fail-loud vs. silent fallback) is intentional, well-documented, and the right security call. The only factual error is the kernel version in the error message ('>= 6.12' vs. the correct '>= 5.13'), which is a non-functional P2. The test quality issue (missing explicit success=True) is minor. src/praisonai/praisonai/sandbox/sandlock.py line 88 (kernel version string); src/praisonai/tests/unit/sandbox/test_sandlock_sandbox.py line 104 (missing success=True in mock) Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["SandlockSandbox.__init__"] --> B{landlock_abi_version?}
B -->|exception| C["raise RuntimeError\n(query failed)"]
B -->|abi < 1| D["raise RuntimeError\n(Landlock unsupported)"]
B -->|abi >= 1| E["Sandbox ready"]
E --> F["execute / run_command / execute_file"]
F --> G["_create_policy(limits, working_dir, extra_readable)"]
G --> H["Filter candidate read paths\n(os.path.isdir)"]
H --> I{network_enabled?}
I -->|Yes| J["net_connect=['0-65535']"]
I -->|No| K["net_allow_hosts=[]"]
J & K --> L["Policy(fs_readable, fs_writable,\nmax_memory, max_processes,\nmax_open_files, max_cpu, **net_kwargs)"]
L --> M["_run_sandlocked(cmd)"]
M --> N["run_in_executor β _run()\nwith Sandbox(policy) as sb:\n sb.run(cmd, timeout=...)"]
N -->|exception| O["SandboxResult(FAILED)"]
N -->|result| P{result.success?}
P -->|True| Q["SandboxResult(COMPLETED)"]
P -->|exit_code == -1| R["SandboxResult(TIMEOUT)"]
P -->|else| S["SandboxResult(FAILED)"]
N --> T["_decode(stdout/stderr)\nbytes β str via UTF-8 replace"]
T --> P
|
| raise RuntimeError( | ||
| "SandlockSandbox requires Landlock support (Linux kernel " | ||
| ">= 6.12 with CONFIG_SECURITY_LANDLOCK=y). This kernel " | ||
| f"reports Landlock ABI version {abi}. Use SubprocessSandbox " | ||
| "explicitly if weaker isolation is acceptable." | ||
| ) |
There was a problem hiding this comment.
Incorrect minimum kernel version in error message
The error message states Linux kernel >= 6.12, but Landlock ABI version 1 was introduced in Linux 5.13 (not 6.12). The PR description itself mentions "running on a kernel <5.13" in the breaking-change section, so the discrepancy is likely a typo. Users on kernels between 5.13 and 6.12 who encounter this error will be told their kernel is too old when it is actually sufficient.
| raise RuntimeError( | |
| "SandlockSandbox requires Landlock support (Linux kernel " | |
| ">= 6.12 with CONFIG_SECURITY_LANDLOCK=y). This kernel " | |
| f"reports Landlock ABI version {abi}. Use SubprocessSandbox " | |
| "explicitly if weaker isolation is acceptable." | |
| ) | |
| if abi < 1: | |
| raise RuntimeError( | |
| "SandlockSandbox requires Landlock support (Linux kernel " | |
| ">= 5.13 with CONFIG_SECURITY_LANDLOCK=y). This kernel " | |
| f"reports Landlock ABI version {abi}. Use SubprocessSandbox " | |
| "explicitly if weaker isolation is acceptable." | |
| ) |
There was a problem hiding this comment.
Actionable comments posted: 2
π§Ή Nitpick comments (2)
src/praisonai/tests/unit/sandbox/test_sandlock_sandbox.py (1)
104-106: Mock stdout/stderr types don't match real sandlock behavior.The mock uses
strforstdout/stderr, but real sandlock returnsbytes. While_decode()handles both types, this test doesn't exercise the byte decoding path. For consistency with the timeout/failure tests (which useb""), consider using bytes here too.π Suggested fix
mock_result = Mock() mock_result.exit_code = 0 - mock_result.stdout = "Hello, World!" - mock_result.stderr = "" + mock_result.stdout = b"Hello, World!" + mock_result.stderr = b"" + mock_result.success = TrueNote: You'll also need to add
mock_result.success = Truesince the implementation checksresult.successfirst.π€ Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/praisonai/tests/unit/sandbox/test_sandlock_sandbox.py` around lines 104 - 106, Update the test's mock_result to mirror real sandlock behavior by setting mock_result.stdout and mock_result.stderr to bytes (e.g., b"Hello, World!" and b"") instead of str, and also set mock_result.success = True so the code path that checks result.success is exercised; this ensures the _decode() byte-handling branch and success branch are covered when tests run.src/praisonai/praisonai/sandbox/sandlock.py (1)
275-286: Broad exception catch is acceptable here but could be narrowed.The static analysis tool flags
except Exception(BLE001). While catching broad exceptions is generally discouraged, this usage is reasonable for wrapping sandlock execution where multiple exception types could occur. The exception details are preserved in the error field rather than silently swallowed.Consider narrowing to specific expected exceptions if sandlock documents them, but the current approach is pragmatic for resilience.
π€ Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/praisonai/praisonai/sandbox/sandlock.py` around lines 275 - 286, The catch-all "except Exception" in the try block that awaits asyncio.get_running_loop().run_in_executor(None, _run) is triggering BLE001; either narrow it to the specific exceptions sandlock can raise (e.g., TimeoutError, RuntimeError, OSError) by replacing "except Exception as e:" with "except (TimeoutError, RuntimeError, OSError) as e:" in the surrounding function, or if you intentionally want the broad catch for sandbox resilience, keep it but add logging.exception(...) to record the traceback and add a concise lint justification comment (e.g., "# broad except to ensure sandbox resilience (BLE001)"), or alternatively suppress the linter for that line with a targeted noqa, while still returning the SandboxResult using SandboxResult, SandboxStatus, execution_id and started_at as currently done.
π€ Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/praisonai/praisonai/sandbox/sandlock.py`:
- Around line 378-391: The code unconditionally adds os.path.dirname(abs_path)
to extra_readable which can be "/" for files at filesystem root and would expose
the entire root; change the logic before calling _run_sandlocked to detect this
edge case (abs_path from os.path.realpath) and if os.path.dirname(abs_path) ==
os.path.sep, pass the file path itself (abs_path) or a more restrictive path
list instead of "/" as the extra_readable entry; update the call site that
constructs extra_readable (in the method that builds cmd and calls
self._run_sandlocked) to use this conditional selection so only the file (or a
non-root directory) is added.
- Around line 85-91: The RuntimeError message raised in SandlockSandbox when abi
< 1 contains an incorrect kernel version; update the text in the raise
RuntimeError (in sandlock.py around the SandlockSandbox check using the variable
abi) to state the correct minimum kernel that introduced Landlock ABI v1 (Linux
kernel >= 5.13) while preserving the rest of the guidance
(CONFIG_SECURITY_LANDLOCK=y and suggestion to use SubprocessSandbox).
---
Nitpick comments:
In `@src/praisonai/praisonai/sandbox/sandlock.py`:
- Around line 275-286: The catch-all "except Exception" in the try block that
awaits asyncio.get_running_loop().run_in_executor(None, _run) is triggering
BLE001; either narrow it to the specific exceptions sandlock can raise (e.g.,
TimeoutError, RuntimeError, OSError) by replacing "except Exception as e:" with
"except (TimeoutError, RuntimeError, OSError) as e:" in the surrounding
function, or if you intentionally want the broad catch for sandbox resilience,
keep it but add logging.exception(...) to record the traceback and add a concise
lint justification comment (e.g., "# broad except to ensure sandbox resilience
(BLE001)"), or alternatively suppress the linter for that line with a targeted
noqa, while still returning the SandboxResult using SandboxResult,
SandboxStatus, execution_id and started_at as currently done.
In `@src/praisonai/tests/unit/sandbox/test_sandlock_sandbox.py`:
- Around line 104-106: Update the test's mock_result to mirror real sandlock
behavior by setting mock_result.stdout and mock_result.stderr to bytes (e.g.,
b"Hello, World!" and b"") instead of str, and also set mock_result.success =
True so the code path that checks result.success is exercised; this ensures the
_decode() byte-handling branch and success branch are covered when tests run.
πͺ Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
βΉοΈ Review info
βοΈ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 2ced78a2-08ac-444c-aaf3-c98f7e9529d1
π Files selected for processing (2)
src/praisonai/praisonai/sandbox/sandlock.pysrc/praisonai/tests/unit/sandbox/test_sandlock_sandbox.py
| if abi < 1: | ||
| raise RuntimeError( | ||
| "SandlockSandbox requires Landlock support (Linux kernel " | ||
| ">= 6.12 with CONFIG_SECURITY_LANDLOCK=y). This kernel " | ||
| f"reports Landlock ABI version {abi}. Use SubprocessSandbox " | ||
| "explicitly if weaker isolation is acceptable." | ||
| ) |
There was a problem hiding this comment.
Incorrect kernel version in error message.
The error message states Landlock requires "Linux kernel >= 6.12", but Landlock ABI v1 was introduced in Linux kernel 5.13 (released 2021). Kernel 6.12 is not accurate and may mislead users into thinking they need a much newer kernel than actually required.
π Suggested fix
if abi < 1:
raise RuntimeError(
"SandlockSandbox requires Landlock support (Linux kernel "
- ">= 6.12 with CONFIG_SECURITY_LANDLOCK=y). This kernel "
+ ">= 5.13 with CONFIG_SECURITY_LANDLOCK=y). This kernel "
f"reports Landlock ABI version {abi}. Use SubprocessSandbox "
"explicitly if weaker isolation is acceptable."
)π Committable suggestion
βΌοΈ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if abi < 1: | |
| raise RuntimeError( | |
| "SandlockSandbox requires Landlock support (Linux kernel " | |
| ">= 6.12 with CONFIG_SECURITY_LANDLOCK=y). This kernel " | |
| f"reports Landlock ABI version {abi}. Use SubprocessSandbox " | |
| "explicitly if weaker isolation is acceptable." | |
| ) | |
| if abi < 1: | |
| raise RuntimeError( | |
| "SandlockSandbox requires Landlock support (Linux kernel " | |
| ">= 5.13 with CONFIG_SECURITY_LANDLOCK=y). This kernel " | |
| f"reports Landlock ABI version {abi}. Use SubprocessSandbox " | |
| "explicitly if weaker isolation is acceptable." | |
| ) |
π€ Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/praisonai/praisonai/sandbox/sandlock.py` around lines 85 - 91, The
RuntimeError message raised in SandlockSandbox when abi < 1 contains an
incorrect kernel version; update the text in the raise RuntimeError (in
sandlock.py around the SandlockSandbox check using the variable abi) to state
the correct minimum kernel that introduced Landlock ABI v1 (Linux kernel >=
5.13) while preserving the rest of the guidance (CONFIG_SECURITY_LANDLOCK=y and
suggestion to use SubprocessSandbox).
| abs_path = os.path.realpath(file_path) | ||
| interp = "bash" if file_path.endswith((".sh", ".bash")) else "python3" | ||
| cmd: List[str] = [interp, abs_path] | ||
| if args: | ||
| cmd.extend(args) | ||
|
|
||
| return await self._run_sandlocked( | ||
| cmd, | ||
| execution_id=execution_id, | ||
| limits=limits, | ||
| env=env, | ||
| working_dir=self._temp_dir, | ||
| extra_readable=[os.path.dirname(abs_path)], | ||
| ) |
There was a problem hiding this comment.
Consider edge case when script is at filesystem root.
If abs_path resolves to a file directly under / (e.g., /script.py), then os.path.dirname(abs_path) returns "/", which would add the entire root filesystem to the read allowlist. While this is an unlikely edge case, it could weaken isolation.
π‘οΈ Suggested guard
abs_path = os.path.realpath(file_path)
+ script_dir = os.path.dirname(abs_path)
+ extra_read = [script_dir] if script_dir and script_dir != "/" else []
interp = "bash" if file_path.endswith((".sh", ".bash")) else "python3"
cmd: List[str] = [interp, abs_path]
if args:
cmd.extend(args)
return await self._run_sandlocked(
cmd,
execution_id=execution_id,
limits=limits,
env=env,
working_dir=self._temp_dir,
- extra_readable=[os.path.dirname(abs_path)],
+ extra_readable=extra_read,
)π Committable suggestion
βΌοΈ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| abs_path = os.path.realpath(file_path) | |
| interp = "bash" if file_path.endswith((".sh", ".bash")) else "python3" | |
| cmd: List[str] = [interp, abs_path] | |
| if args: | |
| cmd.extend(args) | |
| return await self._run_sandlocked( | |
| cmd, | |
| execution_id=execution_id, | |
| limits=limits, | |
| env=env, | |
| working_dir=self._temp_dir, | |
| extra_readable=[os.path.dirname(abs_path)], | |
| ) | |
| abs_path = os.path.realpath(file_path) | |
| script_dir = os.path.dirname(abs_path) | |
| extra_read = [script_dir] if script_dir and script_dir != "/" else [] | |
| interp = "bash" if file_path.endswith((".sh", ".bash")) else "python3" | |
| cmd: List[str] = [interp, abs_path] | |
| if args: | |
| cmd.extend(args) | |
| return await self._run_sandlocked( | |
| cmd, | |
| execution_id=execution_id, | |
| limits=limits, | |
| env=env, | |
| working_dir=self._temp_dir, | |
| extra_readable=extra_read, | |
| ) |
π€ Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/praisonai/praisonai/sandbox/sandlock.py` around lines 378 - 391, The code
unconditionally adds os.path.dirname(abs_path) to extra_readable which can be
"/" for files at filesystem root and would expose the entire root; change the
logic before calling _run_sandlocked to detect this edge case (abs_path from
os.path.realpath) and if os.path.dirname(abs_path) == os.path.sep, pass the file
path itself (abs_path) or a more restrictive path list instead of "/" as the
extra_readable entry; update the call site that constructs extra_readable (in
the method that builds cmd and calls self._run_sandlocked) to use this
conditional selection so only the file (or a non-root directory) is added.
|
@claude You are the FINAL architecture reviewer. Read ALL comments above from Gemini, Qodo, CodeRabbit, and Copilot carefully before responding. Phase 1: Review per AGENTS.md
Phase 2: FIX Valid Issues Phase 3: Final Verdict |
|
Claude finished @MervinPraison's task ββ View job Architecture Review and AnalysisCRITICAL LIMITATION: This is a PR from a FORK - I cannot push fixes directly to this branch. I can only provide review feedback. Todo List:
Phase 1: Architecture Review per AGENTS.mdβ
Protocol-driven Design: This change is appropriately placed in the wrapper ( β Backward Compatible: The changes don't break public APIs. The breaking change (removing silent fallback) is intentional and documented. β
Performance: No hot-path regressions - sandlock is lazy-imported and only affects those explicitly choosing Phase 2: Valid Issues Found by ReviewersI've analyzed all reviewer feedback and identified 2 CRITICAL BUGS that need immediate attention: 1. CRITICAL: Incorrect Kernel Version (
|
Summary
The
SandlockSandboxwrapper insrc/praisonai/praisonai/sandbox/sandlock.pyhas several latent correctness and security issues. A caller who asks for kernel-level isolation could silently get weaker isolation, resource limits get dropped on the floor, and the timeout classifier never actually fires for real timeouts.This PR fixes the integration to match sandlock's actual API, removes the silent fallback that violated the caller's explicit security choice, and strengthens the test suite accordingly.
What's fixed
Network policy intent is now explicit. sandlock uses tri-state semantics for
net_allow_hosts(None= unrestricted,[]= deny all,[...]= allowlist). The previous code passedNoneto aSequence[str]field when network was enabled and[]when disabled β semantically backwards and type-invalid. Rewritten to passnet_connect=[\"0-65535\"]when enabled ornet_allow_hosts=[]when disabled. TCP-level deny-all defaults handle the rest.stdout/stderrare nowstr, notbytes. sandlock returnsbytesfromSandbox.run(); PraisonAI'sSandboxResultis typedstr. Added a_decode()helper witherrors=\"replace\"so downstream consumers never see binary artefacts or crash on.lower()/.split().max_cpuis now actually passed.limits.cpu_percentwas silently ignored. Addedmax_cpu=limits.cpu_percentto the Policy construction.execute_file()passes the script by path. The old implementation slurped the file intopython3 -c <code>which is subject toARG_MAX. Now it invokes[interp, abs_path, *args]directly and adds the script's parent directory to the Landlock read allowlist via a newextra_readableparameter on_create_policy.Timeout detection uses sandlock's structural sentinel. sandlock exposes
ExitStatus::Timeoutasexit_code == -1(seesandlock/_sdk.pyaround line 1475).Sandbox.run()does not populateresult.erroron timeout β only pipelines do β so string-matching is unreliable. Switching toexit_code == -1works uniformly and matches how sandlock itself detects timeouts.Context manager around
Sandbox.with self._sandlock.Sandbox(policy) as sb:guarantees cleanup even if.run()raises mid-flight.fs_readableis filtered to paths that exist. Landlock fails at spawn time if any allowlisted path is missing, so the hardcoded list β which included/usr/local/lib/python3β causedsandlock_spawn failedon most hosts. Thetest_real_sandlock_integrationtest was silently failing on baseline for this reason; it now passes.Breaking change β silent fallback removed
SandlockSandbox.execute/run_command/execute_fileused to fall back toSubprocessSandboxwheneverlandlock_abi_version() < 1, logging only a warning. This violates the caller's explicit choice of kernel-level isolation: aSandlockSandboxthat isn't actually using Landlock is a security footgun, and a warning in the logs is not a consent mechanism.__init__now raisesRuntimeErrorif Landlock support is missing. The three fallback branches inexecute(),run_command(), andexecute_file()are removed.Callers who want graceful degradation should catch
ImportError/RuntimeErrorand constructSubprocessSandboxexplicitly:```python
try:
sb = SandlockSandbox(cfg)
except (ImportError, RuntimeError):
sb = SubprocessSandbox(cfg)
```
This is a deliberate fail-loud change β the previous behavior would silently weaken the security guarantees of any caller running on a kernel <5.13 or in a container with seccomp stripped.
Test plan
test_raises_when_landlock_unavailablereplaces the two old fallback tests and assertsRuntimeErrorat construction time.test_sandlock_execution_timeoutmocksexit_code = -1(the sentinel) instead of patchingtime.time.test_sandlock_execution_failuresetsresult.error = Noneexplicitly (reflects realSandbox.run()behavior).test_policy_creation_with_minimal_limitsstrengthened to verifymax_cpu,net_allow_hosts=[]deny-all, and thatnet_connectis left unset so TCP deny-all defaults apply.test_real_sandlock_integrationnow actually passes (previously failed on baseline due to the/usr/local/lib/python3hardcoded path).execute_filewith args, and a forced real timeout all behave correctly.Files changed
π€ Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
Improvements