git: ce3750b5995c - main - unix: allow listening on an unbound socket, and binding after listen

From: Mark Johnston <markj_at_FreeBSD.org>
Date: Mon, 10 Aug 2026 17:40:38 UTC
The branch main has been updated by markj:

URL: https://cgit.FreeBSD.org/src/commit/?id=ce3750b5995c3c3e0376ae62a7c55d9a6e547bbc

commit ce3750b5995c3c3e0376ae62a7c55d9a6e547bbc
Author:     John Ericson <John.Ericson@Obsidian.Systems>
AuthorDate: 2026-08-10 15:04:33 +0000
Commit:     Mark Johnston <markj@FreeBSD.org>
CommitDate: 2026-08-10 17:31:22 +0000

    unix: allow listening on an unbound socket, and binding after listen
    
    `uipc_listen()` refused a socket that had not been bound, with
    `EDESTADDRREQ`.  That made sense while a pathname was the only way to
    name a peer: an unbound listener could never be reached, so allowing it
    would only have created sockets nothing could connect to.  Now that
    `connectat(2)` can name a peer socket by descriptor, an unbound listener
    *is* reachable, and the restriction only stands in the way.  It also left
    stream sockets oddly stricter than datagram ones, which could already
    reach an unbound peer.
    
    Dropping the check additionally permits `bind(2)` after `listen(2)`:
    `uipc_bindat()` already allows this, as it only rejects re-binding a
    socket that has a name.  That ordering closes a window listeners
    otherwise have to leave open.  Today the socket file must exist before
    the socket may listen, so a client connecting in between is refused;
    binding afterwards publishes the name only once the socket is ready to
    accept.
    
    `unix_seqpacket_test:listen_unbound` asserted the old behaviour, and is
    inverted accordingly.
    
    Signed-off-by: John Ericson <John.Ericson@Obsidian.Systems>
    Assisted-by: Claude Code (Claude Opus 4.8 and Fable 5)
    
    Reviewed by:    glebius, markj
    MFC after:      2 months
    Differential Revision:  https://reviews.freebsd.org/D58683
---
 share/man/man4/unix.4                |  17 +++++-
 sys/kern/uipc_usrreq.c               |   8 ++-
 tests/sys/kern/unix_connectat.c      | 110 +++++++++++++++++++++++++++++++++--
 tests/sys/kern/unix_seqpacket_test.c |   7 ++-
 4 files changed, 131 insertions(+), 11 deletions(-)

diff --git a/share/man/man4/unix.4 b/share/man/man4/unix.4
index 497b26ff49b5..807f7fe4410f 100644
--- a/share/man/man4/unix.4
+++ b/share/man/man4/unix.4
@@ -25,7 +25,7 @@
 .\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 .\" SUCH DAMAGE.
 .\"
-.Dd July 25, 2026
+.Dd August 5, 2026
 .Dt UNIX 4
 .Os
 .Sh NAME
@@ -86,6 +86,16 @@ and a subsequent call to
 Once the socket is bound to a file name, the permissions of the file can not be
 changed this way.
 .Pp
+Unlike with most other protocols, binding is not a prerequisite for
+.Xr listen 2 .
+An unbound socket may listen, in which case it can only be reached by
+descriptor, as described in
+.Sx Naming a peer
+below.
+A socket may equally be bound after it listens, which lets a listener be
+published only once it is ready to accept, rather than leaving a window in
+which the socket file exists but connections to it are refused.
+.Pp
 The length of
 .Ux Ns -domain
 address, required by
@@ -212,8 +222,9 @@ as with descriptor passing over
 the file system access-control and
 .Xr mac 4
 checks that apply to the pathname forms are not repeated.
-Because a descriptor alone suffices, a datagram socket may connect to an
-unbound peer, which no pathname could name.
+Because a descriptor alone suffices, a peer need never be bound at all: a
+datagram socket may connect to an unbound peer, and a listening socket may
+itself be unbound, neither of which a pathname could name.
 .Sh CONTROL MESSAGES
 The
 .Ux Ns -domain
diff --git a/sys/kern/uipc_usrreq.c b/sys/kern/uipc_usrreq.c
index 93add7494644..4a3bc90b0132 100644
--- a/sys/kern/uipc_usrreq.c
+++ b/sys/kern/uipc_usrreq.c
@@ -951,14 +951,18 @@ uipc_listen(struct socket *so, int backlog, struct thread *td)
 
 	/*
 	 * Synchronize with concurrent connection attempts.
+	 *
+	 * An unbound socket may listen: connectat(2) can name it by descriptor,
+	 * so it is reachable without a pathname.  It may also be bound
+	 * afterwards, which lets a listener be published only once it is ready
+	 * to accept, rather than leaving a window where the pathname exists but
+	 * connections are refused.
 	 */
 	error = 0;
 	unp = sotounpcb(so);
 	UNP_PCB_LOCK(unp);
 	if (unp->unp_conn != NULL || (unp->unp_flags & UNP_CONNECTING) != 0)
 		error = EINVAL;
-	else if (unp->unp_vnode == NULL)
-		error = EDESTADDRREQ;
 	if (error != 0) {
 		UNP_PCB_UNLOCK(unp);
 		return (error);
diff --git a/tests/sys/kern/unix_connectat.c b/tests/sys/kern/unix_connectat.c
index 2db42d20e0ff..9861287f5362 100644
--- a/tests/sys/kern/unix_connectat.c
+++ b/tests/sys/kern/unix_connectat.c
@@ -78,10 +78,7 @@ static const struct sockaddr_un empty_sun = {
 	.sun_len = offsetof(struct sockaddr_un, sun_path),
 };
 
-/*
- * Make a bound, listening stream socket.  Binding is not optional:
- * uipc_listen() refuses unbound sockets with EDESTADDRREQ.
- */
+/* Make a bound, listening stream socket. */
 static int
 mklistener(const char *path)
 {
@@ -230,6 +227,108 @@ ATF_TC_BODY(stream_bound, tc)
 	ATF_REQUIRE_EQ(0, close(l));
 }
 
+/*
+ * A socket may listen while unbound, and connectat(2) reaches it by
+ * descriptor: with no pathname there is nothing else that could name it.
+ * mklistener() cannot be used, as it binds first.
+ */
+ATF_TC_WITHOUT_HEAD(listen_unbound);
+ATF_TC_BODY(listen_unbound, tc)
+{
+	char buf[8];
+	int l, s, a;
+
+	ATF_REQUIRE((l = socket(PF_UNIX, SOCK_STREAM, 0)) >= 0);
+	ATF_REQUIRE_MSG(listen(l, 1) == 0, "listen: %s", strerror(errno));
+
+	ATF_REQUIRE((s = socket(PF_UNIX, SOCK_STREAM, 0)) >= 0);
+	ATF_REQUIRE_EQ(0, fdconnect(l, s));
+	ATF_REQUIRE((a = accept(l, NULL, NULL)) >= 0);
+
+	/* A real connection, not just an accepted descriptor. */
+	ATF_REQUIRE_EQ(5, write(s, "hello", 5));
+	ATF_REQUIRE_EQ(5, read(a, buf, sizeof(buf)));
+	ATF_REQUIRE_EQ(0, memcmp(buf, "hello", 5));
+
+	ATF_REQUIRE_EQ(0, close(a));
+	ATF_REQUIRE_EQ(0, close(s));
+	ATF_REQUIRE_EQ(0, close(l));
+}
+
+/*
+ * A socket may be bound after it listens, so a listener can be published only
+ * once it is ready to accept, rather than leaving a window in which the socket
+ * file exists but connections to it are refused.  The late-bound name behaves
+ * like any other.  mklistener() cannot be used: it binds first.
+ */
+ATF_TC_WITHOUT_HEAD(bind_after_listen);
+ATF_TC_BODY(bind_after_listen, tc)
+{
+	struct sockaddr_un sun = { .sun_family = AF_UNIX };
+	struct sockaddr_un peer;
+	socklen_t len;
+	int l, s, a;
+
+	ATF_REQUIRE((l = socket(PF_UNIX, SOCK_STREAM, 0)) >= 0);
+	ATF_REQUIRE_MSG(listen(l, 1) == 0, "listen: %s", strerror(errno));
+
+	strlcpy(sun.sun_path, "late.sock", sizeof(sun.sun_path));
+	sun.sun_len = SUN_LEN(&sun);
+	ATF_REQUIRE_MSG(bind(l, (struct sockaddr *)&sun, sun.sun_len) == 0,
+	    "bind after listen: %s", strerror(errno));
+
+	ATF_REQUIRE((s = socket(PF_UNIX, SOCK_STREAM, 0)) >= 0);
+	ATF_REQUIRE_EQ(0, pathconnect(AT_FDCWD, s, "late.sock"));
+	ATF_REQUIRE((a = accept(l, NULL, NULL)) >= 0);
+
+	/* The name bound after listen(2) is reported to the peer. */
+	memset(&peer, 0, sizeof(peer));
+	len = sizeof(peer);
+	ATF_REQUIRE_EQ(0, getpeername(s, (struct sockaddr *)&peer, &len));
+	ATF_REQUIRE_EQ(0, strcmp(peer.sun_path, "late.sock"));
+
+	ATF_REQUIRE_EQ(0, close(a));
+	ATF_REQUIRE_EQ(0, close(s));
+	ATF_REQUIRE_EQ(0, close(l));
+}
+
+/*
+ * A socket whose connection has gone away may become a listener in its own
+ * right: unp_soisdisconnected() leaves only SS_ISDISCONNECTED set, which
+ * solisten_proto_check() does not reject, and unp_disconnect() has already
+ * cleared unp_conn.  Only the bind requirement stood in the way, and then only
+ * for the usual client socket, which has no name.
+ *
+ * Note: this case is here only to document the current behavior and to catch
+ * it changing in the future.  Such socket reuse is not covered by the
+ * specification, and is discouraged and should not be utilized in real-world
+ * programs.
+ */
+ATF_TC_WITHOUT_HEAD(listen_after_disconnect);
+ATF_TC_BODY(listen_after_disconnect, tc)
+{
+	int l, c, s, a;
+
+	/* Connect a pair, then drop the accepted end to disconnect 'c'. */
+	ATF_REQUIRE((l = socket(PF_UNIX, SOCK_STREAM, 0)) >= 0);
+	ATF_REQUIRE_MSG(listen(l, 1) == 0, "listen: %s", strerror(errno));
+	ATF_REQUIRE((c = socket(PF_UNIX, SOCK_STREAM, 0)) >= 0);
+	ATF_REQUIRE_EQ(0, fdconnect(l, c));
+	ATF_REQUIRE((a = accept(l, NULL, NULL)) >= 0);
+	ATF_REQUIRE_EQ(0, close(a));
+	ATF_REQUIRE_EQ(0, close(l));
+
+	/* The survivor listens, and takes a connection of its own. */
+	ATF_REQUIRE_MSG(listen(c, 1) == 0, "listen: %s", strerror(errno));
+	ATF_REQUIRE((s = socket(PF_UNIX, SOCK_STREAM, 0)) >= 0);
+	ATF_REQUIRE_EQ(0, fdconnect(c, s));
+	ATF_REQUIRE((a = accept(c, NULL, NULL)) >= 0);
+
+	ATF_REQUIRE_EQ(0, close(a));
+	ATF_REQUIRE_EQ(0, close(s));
+	ATF_REQUIRE_EQ(0, close(c));
+}
+
 /* Connect a datagram socket to an unbound peer by its fd. */
 ATF_TC_WITHOUT_HEAD(dgram);
 ATF_TC_BODY(dgram, tc)
@@ -646,6 +745,9 @@ ATF_TP_ADD_TCS(tp)
 {
 	ATF_TP_ADD_TC(tp, stream);
 	ATF_TP_ADD_TC(tp, stream_bound);
+	ATF_TP_ADD_TC(tp, listen_unbound);
+	ATF_TP_ADD_TC(tp, bind_after_listen);
+	ATF_TP_ADD_TC(tp, listen_after_disconnect);
 	ATF_TP_ADD_TC(tp, dgram);
 	ATF_TP_ADD_TC(tp, empty_path_vnode);
 	ATF_TP_ADD_TC(tp, path);
diff --git a/tests/sys/kern/unix_seqpacket_test.c b/tests/sys/kern/unix_seqpacket_test.c
index 27bd430430b4..08544516866c 100644
--- a/tests/sys/kern/unix_seqpacket_test.c
+++ b/tests/sys/kern/unix_seqpacket_test.c
@@ -442,8 +442,11 @@ ATF_TC_BODY(listen_unbound, tc)
 	s = socket(PF_LOCAL, SOCK_SEQPACKET, 0);
 	ATF_REQUIRE(s > 0);
 	r = listen(s, -1);
-	/* expect listen to fail since we haven't called bind(2) */
-	ATF_CHECK(r != 0);
+	/*
+	 * An unbound socket may listen: connectat(2) can name it by descriptor,
+	 * so it does not need a pathname to be reachable.
+	 */
+	ATF_CHECK_EQ(0, r);
 	close(s);
 }