git: ce3750b5995c - main - unix: allow listening on an unbound socket, and binding after listen
- Go to: [ bottom of page ] [ top of archives ] [ this month ]
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);
}