Skip to content

Install the Unix-socket filter through prctl so it works under the service's own unit (v0.55.1) - #199

Merged
mikeathan merged 2 commits into
mainfrom
fix-unix-socket-filter-under-the-unit
Nov 6, 2026
Merged

mikeathan merged 2 commits into
mainfrom
fix-unix-socket-filter-under-the-unit

Conversation

@mikeathan

Copy link
Copy Markdown
Owner

What broke

v0.55.0 (#198) shipped broken. On vertex the nightly audit failed with
nmap failed: exit status 1: sandbox-runner: unix socket denial failed: seccomp: install the unix socket filter: operation not permitted.
The runner failing closed did its job (nmap never ran unconfined), but every jailed child goes through the same runner, so agent terminal commands and automations that use the shell were failing too, not only the audit.

Cause

The filter was installed with the seccomp(2) syscall. The service's unit allows SystemCallFilter=@system-service landlock_create_ruleset landlock_add_rule landlock_restrict_self, and systemd keeps seccomp(2) in @sandbox, which the unit does not list, so under the real unit it answers EPERM. No test environment had that filter (CI runners, and a Docker VM on the production kernel), so everything was green. I called the Ubuntu CI jobs "the amd64 evidence"; that was the wrong gate for this kind of change.

Fix

  • The filter is installed with prctl(PR_SET_SECCOMP), which the unit allows, on a thread the runner pins and execs from (Landlock is per-thread too, so the pin helps it as well). The failure message now names sandboxing.unix_sockets: true as the way to run without the filter.
  • TestUnixSocketDeny_InstallsUnderTheServiceUnitsOwnSyscallFilter refuses seccomp(2) before installing, as the unit does. It reproduced the exact vertex error before the fix and passes after it. It stands in for the one syscall known to differ; it is not the whole unit.
  • scripts/check-sandbox-under-unit.sh (+ scripts/sandbox-probe.py) is the deploy gate: it builds its systemd-run properties from the installed unit, runs a probe as the service user, and prints whether the filter installs by seccomp(2) and by prctl, and what it refuses. Pitfall 68 and the setup notes say to run it before deploying any sandbox change. The probe's own logic was checked in the Docker VM, plain and with seccomp(2) refused first (old route EPERM, new route ok).

Not verified

Nothing here has run under vertex's real unit. Do not deploy before running the probe on vertex (sudo scripts/check-sandbox-under-unit.sh): if prctl is also refused there, this explanation is wrong and the narrowest alternative is adding seccomp to the unit's SystemCallFilter next to the three Landlock calls, which is a unit change for the owner to decide.

Restoring vertex right now (before this is deployed)

Turn on Settings → Security → "Let programs use Unix sockets", or set sandboxing.unix_sockets: true in /var/lib/llm-proxy/settings.yml, then sudo systemctl restart llm-proxy. Turn it back off once the fix is confirmed, or the protection stays off unnoticed.

Checked

Backend build, vet, full tests, -race on the sandbox and progexec packages, complexity gate and harness pass; the sandbox tests, including the conformance test, pass on a real kernel (6.8, arm64) in a Docker VM.

🤖 Generated with Claude Code

mikeathan and others added 2 commits November 6, 2026 15:23
…rvice's own unit (v0.55.1)

v0.55.0 passed every test and failed on its first real run: the service unit's
SystemCallFilter does not allow the seccomp(2) syscall (systemd keeps it in
@sandbox), so installing the filter answered operation not permitted, nmap
failed closed and the nightly audit died. The filter is now installed with
prctl(PR_SET_SECCOMP), which the unit allows, on a thread the runner pins and
execs from. A test that refuses seccomp(2) before installing reproduced the exact
error and now passes. The failure message names sandboxing.unix_sockets as the
way to run without the filter.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…dening

A confinement change has to be tried where the service really runs: a container
or CI runner has no copy of the unit's SystemCallFilter, which is how the
Unix-socket filter passed every test and still failed on vertex. The script builds
its systemd-run properties from the installed unit (or the repo template when
none is installed), runs a small python probe as the service user, and prints
whether the filter installs by seccomp(2) and by prctl and what it refuses. The
probe's own logic was checked in the Docker VM, plain and with seccomp(2)
refused first.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown

📊 Test coverage and CRAP scores

Backend (Go)

Go coverage

Total: 81.0%

Packages below 50% (statement coverage)
  0.0%  llm-proxy
  0.0%  llm-proxy/internal/platform/db
  0.0%  llm-proxy/internal/platform/env
  0.0%  llm-proxy/internal/testing/assistanttest
  0.0%  llm-proxy/internal/testing/mocks
  0.0%  llm-proxy/internal/testing/utils
  0.0%  llm-proxy/tools/check-complexity
  0.0%  llm-proxy/tools/gen-oui
 26.2%  llm-proxy/internal/core/mcp
 29.7%  llm-proxy/internal/platform/process
 46.2%  llm-proxy/internal/core/llm/metadata

Frontend (Vitest)

Total: 86.83% statements, 83.56% branches, 86.05% functions, 87.66% lines

CRAP scores

Report only. Scores above 30 are flagged; lower is better.

Go

4764 functions scored; 55 above 30; highest score 870.00.

Highest 10 flagged functions:

Function Location Complexity Coverage CRAP
Stream llm-proxy/internal/app/playback_bridge.go:183 29 0.0% 870.00
main llm-proxy/tools/check-complexity/main.go:108 22 0.0% 506.00
executeShell llm-proxy/internal/core/tools/terminal.go:550 17 0.0% 306.00
buildGPUProvider llm-proxy/internal/platform/metrics/gpu_providers.go:22 28 30.4% 292.33
DeleteRunByID llm-proxy/internal/platform/persistence/workspace.go:507 14 0.0% 210.00
EffectiveEndpointLocked llm-proxy/internal/core/llm/providers/registrar.go:266 13 0.0% 182.00
AdminProviderKeysPutHandler llm-proxy/internal/transport/http/handlers/secrets_handlers.go:38 13 0.0% 182.00
listByBinary llm-proxy/internal/platform/process/process_darwin.go:13 10 0.0% 110.00
Kill llm-proxy/internal/platform/process/process.go:55 10 0.0% 110.00
applyLandlock llm-proxy/internal/platform/sandbox/landlock_linux.go:193 10 0.0% 110.00

Missing coverage is scored as 0%. Full reports are in the crap-reports artifact.

TypeScript

1833 functions scored; 8 above 30; highest score 146.29.

Highest 8 flagged functions:

Function Location Complexity Coverage CRAP
<anonymous@110:68> src/utils/message/turnGrouper.ts:110 23 38.5% 146.29
verifyWebhook src/composables/useWebhook.ts:69 8 0.0% 72.00
updateWasNearBottom src/composables/ui/useAutoScroll.ts:89 7 0.0% 56.00
handleEvent src/utils/message/messageBuilder.ts:309 38 79.1% 51.18
deleteSessionsByIds src/composables/assistant/useAssistant.ts:482 7 7.1% 46.24
routeShows src/composables/assistant/useRunNotifications.ts:201 6 0.0% 42.00
isLocalEndpoint src/utils/model/modelUtils.ts:19 7 20.0% 32.09
sendMessage src/composables/assistant/useAssistant.ts:343 22 73.7% 30.82

Missing coverage is scored as 0%. Full reports are in the crap-reports artifact.

Full coverage and CRAP reports in the artifacts of this workflow run

@mikeathan
mikeathan merged commit 55466a3 into main Nov 6, 2026
11 checks passed
@mikeathan
mikeathan deleted the fix-unix-socket-filter-under-the-unit branch November 6, 2026 15:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant