From nobody Mon Aug 10 17:40:38 2026 X-Original-To: dev-commits-src-main@mlmmj.nyi.freebsd.org Received: from mx1.freebsd.org (mx1.freebsd.org [IPv6:2610:1c1:1:606c::19:1]) by mlmmj.nyi.freebsd.org (Postfix) with ESMTP id 4hJhqX073mz6X2t0 for ; Mon, 10 Aug 2026 17:40:40 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from mxrelay.nyi.freebsd.org (mxrelay.nyi.freebsd.org [IPv6:2610:1c1:1:606c::19:3]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (4096 bits) server-digest SHA256 client-signature RSA-PSS (4096 bits) client-digest SHA256) (Client CN "mxrelay.nyi.freebsd.org", Issuer "YR1" (not verified)) by mx1.freebsd.org (Postfix) with ESMTPS id 4hJhqW0VWFz494J for ; Mon, 10 Aug 2026 17:40:39 +0000 (UTC) (envelope-from git@FreeBSD.org) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1786383639; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=KnPiWJPhgbwLu4M+znXdtwy0Wu9tPNKoj0Vpn693EkQ=; b=ixOfxs8PLv/0tmUThywWoA7HCp+BlPnCiKKq9n4fs7KOwEdQjQOHncFJ3Ghs3RljJBeqQo TXTvry9dRu+uHrH8UWXRpaCUmiZiFaBjt4OyMjnvS2RvzPya/1ss5ygSFBisKNVGURes1k CYwNDk6wTT48aohOkdiuIy5ixhQzB/2kEY9Ia4GiwYKbvr4odyHZX8bRlbzIgRFUK4R46R G5yENxCA6jo0I2OnmVRpxiq9hKvSAGoi1Fi/TifX5pdYH7iAAF9Jwos+Dfx5UgZLhr5jlW TupkdjqJOMHxpD82d6UCEIXEH5KOV4AK/7EiYlCr1wlwOx49BEGQxW9zVqmQUQ== ARC-Seal: i=1; s=dkim; d=freebsd.org; t=1786383639; a=rsa-sha256; cv=none; b=mTANPiH7f22zAFCEdf7ey12O9ixqs+LUNra7nJ8H3Ob/SkFrvediO3etS/RRi1SG54zkvN NyAzMi2krKDT0EZF83xT+bUU2i2EClz+UaIBn/I+DbAhTXPlIGWvQ8YfxJ9oJF4b8Msscl FJ4Ra5xLRqGDRmRoXLl0BcwBNmAwgJ6VbHx6KI0g8qRtFE4uG65YoyH2eOMoWnGUtwcR7a 2ORPXT4OaSBiXLi+uDTQFsf1q24VNNgxdZks2HCr2GCgYBNGJvgNUoeK2YHGnH5Xzktw7U EftpxG3RR/kTGInR/AMtXqz/oUwJAlelXEnRyPOZ6mVjywtI0WiLkeO/qmKq5g== ARC-Authentication-Results: i=1; mx1.freebsd.org; none ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1786383639; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=KnPiWJPhgbwLu4M+znXdtwy0Wu9tPNKoj0Vpn693EkQ=; b=cw1GE6J/s3PTs58PtRu+Z39JG2vB6ubSM7NZO/rB1hXjRr1l+jz4dBeOjovkmboE6EiBFK t33V1B1pg7eMlKlJ2kltdSS+aFUyWXDCisi/39Aj2dDiFVocik1Is0taSJnOqYhtpjGfJM RehyVVAYagh/JluoCGyCmQMz4lRWgaD8qSV+pYJ1dWR+VVKBkT3zLUDWP6d0+VoYp/x2od iFW18CgluWsdOZaviesKB3ebZxi+ZHox0QGoFzA/P2KKHO18+g5oI4Ca17ufFpOkwudvRn iU2bcx17JSyomnNU/LFnwtB4mSCU25ulbEGvEc1L4MhvQ5boWCzyAppsWFiugA== Received: from gitrepo.freebsd.org (gitrepo.freebsd.org [IPv6:2610:1c1:1:6068::e6a:5]) by mxrelay.nyi.freebsd.org (Postfix) with ESMTP id 4hJhqV5tTtz3bP for ; Mon, 10 Aug 2026 17:40:38 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from git (uid 1279) (envelope-from git@FreeBSD.org) id 3fd7f by gitrepo.freebsd.org (DragonFly Mail Agent v0.13+ on gitrepo.freebsd.org); Mon, 10 Aug 2026 17:40:38 +0000 To: src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-main@FreeBSD.org Cc: John Ericson From: Mark Johnston Subject: git: ce3750b5995c - main - unix: allow listening on an unbound socket, and binding after listen List-Id: Commit messages for the main branch of the src repository List-Archive: https://lists.freebsd.org/archives/dev-commits-src-main List-Help: List-Post: List-Subscribe: List-Unsubscribe: X-BeenThere: dev-commits-src-main@freebsd.org Sender: owner-dev-commits-src-main@FreeBSD.org List-Id: List-Post: List-Help: List-Subscribe: List-Unsubscribe: List-Owner: Precedence: list MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit X-Git-Committer: markj X-Git-Repository: src X-Git-Refname: refs/heads/main X-Git-Reftype: branch X-Git-Commit: ce3750b5995c3c3e0376ae62a7c55d9a6e547bbc Auto-Submitted: auto-generated Date: Mon, 10 Aug 2026 17:40:38 +0000 Message-Id: <6a7a0d16.3fd7f.492133b3@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 AuthorDate: 2026-08-10 15:04:33 +0000 Commit: Mark Johnston 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 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); }