Bring back pledge/SECCOMP sandboxing (#930)#1015
Open
aittalam wants to merge 8 commits into
Open
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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'spledge()(SECCOMP) andunveil()(Landlock); it's a logged no-op on macOS/Windows/older kernels.Two levels
Default —
pledge(), on automatically. The server runsstdio anet rpath: it canaccept()connections but neverconnect()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--pathroot). 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
anet→inetwhen 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/--chatalways pledge and have no network at all.Tests & docs
tests/sandbox_test.c(runs inmake check): syscall-level allow/deny per promise set, plusunveil()enforcement.tests/integration -m sandbox: every server thread carries the filter, inference works sandboxed, a bundled/zip/llamafile loads,--confine-readsconfines while the default does not, and--unsecuredisables it.docs/security.mddocuments both levels and the GPU-unsandboxed caveat.🤖 Generated with Claude Code