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); }