Expose sandbox command wrapper
ober
1c5715a4a65345d6f424d56162788fa2425e111f
--- a/docs/limits-followup.md +++ b/docs/limits-followup.md @@ -112,6 +112,9 @@ Implemented: - `require: '(limits)` fails closed before target exec unless every requested limit reports `installed` or `parent`. - macOS SBPL generation for path policies. +- `sandbox-command-wrapper-needed?`, `sandbox-command-wrapper-available?`, and + `sandbox-wrap-command` expose the reusable command rewrite needed by custom + launchers that cannot call `sandbox-launch` directly. - Linux Landlock hook via `jerboa_landlock_sandbox`. Still incomplete: --- a/lib/std/os/limits/sandbox.ss +++ b/lib/std/os/limits/sandbox.ss @@ -61,6 +61,9 @@ sandbox-launch sandbox-prepare-child! + sandbox-command-wrapper-needed? + sandbox-command-wrapper-available? + sandbox-wrap-command sandbox-policy-sbpl ;; debug: render policy as SBPL string (macOS only) sandbox-result? @@ -348,6 +351,34 @@ (cons "-p" (cons sbpl cmd))))] [else cmd]))) + (def (sandbox-policy-has-paths? pol) + (or (pair? (sandbox-policy-get pol 'read-paths)) + (pair? (sandbox-policy-get pol 'write-paths)) + (pair? (sandbox-policy-get pol 'exec-paths)))) + + (def (sandbox-command-wrapper-needed? pol) + ;; Some backends install policy directly in the child between fork and + ;; exec. macOS path policies are different: they must exec through + ;; sandbox-exec so the target starts inside the deny-default profile. + (and (platform-macos?) + (sandbox-policy-has-paths? pol))) + + (def (sandbox-command-wrapper-available? pol) + (or (not (sandbox-command-wrapper-needed? pol)) + (file-exists? "/usr/bin/sandbox-exec"))) + + (def (sandbox-wrap-command pol cmd) + ;; Pure command rewrite for callers that have their own fork/exec path. + ;; If no wrapper is needed, returns CMD unchanged. If a wrapper is needed + ;; but unavailable, also returns CMD unchanged; callers that require + ;; fail-closed semantics should check sandbox-command-wrapper-available? + ;; before launch. + (cond + [(and (sandbox-command-wrapper-needed? pol) + (sandbox-command-wrapper-available? pol)) + (macos-wrap-command pol cmd)] + [else cmd])) + (def (prepare-freebsd! pol) ;; Capsicum is all-or-nothing capability mode. We can pre-open the ;; declared paths and then enter capsicum. Without the FFI plumbed --- a/tests/test-limits-primitives.ss +++ b/tests/test-limits-primitives.ss @@ -459,6 +459,30 @@ (process-result-stdout-bytes (sandbox-result-process r)) 0)) +(let* ([pol (sandbox-policy 'read-paths: '("/tmp"))] + [cmd '("/bin/echo" "wrapped?")] + [wrapped (sandbox-wrap-command pol cmd)]) + (test "sandbox wrapper availability is boolean" + (boolean? (sandbox-command-wrapper-available? pol)) + #t) + (test "sandbox wrapper need is boolean" + (boolean? (sandbox-command-wrapper-needed? pol)) + #t) + (test-pred "sandbox command wrapper preserves command shape" + wrapped + (lambda (x) + (and (pair? x) + (string? (car x))))) + (test "sandbox command wrapper changes command only when needed and available" + (if (and (sandbox-command-wrapper-needed? pol) + (sandbox-command-wrapper-available? pol)) + (car wrapped) + wrapped) + (if (and (sandbox-command-wrapper-needed? pol) + (sandbox-command-wrapper-available? pol)) + "/usr/bin/sandbox-exec" + cmd))) + ;; A required axis that isn't installed must refuse to launch the child. (let ([r (guard (exn [#t (cons 'error (if (message-condition? exn)