Files
zip/internal/sock/sock.go
T
zeekayandHanzo Dev 5112db48a9 a socket path that cannot bind says so, instead of dying as EINVAL
sun_path is a fixed field holding a terminated string, so a unix address has
103 usable bytes on Darwin and 107 on Linux. Over that the kernel refuses the
bind with EINVAL, which surfaces as "invalid argument" and names neither the
length nor the path — the plugin child exits 1 and the host reports "exited
before listening", which reads like a timing bug and is not.

The limit now lives in internal/sock beside the path shape, since both are
properties of the same address: minting one is not enough, it also has to be
bindable. start() checks the longest address in play — the child's .sock.http
upgrade listener — and refuses to launch rather than watching it die.

103 is measured, not read off a header: on Darwin a 103-byte path binds and a
104-byte one does not. The first cut of this used 104 and let through a path
that was exactly one byte too long.

The private instance directory also stops repeating the plugin's name. The
binary and the socket inside it are already named for the plugin, so the copy
in the directory spent len(name)+1 of a 103-byte budget saying it twice, and
that was what pushed a long name over.

Four idle tests and the status test built their Plugin.Dir from t.TempDir,
which spends 89 bytes on the test's own name; sockDir has existed for exactly
this reason. shortDir was a third copy of sockDir in the same test package,
written from a comment that blamed sockDir for t.TempDir's length.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-21 01:02:30 -07:00

79 lines
3.2 KiB
Go

// Package sock is the fleet's socket-path scheme, in one place.
//
// A service NAME maps to exactly one path — <Dir()>/<name>.sock — and both
// halves of every connection use this mapping, which is what keeps them from
// drifting: a service serves at that path and a caller dials it by name, with no
// registry and no discovery service in between.
//
// It lives down here, below zip, because zip is not the only caller any more.
// zip.RuntimeDir and zip.SocketPath are the public spelling of it, and the
// telemetry exporter needs the same answer without importing zip — which it
// cannot do, since zip imports the exporter. A second copy of the rule would be
// a fleet where a program serves at one path and its telemetry looks for another.
package sock
import (
"fmt"
"os"
"path/filepath"
)
// DirEnv names the directory holding one socket per service. It is the one knob
// in the scheme.
const DirEnv = "ZIP_RUNTIME_DIR"
// Dir is the directory that holds the fleet's sockets, resolved in one order:
//
// $ZIP_RUNTIME_DIR set explicitly — always wins
// $XDG_RUNTIME_DIR/zip a developer's per-user runtime dir (/run/user/1000)
// /run/zip the system default
//
// The middle case is what makes a dev box work without configuration: /run/zip
// is not writable by a normal user and $XDG_RUNTIME_DIR is.
//
// The environment is read on every call rather than cached, so a process that
// arranges its own runtime directory after start — and a test that does — sees
// the directory it actually has.
func Dir() string {
if d := os.Getenv(DirEnv); d != "" {
return d
}
if d := os.Getenv("XDG_RUNTIME_DIR"); d != "" {
return filepath.Join(d, "zip")
}
return "/run/zip"
}
// Path is the socket a service of this name is reached at.
func Path(name string) string { return In(Dir(), name) }
// In is the shape of every socket path, stated once: the plugin loader names a
// child's private socket with it too, so "a service's socket is
// <dir>/<name>.sock" has one definition.
func In(dir, name string) string { return filepath.Join(dir, name+".sock") }
// Max is how long a socket path may be. sun_path is a fixed-size field inside
// the address struct — 104 bytes on Darwin, 108 on Linux — and it holds a
// TERMINATED string, so the usable length is one less than the field: 103 and
// 107. The smaller is the one number here, so an address that binds on Darwin
// binds on Linux.
//
// Measured, not read off a header: on Darwin a 103-byte path binds and a
// 104-byte path fails. Off by one here is not a rounding error — it is a
// plugin that starts everywhere except the one machine whose temp directory is
// a byte longer.
const Max = 103
// Fits reports whether a path is short enough to bind, naming the overrun when
// it is not. The kernel's own answer is EINVAL, which arrives as "invalid
// argument" and mentions neither the length nor the path — so the first guess
// is always permissions, and the directory looks fine, and the real cause is a
// deep parent directory nobody thought to count.
func Fits(path string) error {
if len(path) <= Max {
return nil
}
return fmt.Errorf("socket path is %d bytes, %d over the %d-byte limit: %s",
len(path), len(path)-Max, Max, path)
}