Skip to content

server: --unix-socket listens on an owner-only Unix socket instead of TCP - #2389

Open
kotevcode wants to merge 3 commits into
ashhart:mainfrom
kotevcode:server-unix-socket
Open

kotevcode wants to merge 3 commits into
ashhart:mainfrom
kotevcode:server-unix-socket

Conversation

@kotevcode

Copy link
Copy Markdown

What this changes

Adds tensorfold serve --unix-socket PATH. Instead of a TCP port, the server listens on a new Unix stream socket, mode 0600, and removes it on shutdown. An existing PATH is refused, not replaced. Without the flag, TCP behaves as before.

Why: a local app that embeds the engine can reach it without its requests crossing a network interface, where anyone with capture rights on the machine can read loopback traffic. The caller I have in mind is the Antseed Mac app, a menubar worker that bundles TensorFold and serves end-to-end encrypted requests. It decrypts each request and hands it to the engine, and it can't currently do that without the plaintext crossing loopback. Its integration with this flag isn't written yet; it would also check the peer's PID on each connection, so nothing else can stand in at the path.

Checks on this head

  • Head commit: 91e0593
  • Lean checker, python3 tools/zig/lean_check.py, on the base and on this head: 210 on the base, 210 on this head, 0 new (lean_new.py). All 210 are inherited.
  • Tests run, with the command and pass, fail and skip counts:
    • zig build test test-golden test-server-security -Dcpu=apple_m1 --summary all: 2555 of 2575 tests passed, 20 skipped, 0 failed. Those counts leave out steps cached from earlier runs. 650 of 724 steps succeeded and 36 failed. No compile error appears outside the .metal kernels; see "Not run". test-golden and test-server-security passed.
    • New test a Unix socket listener is owner-only, accepts, refuses an existing path and removes its path. It could not compile before the change. With chmod planted to 0o644 it fails with expected 384, found 420.
    • fake_serve serve zig/tests/server/fixtures --unix-socket PATH, driven with curl --unix-socket: /v1/models and /v1/chat/completions answered, the socket was srw-------, the process held no TCP sockets, a second server on the same path exited with BindFailed, and the path was gone after SIGTERM.
    • zig fmt --check build.zig zig and git diff --check pass.
    • zig build test-golden -Dfixed-cpu with -Dtarget=x86_64-linux-gnu and -Dtarget=x86_64-windows-gnu showed no compile errors in a filtered log. This is a compile check only; nothing ran on those targets.
  • CI: not yet run on this head.
  • Not run, and why:
    • The native binary and every Metal test, on a real checkpoint or otherwise. My only build machine with Xcode has Xcode 26.3 on macOS 26.2, whose Metal compiler rejects uint4b_format in zig/kernels/metal/nemotron/. That accounts for the 36 failed steps above, and I did not run them on another toolchain.
    • zig build dev-tools, since no API the tools call changed.
    • Windows at runtime. The flag returns error.Unsupported there and that path is not exercised.
    • Linux at runtime.

Receipt

n/a: this cannot reach inference. It changes how connections are accepted, and only when the flag is set. With the flag unset, the only additions on the TCP path are two null checks. No request handling, engine or kernel code changes, so there are no speed or exactness numbers.

Checklist

  • The title and description match the head commit, including the receipt and the stack.
  • New tests fail before the change, pass after it, and skip cleanly without their dependency.
  • Comments and docstrings are one line. No measurements or history in the source.
  • No personal data, machine names, internal hosts or local paths in the diff or its commits. No AI attribution lines.

The inference checklist is skipped because the Receipt is n/a.

@ashhart

ashhart commented Nov 6, 2026

Copy link
Copy Markdown
Owner

Thanks @kotevcode. The use case is a good one, and the default path is untouched: openTcp is main's code moved into a function. Setting the mode before listen is right, since nothing can connect until then. A few things before this lands, mostly so "owner-only" holds by itself:

  1. Check the peer. The guarantee rests on the socket file's mode. Linux enforces that at connect, but POSIX leaves it unspecified. Could the server read each Unix peer's credentials (SO_PEERCRED on Linux, getpeereid on macOS) and close the connection unless the peer's effective UID is the server's? Please test it with a second user on both systems.
  2. The chmod acts on the path. Between bind and chmod, anyone who can write to the parent directory could swap the name. Could you refuse a parent that others can write to (unless it's sticky), or bind in a fresh 0700 directory and rename the socket into place? On shutdown, please unlink only if the path is still the socket you bound (same st_dev and st_ino).
  3. TCP flags. Please refuse --host and --port alongside --unix-socket. Today they're ignored, and --host 0.0.0.0 prints the "non-loopback server has no API key" warning. On Windows, please refuse the flag at parse time, not after the model loads.
  4. The startup line still prints serving … at http://127.0.0.1:0/v1. Could it name the socket instead?
  5. Say plainly that keys still apply. API keys, --metrics-open and the Host check work over the socket as on TCP: a client must send a loopback Host or be in --allowed-host. Please say so in the README row, and that the socket's permissions are a second layer, not a substitute. A test that a keyed server answers 401 and 200 over curl --unix-socket would pin it.
  6. An existing path: please name the cause ("PATH already exists; if no server is using it, remove it") rather than BindFailed.
  7. The CHANGELOG line should end "(server: --unix-socket listens on an owner-only Unix socket instead of TCP #2389). Thanks to @kotevcode.", under the current Unreleased.

@kotevcode

Copy link
Copy Markdown
Author

Thanks @ashhart. Rebased on main; all seven points are in c50fe93 and c7c91e2:

  1. Peer check. accept reads each Unix peer's user (SO_PEERCRED on Linux, getpeereid on macOS) and closes the connection unless it is the server's effective UID, logging refused a Unix socket connection from another user.
  2. Path swap. openUnix refuses a parent directory that group or others can write to unless it is sticky (/tmp is allowed). On shutdown the path is unlinked only while its st_dev/st_ino match the socket that was bound; a test replaces the socket with a file and checks that the file survives.
  3. TCP flags. --host or --port with --unix-socket is a usage error (argument --unix-socket: not allowed with --host), and Windows refuses the flag at parse time.
  4. Startup line. serving NAME on Unix socket PATH, /v1 (...); the separate "listening on" line is gone.
  5. Keys still apply. The README row says so, and that the socket's permissions are a second layer. A new test in auth_scope_http_test.zig runs a keyed server over a real Unix socket: 401 with no key or a wrong key, 200 with a valid key, 401 on /metrics, and 403 for a non-loopback Host.
  6. Existing path. --unix-socket PATH already exists; if no server is using it, remove it.
  7. CHANGELOG. Under the current Unreleased, ending "(server: --unix-socket listens on an owner-only Unix socket instead of TCP #2389). Thanks to @kotevcode."

Second-user test, with the CPU fake server (zig build server-cpu) on a socket in a 0711 directory. The second user is nobody on macOS and a second account on Linux:

macOS 26.2, M1 Max Linux 6.12, x86_64 (Debian 12)
owner 200 200
second user, socket 0600 refused at connect (curl 7) refused at connect (curl 7)
second user, socket chmod 0777 (peer check alone) closed by the server (curl 52), refusal logged reset by the server (curl 56), refusal logged
owner again 200 200
socket after SIGINT removed removed

Checks on c7c91e2: the listener, Unix-socket CLI and auth HTTP tests pass on macOS (29/29); the server tests compile for x86_64 and aarch64 Linux (glibc 2.31) and x86_64 Windows; tools/pr_check.py --pr 2389 reports 0 errors and 0 new lean findings.

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.

2 participants