Skip to content

Bring back pledge/SECCOMP sandboxing (#930)#1015

Open
aittalam wants to merge 8 commits into
mainfrom
pledge-seccomp
Open

Bring back pledge/SECCOMP sandboxing (#930)#1015
aittalam wants to merge 8 commits into
mainfrom
pledge-seccomp

Conversation

@aittalam

Copy link
Copy Markdown
Member

Closes #930.

Reintroduces self-sandboxing (present in 0.9.3, dropped in the 0.10 rewrite) for the CPU --server, --cli, and --chat. On Linux it uses Cosmopolitan's pledge() (SECCOMP) and unveil() (Landlock); it's a logged no-op on macOS/Windows/older kernels.

Two levels

Default — pledge(), on automatically. The server runs stdio anet rpath: it can accept() connections but never connect() out, and cannot write, create, execute, or fork. So a compromised server (e.g. via a GGUF-parser bug) can't exfiltrate over the network or drop a payload. Reads stay allowed, so normal serving isn't affected. Turn it off with --unsecure.

Opt-in — unveil(), via --confine-reads. Adds path confinement: the server may only read the executable and the weights directories (model + shards, mmproj, LoRA, draft model, --media-path, static --path root). It's opt-in because locking the readable paths at startup is incompatible with files a server opens by path at request time (multimodal media, etc.), so it shouldn't be forced on everyone.

Automatic adjustments (each logged): the network promise relaxes anetinet when the server must dial out (--rpc, server-side tools, the MCP proxy); the sandbox is skipped in GPU mode (drivers need device access) and in combined TUI+server mode. --cli/--chat always pledge and have no network at all.

Tests & docs

  • tests/sandbox_test.c (runs in make check): syscall-level allow/deny per promise set, plus unveil() enforcement.
  • tests/integration -m sandbox: every server thread carries the filter, inference works sandboxed, a bundled /zip/ llamafile loads, --confine-reads confines while the default does not, and --unsecure disables it.
  • docs/security.md documents both levels and the GPU-unsandboxed caveat.

🤖 Generated with Claude Code

aittalam and others added 7 commits July 3, 2026 09:33
…estable policy

Addresses 13 internal review findings on the issue #930 sandbox.

Security:
- Server always pledges rpath. The loader always does a real open() (disk
  weights, or the executable itself to read its /zip/ store), so the old
  "embedded => no rpath" heuristic broke bundled llamafiles at model load.
- Confine that rpath with unveil()/Landlock to the executable and weights
  directories. A fork() governability probe degrades to pledge-only on
  filesystems Landlock can't govern (virtiofs/9p/NFS) instead of failing
  to load the model.
- Broaden llamafile_open_gguf to fall back to the zip store on EACCES/EPERM
  (not just ENOENT) so bare-name convert bundles work under unveil.
- Quiesce the llama.cpp log worker (common_log_pause/resume) around the
  sandbox call: cosmo pledge is per-thread with no TSYNC, so a pre-existing
  thread would otherwise run unsandboxed.

Correctness:
- --slot-save-path now also grants rpath (slot restore reads the file).
- Remove router-mode handling (llamafile always requires a model, so router
  mode is unreachable): drop the LLAMA_SERVER_ROUTER_PORT env sniffing.

Refactor:
- Move server promise/unveil policy out of the vendored server.cpp patch
  into unit-testable llamafile_sandbox_server()/llamafile_sandbox_server_promises().
- Share llamafile_sandbox_enter() (CLI/chat) and llamafile_consume_flag()
  (--unsecure stripping).

Tests:
- Gate sandbox_test.c enforcement to Linux (anet/EPERM are Linux-specific);
  add promise-derivation and unveil-confinement cases.
- Integration: assert every server thread carries the filter; add a bundled
  /zip/ llamafile load test; skip filter-count assertions on kernels lacking
  Seccomp_filters; drop the pty for start_combined().

Docs: correct the EPERM-vs-SIGABRT (Linux-only) and kernel-version claims,
and replace "no filesystem access whatsoever" with read-only + unveil.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… gap

Addresses 10 review findings (2 router findings are no-ops: llamafile can't
start without a model, so is_router_server is unreachable upstream code).

unveil() correctness & security:
- Generalize llamafile_sandbox_server() to read_paths[]/rw_paths[] lists the
  server patch builds from common_params (model, mmproj, draft model,
  lookup cache, control vectors, LoRA, public path; slot-save + prompt
  cache as read-write). One list drives both apply and the probe, so they
  can't drift, and every file the loader opens after the lock is covered
  (fixes the -md draft model being denied under confinement).
- Root-model fix: -m /model.gguf derived dir "/" and unveiled the whole
  filesystem while claiming confinement; container_of() now unveils the
  file itself at the root, and is directory-vs-file aware.
- Governability probe now proves confinement instead of assuming it: a
  canary outside every rule (/etc/passwd) must be actively denied
  (EACCES/EPERM), so a kernel without Landlock -- where cosmo's unveil()
  silently no-ops and returns 0 -- no longer reports confined=true. Every
  path check is captured before the ruleset locks.
- wpath/cpath keyed on a non-empty write target, not list length.
- unveil /etc/hosts and /etc/resolv.conf so a non-numeric --host resolves.

Thread coverage:
- Wrap the CLI and chat pledge in common_log_pause/resume like the server,
  so the log worker doesn't outlive the per-thread filter unsandboxed.

Diagnostics:
- Preserve the original errno in llamafile_open_gguf's zip-store fallback,
  so a real permission/sandbox denial isn't reported as "No such file".

Tests:
- sandbox_test.c child_unveil SKIPs when confinement isn't actually
  enforced (no Landlock, or an over-denying filesystem) rather than failing.
- Integration positive cases fall back to the Seccomp mode field when
  Seccomp_filters is absent, so an unsandboxed regression can't ship green
  on pre-5.9 kernels; add a chat all-threads-filtered regression test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The governability probe reads a file back after locking the unveil ruleset
to confirm confinement actually bites (a still-readable file means Landlock
installed nothing). The canary must be world-readable so that only Landlock
can deny it -- but /etc/passwd reads read like credential recon to an
auditor or EDR, even though we only open()/close() it and never read a byte.

Switch to /etc/os-release then /etc/hostname (both world-readable, ubiquitous,
and routinely read by ordinary software for distro/host detection); the probe
uses the first that exists and conservatively reports "not confined" if none
do. The unit test now uses a self-created temp file outside the unveiled dir
as its canary instead of a system file at all.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
pledge() and unveil() have very different breakage profiles. pledge (no
outbound network / writes / exec) is the cheap, high-value protection 0.9.3
shipped on by default; unveil() path confinement, added in this branch, is
what breaks the modern default cases -- it locks the readable paths at
startup, which is incompatible with files a server opens by path at request
time (multimodal media, static assets). So default them differently.

- pledge() stays on by default for the server (stdio anet rpath, +wpath
  cpath for slot-save/prompt-cache). unveil() is opt-in via --confine-reads.
- Relax anet->inet when the server must dial out (--rpc, server-side tools,
  the MCP proxy) instead of breaking or skipping; writes/exec stay blocked.
  --rpc has no params field, so it's detected on argv/env.
- Add media_path to the confinement read set (multimodal images loaded by
  path at request time); audited common_params for other post-sandbox reads.

Cleanup (review):
- Collapse the gate/pledge triplication into shared sandbox_skip_status() and
  sandbox_install_pledge(); llamafile_sandbox_server() now takes a
  llamafile_sandbox_spec struct and returns ACTIVE / ACTIVE_CONFINED /
  ACTIVE_UNCONFINED (describe() owns the text) instead of out-params.
- Explicit combined_mode bool; --unsecure named in the pledge-failed error.
- Tests: split coverage (default not confined; --confine-reads confines and
  serves); promise cases for inet/outbound; shared _assert_no_added_filter.
- Docs: default posture (egress/write/exec block), --confine-reads for path
  confinement, and the GPU-unsandboxed + directory-granularity caveats.

There is no live-server confinement integration test: the HTTP API exposes
no arbitrary-read primitive (httplib rejects static symlink/traversal, media
uses fs_validate_filename), so unveil() enforcement is proven at the syscall
level in tests/sandbox_test.c instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- standalone llama-server: consume --confine-reads too (sibling of --unsecure),
  so it doesn't reach common_params_parse() and error out.
- warn when --confine-reads is passed to a non-server mode, where it's a no-op,
  instead of silently ignoring it.
- show --confine-reads (and --unsecure) in `--server --help`, which previously
  listed none of llamafile's own server flags.
- capture errno before common_log_resume() on the pledge-failed path so the
  error message can't print a stale errno.
- fix a stale comment on llamafile_sandbox_apply().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bring back pledge/SECCOMP sandboxing

1 participant