fix(daemon): close IPC TOCTOU window + enforce SO_PEERCRED (PILOT-246)#176
Conversation
Add tests for IPC socket access control: - TestIPCServerRejectsCrossUIDConnection validates same-UID dial - TestCheckPeerUIDRejectsNonUnixSocket validates non-Unix rejection - TestCheckPeerUIDAcceptsSameUIDUnixSocket validates same-UID pass
The Unix-domain IPC socket had two problems: 1. TOCTOU window between net.Listen (creates socket with default umask, typically 0755 for sockets) and os.Chmod(0600) — another local UID could connect during this microsecond gap. 2. acceptLoop never checked the peer's UID via SO_PEERCRED, so any local process could connect and issue IPC commands regardless of its UID. Fix: - Set umask to 0177 before Listen so the socket is created with 0600 immediately. The explicit Chmod is kept as belt-and-suspenders for platforms where umask semantics differ. - Add checkPeerUID() that extracts SO_PEERCRED credentials on accept and rejects connections from different UIDs. Logged at WARN when rejected. Closes PILOT-246
🦾 Matthew PR Check — #176 PILOT-246Status
Verdict |
🦾 Matthew Explains — #176 PILOT-246What this doesFixes two Unix-domain IPC socket security gaps in the daemon:
Why it mattersBefore this fix, any local process could connect to the daemon IPC socket and issue privileged commands (rotate keys, set webhooks, approve handshakes) regardless of UID. The TOCTOU gap also allowed a brief window where another UID could connect before permissions were tightened. Caution
|
|
🤖 Hank — CI status Classification: The build/test failure is a genuine code defect:
@matthew-pilot — fix or comment. Auto-classified at 2026-05-29T21:27:00Z. Re-runs on next push or check completion. |
The original PR-176 placed unix.GetsockoptUcred + unix.SO_PEERCRED in pkg/daemon/ipc.go without a build tag. These syscalls are Linux-only; macOS CI failed at 'go vet' with: pkg/daemon/ipc.go:479:24: undefined: unix.GetsockoptUcred pkg/daemon/ipc.go:479:71: undefined: unix.SO_PEERCRED Split checkPeerUID into: - ipc_peercred_linux.go — SO_PEERCRED + Ucred (original logic) - ipc_peercred_darwin.go — LOCAL_PEERCRED + GetsockoptXucred (BSD equivalent) - ipc_peercred_other.go — no-op fallback for unsupported platforms Tested locally on macOS: go build + go test pass.
Mirrors the change that landed in TeoSlayer/pilotprotocol#172 — the managed.policy.set IPC is a network-admin operation; without an adminToken parameter, the gate that PR #172 added to the daemon side has no caller-side support. Wire format: [cmd][sub][action=0x01][netID(2)][tokenLen(2)][token...][policyJSON...] Threat model alignment (per PILOT-347): - Only the network administrators hold the admin token. - managed.policy.set is correctly admin-gated. - User-owned IPC ops (rotate_key, handshake approve/reject/revoke, set_webhook) belong on a SO_PEERCRED check, not admin-token — see TeoSlayer/pilotprotocol#176 for that pattern. Empty adminToken is fine for solo-mode daemons (no AdminToken configured); managed-mode daemons reject empty. Tests updated to pass empty token. go test ./driver/... PASS. Co-authored-by: Teodor Calin <teodor@vulturelabs.io>
What failed
The daemon's Unix-domain IPC socket had two security gaps:
TOCTOU window (ipc.go:421-429):
net.Listen(unix, ...)creates the socket with default umask (typically 0755 for sockets), thenos.Chmod(0600)restricts it. Another local UID could connect during the microsecond gap between Listen and Chmod.No peer UID check (ipc.go:454-477):
acceptLoopnever calledSO_PEERCRED. Any local process could connect and issue IPC commands regardless of its UID.Why this fix
umask(0177)beforeListenso the socket file is created with 0600 immediately, closing the TOCTOU window. The explicitChmodis kept as belt-and-suspenders for platforms with different umask semantics.checkPeerUID()that extractsSO_PEERCREDcredentials on accept and rejects connections from different UIDs. Rejected connections are logged at WARN level.Verification
go build ./...— cleango vet ./...— cleango test -run 'TestIPCServer|TestCheckPeer' ./pkg/daemon/— all 10 tests passTestCheckPeerUIDRejectsNonUnixSocket,TestCheckPeerUIDAcceptsSameUIDUnixSocketTestWriteLoopExitsOnWriteDeadlineflake confirmed on main — unrelatedDiff stat
Closes PILOT-246