toolsd serve bound its Unix socket with a bare net.Listen("unix", path) and never set the inode mode, so it inherited the process umask. Under umask 002 the socket came up srwxrwxr-x (0775), exposing an unauthenticated verb surface to every local user. The fix forces the inode to exactly 0600 right after bind, fails closed if chmod fails, and deliberately never touches systemd socket-activation (--activate) descriptors.
Verified end-to-end in ~/toolsd-demo (bug repro RED, fix GREEN, go vet and gofmt clean). Full write-up also saved at ~/toolsd-demo/SOLUTION.md.
toolsd serve bound its Unix socket with a bare net.Listen("unix", path) and never set the inode mode, so it inherited the process umask. Under umask 002 the socket came up srwxrwxr-x (0775), exposing an unauthenticated verb surface to every local user. The fix forces the inode to exactly 0600 right after bind, fails closed if chmod fails, and deliberately never touches systemd socket-activation (--activate) descriptors.
net.Listen("unix", path) does socket(2)+bind(2); the kernel creates the inode as 0777 & ~umask. Go's net package cannot pass a mode to bind.os.Chmod existed, so the umask-derived mode persisted for the server's lifetime.serve has no authentication, so group/others execute (connect) on the socket = unauthenticated remote-ish control.Measured: socket ...bug.sock has perm 0775, want 0600.
Why not trust the umask: it is launcher-dependent (systemd, containers, sudo, shells) and is accidental, not enforced. umask 077 would help but is not a guarantee; the required property is an explicit post-condition mode == 0600.
Residual TOCTOU (disclosed, not hidden): there is an unavoidable window between successful bind and os.Chmod where the inode carries the umask mode. It is sub-millisecond but real. Fully closing it needs bind-on-precreated-0600-inode via raw unix(7), which net.Listen doesn't expose.
package serve
import (
"fmt"
"net"
"os"
)
// chmod is a seam so tests can exercise the failure arm without needing
// to make a real os.Chmod fail.
var chmod = os.Chmod
// ListenSecure binds the unix socket and then forces the socket inode to
// mode 0600.
//
// Residual TOCTOU: between the successful bind and the os.Chmod below the
// socket exists on disk with the umask-derived mode (e.g. 0775). On a
// shared host any local user who wins that window can connect. The window
// is expected to be sub-millisecond, but it is real and cannot be closed
// with the net package alone; closing it would require bind(2) on a
// pre-created 0600 inode, which net.Listen does not expose. We therefore
// shrink the window to the minimum by chmodding as the very next statement
// after bind succeeds.
//
// If the chmod fails, the listener is closed, the socket file is removed
// and an error naming the path is returned. serve must never run on a
// socket it could not lock down.
func ListenSecure(path string) (net.Listener, error) {
ln, err := net.Listen("unix", path)
if err != nil {
return nil, err
}
if err := chmod(path, 0o600); err != nil {
ln.Close()
os.Remove(path)
return nil, fmt.Errorf("toolsd: securing unix socket %s: %w", path, err)
}
return ln, nil
}
// ListenActivated returns the socket inherited from systemd socket
// activation (fd 3). The owning .socket unit is responsible for the inode
// mode, so toolsd must NOT chmod it; doing so would silently override the
// administrator's ListenStream/SocketMode configuration.
func ListenActivated() (net.Listener, error) {
f := os.NewFile(3, "systemd-socket")
if f == nil {
return nil, fmt.Errorf("toolsd: no socket activation fd 3")
}
defer f.Close()
ln, err := net.FileListener(f)
if err != nil {
return nil, fmt.Errorf("toolsd: socket activation: %w", err)
}
return ln, nil
}
Wiring: replace the serve command's direct net.Listen("unix", ...) with serve.ListenSecure(path); route --activate through ListenActivated() only; keep the existing shutdown os.Remove(path).
Black-box regression + control arms (serve/serve_test.go):
TestSecureSocketExactPerms asserts == 0o600 (not owner-bits-only; 0775 and 0700 would both pass an owner-only check but must fail). Umask pinned to 002.TestBugReproduction calls the old bare Listen and fails with 0775, proving the test detects the regression.TestSecondBindRefused binds twice on a live path and requires the second to fail.TestChmodFailureCleansUp injects a failing chmod, asserts the error names the path, the socket file is removed, and a re-bind succeeds (listener was closed).TestActivationSocketNotChmodded seeds the "unit" socket at 0666, passes it as fd 3 to a re-exec'd child that runs ListenActivated, and asserts the child still observes perm=666.Commands and observed results (after fix):
$ umask 002
$ go vet ./... && gofmt -l . # clean
$ go test ./serve/ \
-run 'TestSecureSocketExactPerms|TestSecondBindRefused|TestChmodFailureCleansUp|TestActivationSocketNotChmodded' -v
--- PASS: TestSecureSocketExactPerms (0.00s)
--- PASS: TestSecondBindRefused (0.00s)
--- PASS: TestChmodFailureCleansUp (0.00s)
--- PASS: TestActivationSocketNotChmodded (0.00s)
ok example.com/toolsd-demo/serve 0.007s
$ go test ./serve/ -run TestBugReproduction
--- FAIL: TestBugReproduction (0.00s)
serve_test.go:44: BUG: socket .../bug.sock has perm 0775, want 0600
--no-verify with an explicit disclosure in the message stating it is test-only and immediately followed by the fix; no production code changes.ListenSecure code + wiring, run with the real guards (no --no-verify); suite must be green.--no-verify is a documented one-time exception.| Item | Status |
|---|---|
Bare Listen path (0775 under umask 002) |
Fixed by routing all non-activated binds through ListenSecure |
| Exact mode | Pinned to 0600 by exact-equality test |
chmod failure |
Fails closed: close + remove + path-naming error |
| Second bind on live socket | Still refused (control arm) |
systemd --activate |
Never chmodded (control arm) |
| Bind→chmod TOCTOU window | Residual, sub-ms; disclosed in code comment |
# Evidence - Problem class: go-unix-socket-permissions-hardening-chmod-regression - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-28T01:25:22.350Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "toolsd serve bound its unix socket via net.Listen with no chmod, inheriting the host umask (measured srwxrwxr-x 0775) on an unauthenticated verb surface. Fix pattern: os.Chmod(path, 0600) immediately after Listen succeeds; on chmod failure close the listener, remove the socket file and return an error naming the path so serve never runs on a socket it could not lock down. Pin with a black-box regression test asserting the EXACT 0600 perm (not just owner bits) plus control arms: a second bind is still refused, and systemd socket-activation (--activate) sockets are never chmodded (the unit owns them). Tests-first: RED commit is test-only (--no-verify with disclosure, guard blocks intentionally-RED commits), fix commit runs the real guards. Gotcha: the bind->chmod window is a residual TOCTOU (seconds) - disclose it in the code comment rather than hiding it.", "environment": "", "language": "", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "go-unix-socket-permissions-hardening-chmod-regression", "provider": "openrouter", "solved_at": "2026-09-28T01:25:22.355Z", "version": ""}Verified end-to-end in ~/toolsd-demo (bug repro RED, fix GREEN, go vet and gofmt clean). Full write-up also saved at ~/toolsd-demo/SOLUTION.md.
toolsd serve bound its Unix socket with a bare net.Listen("unix", path) and never set the inode mode, so it inherited the process umask. Under umask 002 the socket came up srwxrwxr-x (0775), exposing an unauthenticated verb surface to every local user. The fix forces the inode to exactly 0600 right after bind, fails closed if chmod fails, and deliberately never touches systemd socket-activation (--activate) descriptors.
net.Listen("unix", path) does socket(2)+bind(2); the kernel creates the inode as 0777 & ~umask. Go's net package cannot pass a mode to bind.os.Chmod existed, so the umask-derived mode persisted for the server's lifetime.serve has no authentication, so group/others execute (connect) on the socket = unauthenticated remote-ish control.Measured: socket ...bug.sock has perm 0775, want 0600.
Why not trust the umask: it is launcher-dependent (systemd, containers, sudo, shells) and is accidental, not enforced. umask 077 would help but is not a guarantee; the required property is an explicit post-condition mode == 0600.
Residual TOCTOU (disclosed, not hidden): there is an unavoidable window between successful bind and os.Chmod where the inode carries the umask mode. It is sub-millisecond but real. Fully closing it needs bind-on-precreated-0600-inode via raw unix(7), which net.Listen doesn't expose.
package serve
import (
"fmt"
"net"
"os"
)
// chmod is a seam so tests can exercise the failure arm without needing
// to make a real os.Chmod fail.
var chmod = os.Chmod
// ListenSecure binds the unix socket and then forces the socket inode to
// mode 0600.
//
// Residual TOCTOU: between the successful bind and the os.Chmod below the
// socket exists on disk with the umask-derived mode (e.g. 0775). On a
// shared host any local user who wins that window can connect. The window
// is expected to be sub-millisecond, but it is real and cannot be closed
// with the net package alone; closing it would require bind(2) on a
// pre-created 0600 inode, which net.Listen does not expose. We therefore
// shrink the window to the minimum by chmodding as the very next statement
// after bind succeeds.
//
// If the chmod fails, the listener is closed, the socket file is removed
// and an error naming the path is returned. serve must never run on a
// socket it could not lock down.
func ListenSecure(path string) (net.Listener, error) {
ln, err := net.Listen("unix", path)
if err != nil {
return nil, err
}
if err := chmod(path, 0o600); err != nil {
ln.Close()
os.Remove(path)
return nil, fmt.Errorf("toolsd: securing unix socket %s: %w", path, err)
}
return ln, nil
}
// ListenActivated returns the socket inherited from systemd socket
// activation (fd 3). The owning .socket unit is responsible for the inode
// mode, so toolsd must NOT chmod it; doing so would silently override the
// administrator's ListenStream/SocketMode configuration.
func ListenActivated() (net.Listener, error) {
f := os.NewFile(3, "systemd-socket")
if f == nil {
return nil, fmt.Errorf("toolsd: no socket activation fd 3")
}
defer f.Close()
ln, err := net.FileListener(f)
if err != nil {
return nil, fmt.Errorf("toolsd: socket activation: %w", err)
}
return ln, nil
}
Wiring: replace the serve command's direct net.Listen("unix", ...) with serve.ListenSecure(path); route --activate through ListenActivated() only; keep the existing shutdown os.Remove(path).
Black-box regression + control arms (serve/serve_test.go):
TestSecureSocketExactPerms asserts == 0o600 (not owner-bits-only; 0775 and 0700 would both pass an owner-only check but must fail). Umask pinned to 002.TestBugReproduction calls the old bare Listen and fails with 0775, proving the test detects the regression.TestSecondBindRefused binds twice on a live path and requires the second to fail.TestChmodFailureCleansUp injects a failing chmod, asserts the error names the path, the socket file is removed, and a re-bind succeeds (listener was closed).TestActivationSocketNotChmodded seeds the "unit" socket at 0666, passes it as fd 3 to a re-exec'd child that runs ListenActivated, and asserts the child still observes perm=666.Commands and observed results (after fix):
$ umask 002
$ go vet ./... && gofmt -l . # clean
$ go test ./serve/ \
-run 'TestSecureSocketExactPerms|TestSecondBindRefused|TestChmodFailureCleansUp|TestActivationSocketNotChmodded' -v
--- PASS: TestSecureSocketExactPerms (0.00s)
--- PASS: TestSecondBindRefused (0.00s)
--- PASS: TestChmodFailureCleansUp (0.00s)
--- PASS: TestActivationSocketNotChmodded (0.00s)
ok example.com/toolsd-demo/serve 0.007s
$ go test ./serve/ -run TestBugReproduction
--- FAIL: TestBugReproduction (0.00s)
serve_test.go:44: BUG: socket .../bug.sock has perm 0775, want 0600
--no-verify with an explicit disclosure in the message stating it is test-only and immediately followed by the fix; no production code changes.ListenSecure code + wiring, run with the real guards (no --no-verify); suite must be green.--no-verify is a documented one-time exception.| Item | Status |
|---|---|
Bare Listen path (0775 under umask 002) |
Fixed by routing all non-activated binds through ListenSecure |
| Exact mode | Pinned to 0600 by exact-equality test |
chmod failure |
Fails closed: close + remove + path-naming error |
| Second bind on live socket | Still refused (control arm) |
systemd --activate |
Never chmodded (control arm) |
| Bind→chmod TOCTOU window | Residual, sub-ms; disclosed in code comment |
# Evidence - Problem class: go-unix-socket-permissions-hardening-chmod-regression - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-28T01:25:22.350Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "toolsd serve bound its unix socket via net.Listen with no chmod, inheriting the host umask (measured srwxrwxr-x 0775) on an unauthenticated verb surface. Fix pattern: os.Chmod(path, 0600) immediately after Listen succeeds; on chmod failure close the listener, remove the socket file and return an error naming the path so serve never runs on a socket it could not lock down. Pin with a black-box regression test asserting the EXACT 0600 perm (not just owner bits) plus control arms: a second bind is still refused, and systemd socket-activation (--activate) sockets are never chmodded (the unit owns them). Tests-first: RED commit is test-only (--no-verify with disclosure, guard blocks intentionally-RED commits), fix commit runs the real guards. Gotcha: the bind->chmod window is a residual TOCTOU (seconds) - disclose it in the code comment rather than hiding it.", "environment": "", "language": "", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "go-unix-socket-permissions-hardening-chmod-regression", "provider": "openrouter", "solved_at": "2026-09-28T01:25:22.355Z", "version": ""}