Skip to content

About

A policy-driven process sandboxing library for Common Lisp

Resources

Stars

3 stars

Watchers

0 watching

Forks

Latest commit

 

History

42 Commits

Folders and files

Repository files navigation

cl-exec-sandbox

cl-exec-sandbox is a policy-driven process sandboxing library for Common Lisp. Its policy model provides:

  • read, write, and deny filesystem rules
  • literal paths, deny globs, and portable special roots
  • more-specific nested policy overrides
  • read-only host and workspace-write presets
  • protected project metadata such as .git
  • full, isolated, and managed proxy-only network modes
  • process, user, IPC, UTS, and network namespace isolation on Linux
  • fresh /proc and minimal /dev mounts
  • child timeouts, cancellation, bounded raw-byte capture and retained files
  • explicit capability discovery

One policy is translated by a per-host backend. A policy a backend cannot enforce signals sandbox-unavailable naming the missing capability. Query sandbox-capabilities or sandbox-supported-p before relying on a capability.

The policy surface and Linux enforcement model were checked against OpenAI Codex commit 2e1607ee2fa8099a233df7437adee5f16a741905. Codex is a reference.

Linux backend

The Linux backend targets x86-64 and SBCL. It uses the system bwrap binary for filesystem and namespace isolation, plus a private helper for no_new_privs, seccomp, and managed proxy routing. The helper lives beside the Lisp sources.

macOS backend

The macOS backend runs commands under Seatbelt through /usr/bin/sandbox-exec, generating a profile from the same policy. Resolved rules are emitted from the broadest to the most specific, so Seatbelt’s last-match-wins resolution reproduces nested overrides and protected metadata. Whole-root rules are emitted before device access, so a read-only root still permits /dev. A rule naming a single file becomes a literal filter.

On macOS:

  • :enabled and :isolated networking work
  • sandbox-capabilities reports :process-namespaces as false
  • process separation is an accidental-damage boundary

Deny globs need rg on either host. Seatbelt installations outside /usr/bin can set CL_EXEC_SANDBOX_SEATBELT to an absolute executable path.

Windows backend

The experimental Windows backend combines a fresh AppContainer identity with an invocation-specific restricted-token SID for deny rules and a kill-on-close Job Object for descendants. Run it as a standard Windows user. Build scripts/build-windows-helper.ps1 from a Visual Studio SDK developer shell with Clang installed, then ship build/cl-exec-sandbox-windows.exe beside the Lisp sources. Alternatively, set CL_EXEC_SANDBOX_WINDOWS_HELPER to its absolute path outside writable scopes.

Use appcontainer-sandbox-policy with explicit existing :read-roots and :workspace-roots. Include the command’s binaries and dependencies in the read scopes. Windows also supplies its normal AppContainer system-resource access and private profile storage. Networking is isolated, including loopback.

(let ((policy
        (cl-exec-sandbox:appcontainer-sandbox-policy
         :workspace-roots (list #P"C:/work/project/")
         :read-roots (list #P"C:/tools/"))))
  (cl-exec-sandbox:run-sandboxed
   "C:/tools/tool.exe" '("--version")
   :policy policy :working-directory #P"C:/work/project/" :timeout 30))

The backend rejects whole-host policies, network modes other than :isolated, proc mounts, PID namespaces, globs, drive roots, UNC/device paths, reparse points, hard-linked files, and paths of 240 characters or more. The default protected metadata names are .git, .agents, and .codex. This backend is not policy-equivalent to the Linux whole-host presets.

Filesystem grants are temporary SID-specific ACL entries. Native execution and ACL changes are serialized within a Windows session. Serialize calls with overlapping roots as well, including plan construction and cleanup of missing metadata directories. Keep the trees and ACLs free of concurrent changes by other host processes during a run. Normal completion and run-sandboxed cancellation remove the grants and profile; cleanup failures are reported. Terminating both the Lisp supervisor and helper can leave stale SID entries and a profile. This prototype has no persistent crash-recovery journal. When launching a plan yourself, call sandbox-plan-cleanup after terminating and waiting for its helper.

Run (asdf:test-system :cl-exec-sandbox/windows-tests) after compiling tests/windows-child.c to build/windows-child.exe. CI runs these enforcement tests under a disposable non-administrator account.

Example

(let ((policy
        (cl-exec-sandbox:workspace-write-sandbox-policy
         :workspace-roots (list #P"/work/project/")
         :network :isolated)))
  (cl-exec-sandbox:run-sandboxed
   "/bin/sh"
   '("-c" "git status --short")
   :policy policy
   :working-directory #P"/work/project/"
   :output-limit 65536
   :error-output-limit 65536))

When a limit is exceeded, the returned prefix is available through sandbox-result-output or sandbox-result-error-output. Inspect sandbox-result-output-truncated-p and sandbox-result-error-output-truncated-p before presenting captured output.

Retained raw output

run-sandboxed drains binary stdout/stderr pipes into private files. Set :capture-directory to an existing caller-owned private directory, or set :retain-output-p t to retain generated temporary files. The caller owns retained files and their deletion. Without either option, capture files are removed after the final callback. The per-stream :capture-byte-limit defaults to 67,108,864 bytes and must be a finite non-negative integer. Excess bytes are drained and counted without increasing disk usage.

(let* ((result (cl-exec-sandbox:run-sandboxed
                "/bin/sh" '("-c" "make")
                :policy (cl-exec-sandbox:unrestricted-sandbox-policy)
                :capture-directory #P"/private/command-logs/"
                :capture-byte-limit (* 32 1024 1024)
                :output-limit 0 :error-output-limit 0))
       (capture (cl-exec-sandbox:sandbox-result-error-capture result)))
  (multiple-value-bind (head tail omitted)
      (cl-exec-sandbox:sample-capture capture :head-bytes 1024 :tail-bytes 4096)
    (values (cl-exec-sandbox:decode-capture-bytes head)
            (cl-exec-sandbox:decode-capture-bytes tail)
            omitted)))

Inspect sandbox-result-output-capture and sandbox-result-error-capture. With :merge-output-p t, stdout and stderr share the output capture and the error capture is NIL. Separate streams have independent ordering. Capture metadata includes sandbox-capture-path, sandbox-capture-byte-count (actual closed file size), sandbox-capture-observed-byte-count (bytes drained), sandbox-capture-complete-p, sandbox-capture-truncated-p and sandbox-capture-status. Status is one of :complete, :limit, :write-error, :interrupted, :timeout, :cancelled or :launch-failed. Completeness requires ordinary command completion, EOF and successful storage of all drained bytes; a nonzero command exit can have a complete capture. Storage limits/failures retain the bytes successfully written and continue draining the child. sandbox-capture-retained-p identifies caller-owned files; transient metadata records the captured bytes even though cleanup removes the file.

sample-capture returns three values: raw head octets, non-overlapping tail octets, and omitted retained bytes. It seeks directly to the tail and allocates only the requested samples. decode-capture-bytes renders UTF-8 with replacement characters for malformed input; the retained file preserves raw bytes. The legacy :output-limit and :error-output-limit count rendered characters. Retained modes default omitted text limits to zero, so result construction uses bounded memory. Transient legacy capture defaults to complete rendered text; pass explicit NIL for that behavior in retained mode, or a finite limit.

:cancel-function is a zero-argument predicate polled during execution. sandbox-result-status distinguishes :exited, :timeout, :cancelled, :launch-failed and :interrupted; timeout and cancellation also have boolean result accessors. :capture-created-function receives initial result metadata synchronously after capture allocation and before native launch, allowing the caller to durably record paths before command side effects. Initial byte counts are zero, exit code is NIL and status is :interrupted with completeness false. An error in this callback prevents launch.

:capture-function receives final result metadata on normal completion and every executor unwind, after termination, pipe shutdown and file closure. Launch failures signal sandbox-execution-error with the final result through sandbox-execution-error-result. Input/policy validation occurs before capture setup. Callback failures cannot skip process cleanup. Pipes held open by surviving descendants have bounded reader shutdown and an incomplete capture. Asynchronous interrupts are deferred through ordered cleanup and the final callback. Keep metadata callbacks brief. On POSIX, generated files have mode 0600; on Windows they inherit the private capture directory’s ACL. Allocate a directory accessible only to its owner.

Checks

Build the host’s private helper and run the complete supported-host test suite:

./check

Portable execution, retained capture and Seatbelt profile translation cases run on POSIX hosts. Linux-only enforcement cases are explicitly skipped elsewhere. Run the separate native Windows test system for AppContainer enforcement.

Applications which vendor this system should run scripts/build-helper during their build and ship build/cl-exec-sandbox-process-group beside the Lisp sources. Linux applications using restricted networking must also ship build/cl-exec-sandbox-helper. Set CL_EXEC_SANDBOX_PROCESS_GROUP_HELPER or CL_EXEC_SANDBOX_HELPER to alternate absolute helper paths when the installed layout differs. Packaged Bubblewrap installations outside /usr/bin and /bin can set CL_EXEC_SANDBOX_BWRAP to its absolute executable path.

Part of the Lambda Symbolics library shelf.

About

A policy-driven process sandboxing library for Common Lisp

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages