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

Mark Johnston <[email protected]>
Newsgroups gmane.os.freebsd.devel.cvs.src
Message-ID <6a7a0d16.3fd7f.492133b3__45759.9063798384$1786383826$gmane$org@gitrepo.freebsd.org>
The branch main has been updated by markj:

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

commit ce3750b5995c3c3e0376ae62a7c55d9a6e547bbc
Author:     John Ericson <[email protected]>
AuthorDate: 2026-08-10 15:04:33 +0000
Commit:     Mark Johnston <[email protected]>
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 <[email protected]>
    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);
 }
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.