Skip to content

Commit e75a8b9

Browse files
fix: only unlink the command socket while holding the lock
openComms() logs an error but carries on to bind() when it cannot open or flock() the .lock file, so a zms can be serving without holding the lock. In that state the unlink in closeComms() could remove a socket belonging to the zms that does hold the lock, leaving it unreachable. Guard the unlink on lock_fd >= 0. Leaking the socket file when we never held the lock is no worse than the behaviour before the unlink was added. Expand the comment to spell out that this path is the command socket rather than the .lock file, that our successor unlinks it itself on the way to bind(), and why the unlink has to precede releasing the lock. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent c93b8d7 commit e75a8b9

1 file changed

Lines changed: 22 additions & 9 deletions

File tree

src/zm_stream.cpp

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -413,16 +413,29 @@ void StreamBase::closeComms() {
413413
close(sd);
414414
sd = -1;
415415
}
416-
// Remove our command socket while we still hold the lock. Another zms with
417-
// the same connkey blocks on flock(LOCK_EX) in openComms() before it
418-
// unlinks and binds, so it cannot have created its own socket yet and we
419-
// cannot be deleting a file that belongs to it.
416+
// Remove our command socket, but only while we still hold the lock, and only
417+
// if we actually got the lock.
420418
//
421-
// This has to happen: web/ajax/stream.php uses file_exists() on this path
422-
// to decide whether zms is listening. A socket left behind by an exited zms
423-
// makes that check succeed, so the sendto() fails with ECONNREFUSED instead
424-
// of waiting for the new process to bind, and the command is lost.
425-
if ( loc_sock_path[0] && (unlink(loc_sock_path) < 0) && (errno != ENOENT) ) {
419+
// Note this is zms-<connkey>s.sock, not the .lock file. A second zms sharing
420+
// this connkey is blocked in flock(LOCK_EX) on the .lock file, and the first
421+
// thing it does after winning that lock is unlink this very path itself
422+
// ("Unlink before bind" in openComms()). It never reads our socket file, so
423+
// removing it here only does early what our successor would do anyway.
424+
//
425+
// Ordering matters: if we unlinked after releasing the lock we could delete
426+
// a socket the successor had already bound, leaving it unreachable.
427+
//
428+
// The lock_fd guard matters because openComms() carries on and binds even
429+
// when it fails to take the lock, so without it a lockless zms could delete
430+
// a socket belonging to the zms that does hold the lock. Leaking the file in
431+
// that case is no worse than the behaviour before this check existed.
432+
//
433+
// This is worth doing because web/ajax/stream.php uses file_exists() on this
434+
// path to decide whether zms is listening. A socket left behind by an exited
435+
// zms makes that check succeed, so the sendto() fails with ECONNREFUSED
436+
// instead of waiting for the new process to bind, and the command is lost.
437+
if ( (lock_fd >= 0) && loc_sock_path[0]
438+
&& (unlink(loc_sock_path) < 0) && (errno != ENOENT) ) {
426439
Warning("Failed to unlink '%s': %s", loc_sock_path, strerror(errno));
427440
}
428441
// Don't unlink sock_path_lock: another zms may already be waiting on it.

0 commit comments

Comments
 (0)