]> git.ipfire.org Git - thirdparty/kernel/stable.git/commitdiff
selftests/net/af_unix: test listen() rejects wrong socket states
authorJohn Ericson <mail@johnericson.me>
Sat, 18 Jul 2026 18:29:02 +0000 (14:29 -0400)
committerJakub Kicinski <kuba@kernel.org>
Fri, 24 Jul 2026 22:10:05 +0000 (15:10 -0700)
Add a regression test for the unix_listen() state check. The key case is
listen() on a bound socket that has already been connected: it is no
longer in TCP_CLOSE or TCP_LISTEN, so it must fail with EINVAL. A
prepare_peercred() call slipped in ahead of that check once left err at 0
and made listen() silently succeed there instead; this guards against a
repeat.

The neighbouring outcomes are covered too so they cannot regress the same
way: a bound socket in TCP_CLOSE listens fine, calling listen() again on a
socket already in TCP_LISTEN is allowed, and an unbound socket fails with
EINVAL.

Each case runs for both listenable socket types (SOCK_STREAM and
SOCK_SEQPACKET) and both pathname and abstract addresses.

Fixes: fd0a109a0f6b ("net, pidfs: prepare for handing out pidfds for reaped sk->sk_peer_pid")
Signed-off-by: John Ericson <mail@johnericson.me>
Link: https://patch.msgid.link/20260718182903.2295560-2-John.Ericson@Obsidian.Systems
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
tools/testing/selftests/net/af_unix/.gitignore
tools/testing/selftests/net/af_unix/Makefile
tools/testing/selftests/net/af_unix/unix_listen.c [new file with mode: 0644]

index 240b26740c9e0eb56b4e93078d52d5f1ae545d12..9731766441036564c3c156f7a10336e95535173e 100644 (file)
@@ -6,3 +6,4 @@ scm_rights
 so_peek_off
 unix_connect
 unix_connreset
+unix_listen
index 4c0375e28bbeebcb9c02de760adccc369d57b8bd..57d159803a3abd6e855ea7949a94d2bd0aad3a7f 100644 (file)
@@ -14,6 +14,7 @@ TEST_GEN_PROGS := \
        so_peek_off \
        unix_connect \
        unix_connreset \
+       unix_listen \
 # end of TEST_GEN_PROGS
 
 include ../../lib.mk
diff --git a/tools/testing/selftests/net/af_unix/unix_listen.c b/tools/testing/selftests/net/af_unix/unix_listen.c
new file mode 100644 (file)
index 0000000..416fa3e
--- /dev/null
@@ -0,0 +1,187 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Tests for the state checks in AF_UNIX listen().
+ *
+ * The central case is a regression test: listen() on a bound socket that
+ * is already connected (i.e. not in TCP_CLOSE or TCP_LISTEN state) must
+ * fail with EINVAL.  A prior change accidentally let it return success
+ * without doing anything, because a helper called in between reset the
+ * error code to 0.  The neighbouring checks (unbound, already listening)
+ * are tested too so they cannot silently regress the same way.
+ *
+ * Every case runs for both listenable socket types (SOCK_STREAM and
+ * SOCK_SEQPACKET) and both pathname and abstract addresses.
+ */
+#define _GNU_SOURCE
+
+#include <errno.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <sys/socket.h>
+#include <sys/un.h>
+
+#include "kselftest_harness.h"
+
+#define SK_NAME                "unix_listen_sk"
+#define SRV_NAME       "unix_listen_srv"
+
+FIXTURE(unix_listen)
+{
+       int sk;                 /* socket under test */
+       int server;             /* a listening peer, when a test needs one */
+       struct sockaddr_un addr, srv_addr;
+       socklen_t addrlen, srv_addrlen;
+};
+
+FIXTURE_VARIANT(unix_listen)
+{
+       int type;
+       int abstract;
+};
+
+FIXTURE_VARIANT_ADD(unix_listen, stream_pathname)
+{
+       .type = SOCK_STREAM,
+       .abstract = 0,
+};
+
+FIXTURE_VARIANT_ADD(unix_listen, stream_abstract)
+{
+       .type = SOCK_STREAM,
+       .abstract = 1,
+};
+
+FIXTURE_VARIANT_ADD(unix_listen, seqpacket_pathname)
+{
+       .type = SOCK_SEQPACKET,
+       .abstract = 0,
+};
+
+FIXTURE_VARIANT_ADD(unix_listen, seqpacket_abstract)
+{
+       .type = SOCK_SEQPACKET,
+       .abstract = 1,
+};
+
+/* Fill @addr with a pathname or abstract address named @name. */
+static socklen_t unix_set_addr(struct sockaddr_un *addr, const char *name,
+                              int abstract)
+{
+       size_t len = strlen(name);
+
+       memset(addr, 0, sizeof(*addr));
+       addr->sun_family = AF_UNIX;
+       /* An abstract address leads with a NUL and has no filesystem entry. */
+       memcpy(addr->sun_path + (abstract ? 1 : 0), name, len);
+
+       return offsetof(struct sockaddr_un, sun_path) + len + 1;
+}
+
+FIXTURE_SETUP(unix_listen)
+{
+       self->sk = -1;
+       self->server = -1;
+       self->addrlen = unix_set_addr(&self->addr, SK_NAME, variant->abstract);
+       self->srv_addrlen = unix_set_addr(&self->srv_addr, SRV_NAME,
+                                         variant->abstract);
+}
+
+FIXTURE_TEARDOWN(unix_listen)
+{
+       if (self->sk >= 0)
+               close(self->sk);
+       if (self->server >= 0)
+               close(self->server);
+
+       /* Pathname sockets leave a filesystem entry behind; abstract ones do not. */
+       if (!variant->abstract) {
+               remove(SK_NAME);
+               remove(SRV_NAME);
+       }
+}
+
+/* A bound socket in TCP_CLOSE is the normal, allowed case. */
+TEST_F(unix_listen, bound_is_ok)
+{
+       int err;
+
+       self->sk = socket(AF_UNIX, variant->type, 0);
+       ASSERT_LE(0, self->sk);
+
+       err = bind(self->sk, (struct sockaddr *)&self->addr, self->addrlen);
+       ASSERT_EQ(0, err);
+
+       err = listen(self->sk, 8);
+       EXPECT_EQ(0, err);
+}
+
+/* Listening again on an already-listening socket (TCP_LISTEN) is allowed. */
+TEST_F(unix_listen, relisten_is_ok)
+{
+       int err;
+
+       self->sk = socket(AF_UNIX, variant->type, 0);
+       ASSERT_LE(0, self->sk);
+
+       err = bind(self->sk, (struct sockaddr *)&self->addr, self->addrlen);
+       ASSERT_EQ(0, err);
+
+       err = listen(self->sk, 8);
+       ASSERT_EQ(0, err);
+
+       err = listen(self->sk, 16);
+       EXPECT_EQ(0, err);
+}
+
+/* listen() on an unbound socket fails: there is nothing to listen on. */
+TEST_F(unix_listen, unbound_is_einval)
+{
+       int err;
+
+       self->sk = socket(AF_UNIX, variant->type, 0);
+       ASSERT_LE(0, self->sk);
+
+       err = listen(self->sk, 8);
+       EXPECT_EQ(-1, err);
+       EXPECT_EQ(EINVAL, errno);
+}
+
+/*
+ * The regression: a bound socket that has already been connected is not in
+ * TCP_CLOSE or TCP_LISTEN, so listen() must reject it with EINVAL rather
+ * than quietly succeeding.
+ */
+TEST_F(unix_listen, connected_is_einval)
+{
+       int err;
+
+       self->server = socket(AF_UNIX, variant->type, 0);
+       ASSERT_LE(0, self->server);
+
+       err = bind(self->server, (struct sockaddr *)&self->srv_addr,
+                  self->srv_addrlen);
+       ASSERT_EQ(0, err);
+
+       err = listen(self->server, 8);
+       ASSERT_EQ(0, err);
+
+       self->sk = socket(AF_UNIX, variant->type, 0);
+       ASSERT_LE(0, self->sk);
+
+       /* Bind first so the unbound check does not mask the state check. */
+       err = bind(self->sk, (struct sockaddr *)&self->addr, self->addrlen);
+       ASSERT_EQ(0, err);
+
+       err = connect(self->sk, (struct sockaddr *)&self->srv_addr,
+                     self->srv_addrlen);
+       ASSERT_EQ(0, err);
+
+       err = listen(self->sk, 8);
+       EXPECT_EQ(-1, err);
+       EXPECT_EQ(EINVAL, errno);
+}
+
+TEST_HARNESS_MAIN