detached d2968dd

fix(ipc): serialize socket bind and stale recovery with a sibling lock

commit
d2968ddeed90cf5cd8f9e6e0f7ba52b945862dda
author
brice · · 2026-09-23 21:37 UTC
parent
665fa7b
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +27 -0
M internal/adapters/ipc/listener.go +27 -0
@@ -76,6 +76,15 @@ func Listen(dir string, opts ...Option) (wire.Listener, error) {
7676 // changed, the bind is retried (and typically reports the racing owner live)
7777 // instead of removing it.
7878 func listenUnixStaleSafe(sockPath string) (*net.UnixListener, error) {
79+ // A socket file exists before its owner calls listen(2), and a dial in
80+ // that gap is refused exactly like a dial to a dead socket. Serialize
81+ // bind and stale recovery on a sibling lock so no contender can mistake
82+ // another's half-bound socket for a stale one and unlink it.
83+ unlock, err := lockSocketPath(sockPath)
84+ if err != nil {
85+ return nil, err
86+ }
87+ defer unlock()
7988 for attempt := 0; attempt < staleRecoveryAttempts; attempt++ {
8089 ln, err := bindUnix(sockPath)
8190 if err == nil {
@@ -126,6 +135,24 @@ func listenUnixStaleSafe(sockPath string) (*net.UnixListener, error) {
126135 return nil, ErrDaemonRunning
127136 }
128137
138+// lockSocketPath takes an exclusive flock on sockPath + ".lock" and returns
139+// its release. The lock file is owner-only and never removed, so every
140+// contender locks the same inode.
141+func lockSocketPath(sockPath string) (func(), error) {
142+ file, err := os.OpenFile(sockPath+".lock", os.O_CREATE|os.O_RDWR|syscall.O_NOFOLLOW|syscall.O_CLOEXEC, 0o600)
143+ if err != nil {
144+ return nil, fmt.Errorf("ipc: opening socket lock for %s: %w", sockPath, err)
145+ }
146+ if err := syscall.Flock(int(file.Fd()), syscall.LOCK_EX); err != nil {
147+ _ = file.Close()
148+ return nil, fmt.Errorf("ipc: locking socket %s: %w", sockPath, err)
149+ }
150+ return func() {
151+ _ = syscall.Flock(int(file.Fd()), syscall.LOCK_UN)
152+ _ = file.Close()
153+ }, nil
154+}
155+
129156 // bindUnix binds and listens on sockPath.
130157 func bindUnix(sockPath string) (*net.UnixListener, error) {
131158 addr, err := net.ResolveUnixAddr("unix", sockPath)
@@ -76,6 +76,15 @@ func Listen(dir string, opts ...Option) (wire.Listener, error) {
76 // changed, the bind is retried (and typically reports the racing owner live)76 // changed, the bind is retried (and typically reports the racing owner live)
77 // instead of removing it.77 // instead of removing it.
78 func listenUnixStaleSafe(sockPath string) (*net.UnixListener, error) {78 func listenUnixStaleSafe(sockPath string) (*net.UnixListener, error) {
79+ // A socket file exists before its owner calls listen(2), and a dial in
80+ // that gap is refused exactly like a dial to a dead socket. Serialize
81+ // bind and stale recovery on a sibling lock so no contender can mistake
82+ // another's half-bound socket for a stale one and unlink it.
83+ unlock, err := lockSocketPath(sockPath)
84+ if err != nil {
85+ return nil, err
86+ }
87+ defer unlock()
79 for attempt := 0; attempt < staleRecoveryAttempts; attempt++ {88 for attempt := 0; attempt < staleRecoveryAttempts; attempt++ {
80 ln, err := bindUnix(sockPath)89 ln, err := bindUnix(sockPath)
81 if err == nil {90 if err == nil {
@@ -126,6 +135,24 @@ func listenUnixStaleSafe(sockPath string) (*net.UnixListener, error) {
126 return nil, ErrDaemonRunning135 return nil, ErrDaemonRunning
127 }136 }
128 137
138+// lockSocketPath takes an exclusive flock on sockPath + ".lock" and returns
139+// its release. The lock file is owner-only and never removed, so every
140+// contender locks the same inode.
141+func lockSocketPath(sockPath string) (func(), error) {
142+ file, err := os.OpenFile(sockPath+".lock", os.O_CREATE|os.O_RDWR|syscall.O_NOFOLLOW|syscall.O_CLOEXEC, 0o600)
143+ if err != nil {
144+ return nil, fmt.Errorf("ipc: opening socket lock for %s: %w", sockPath, err)
145+ }
146+ if err := syscall.Flock(int(file.Fd()), syscall.LOCK_EX); err != nil {
147+ _ = file.Close()
148+ return nil, fmt.Errorf("ipc: locking socket %s: %w", sockPath, err)
149+ }
150+ return func() {
151+ _ = syscall.Flock(int(file.Fd()), syscall.LOCK_UN)
152+ _ = file.Close()
153+ }, nil
154+}
155+
129 // bindUnix binds and listens on sockPath.156 // bindUnix binds and listens on sockPath.
130 func bindUnix(sockPath string) (*net.UnixListener, error) {157 func bindUnix(sockPath string) (*net.UnixListener, error) {
131 addr, err := net.ResolveUnixAddr("unix", sockPath)158 addr, err := net.ResolveUnixAddr("unix", sockPath)

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close