selftests/net/af_unix: test listen() rejects wrong socket states

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: fd0a109a0f ("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>
This commit is contained in:
John Ericson 2026-07-18 14:29:02 -04:00 committed by Jakub Kicinski
parent f0d9c3ffc2
commit 7f57c650d0
3 changed files with 189 additions and 0 deletions

View File

@ -6,3 +6,4 @@ scm_rights
so_peek_off
unix_connect
unix_connreset
unix_listen

View 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

View File

@ -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