selftests: af_unix: Add tests for ECONNRESET and EOF semantics

Add selftests to verify and document Linux’s intended behaviour for
UNIX domain sockets (SOCK_STREAM and SOCK_DGRAM) when a peer closes.
The tests verify that:

 1. SOCK_STREAM returns EOF when the peer closes normally.
 2. SOCK_STREAM returns ECONNRESET if the peer closes with unread data.
 3. SOCK_SEQPACKET returns EOF when the peer closes normally.
 4. SOCK_SEQPACKET returns ECONNRESET if the peer closes with unread data.
 5. SOCK_DGRAM does not return ECONNRESET when the peer closes.

This follows up on review feedback suggesting a selftest to clarify
Linux’s semantics.

Suggested-by: Kuniyuki Iwashima <kuniyu@google.com>
Signed-off-by: Sunday Adelodun <adelodunolaoluwa@yahoo.com>
Link: https://patch.msgid.link/20251113112802.44657-1-adelodunolaoluwa@yahoo.com
Signed-off-by: Paolo Abeni <pabeni@redhat.com>
This commit is contained in:
Sunday Adelodun 2025-11-13 12:28:02 +01:00 committed by Paolo Abeni
parent 4e1b0afa61
commit 45a1cd8346
3 changed files with 179 additions and 0 deletions

View File

@ -65,3 +65,4 @@ udpgso
udpgso_bench_rx udpgso_bench_rx
udpgso_bench_tx udpgso_bench_tx
unix_connect unix_connect
unix_connreset

View File

@ -7,6 +7,7 @@ TEST_GEN_PROGS := \
scm_pidfd \ scm_pidfd \
scm_rights \ scm_rights \
unix_connect \ unix_connect \
unix_connreset \
# end of TEST_GEN_PROGS # end of TEST_GEN_PROGS
include ../../lib.mk include ../../lib.mk

View File

@ -0,0 +1,177 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Selftest for AF_UNIX socket close and ECONNRESET behaviour.
*
* This test verifies:
* 1. SOCK_STREAM returns EOF when the peer closes normally.
* 2. SOCK_STREAM returns ECONNRESET if peer closes with unread data.
* 3. SOCK_SEQPACKET returns EOF when the peer closes normally.
* 4. SOCK_SEQPACKET returns ECONNRESET if the peer closes with unread data.
* 5. SOCK_DGRAM does not return ECONNRESET when the peer closes.
*
* These tests document the intended Linux behaviour.
*
*/
#define _GNU_SOURCE
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/un.h>
#include "../../kselftest_harness.h"
#define SOCK_PATH "/tmp/af_unix_connreset.sock"
static void remove_socket_file(void)
{
unlink(SOCK_PATH);
}
FIXTURE(unix_sock)
{
int server;
int client;
int child;
};
FIXTURE_VARIANT(unix_sock)
{
int socket_type;
const char *name;
};
FIXTURE_VARIANT_ADD(unix_sock, stream) {
.socket_type = SOCK_STREAM,
.name = "SOCK_STREAM",
};
FIXTURE_VARIANT_ADD(unix_sock, dgram) {
.socket_type = SOCK_DGRAM,
.name = "SOCK_DGRAM",
};
FIXTURE_VARIANT_ADD(unix_sock, seqpacket) {
.socket_type = SOCK_SEQPACKET,
.name = "SOCK_SEQPACKET",
};
FIXTURE_SETUP(unix_sock)
{
struct sockaddr_un addr = {};
int err;
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, SOCK_PATH);
remove_socket_file();
self->server = socket(AF_UNIX, variant->socket_type, 0);
ASSERT_LT(-1, self->server);
err = bind(self->server, (struct sockaddr *)&addr, sizeof(addr));
ASSERT_EQ(0, err);
if (variant->socket_type == SOCK_STREAM ||
variant->socket_type == SOCK_SEQPACKET) {
err = listen(self->server, 1);
ASSERT_EQ(0, err);
}
self->client = socket(AF_UNIX, variant->socket_type | SOCK_NONBLOCK, 0);
ASSERT_LT(-1, self->client);
err = connect(self->client, (struct sockaddr *)&addr, sizeof(addr));
ASSERT_EQ(0, err);
}
FIXTURE_TEARDOWN(unix_sock)
{
if (variant->socket_type == SOCK_STREAM ||
variant->socket_type == SOCK_SEQPACKET)
close(self->child);
close(self->client);
close(self->server);
remove_socket_file();
}
/* Test 1: peer closes normally */
TEST_F(unix_sock, eof)
{
char buf[16] = {};
ssize_t n;
if (variant->socket_type == SOCK_STREAM ||
variant->socket_type == SOCK_SEQPACKET) {
self->child = accept(self->server, NULL, NULL);
ASSERT_LT(-1, self->child);
close(self->child);
} else {
close(self->server);
}
n = recv(self->client, buf, sizeof(buf), 0);
if (variant->socket_type == SOCK_STREAM ||
variant->socket_type == SOCK_SEQPACKET) {
ASSERT_EQ(0, n);
} else {
ASSERT_EQ(-1, n);
ASSERT_EQ(EAGAIN, errno);
}
}
/* Test 2: peer closes with unread data */
TEST_F(unix_sock, reset_unread_behavior)
{
char buf[16] = {};
ssize_t n;
/* Send data that will remain unread */
send(self->client, "hello", 5, 0);
if (variant->socket_type == SOCK_DGRAM) {
/* No real connection, just close the server */
close(self->server);
} else {
self->child = accept(self->server, NULL, NULL);
ASSERT_LT(-1, self->child);
/* Peer closes before client reads */
close(self->child);
}
n = recv(self->client, buf, sizeof(buf), 0);
ASSERT_EQ(-1, n);
if (variant->socket_type == SOCK_STREAM ||
variant->socket_type == SOCK_SEQPACKET) {
ASSERT_EQ(ECONNRESET, errno);
} else {
ASSERT_EQ(EAGAIN, errno);
}
}
/* Test 3: closing unaccepted (embryo) server socket should reset client. */
TEST_F(unix_sock, reset_closed_embryo)
{
char buf[16] = {};
ssize_t n;
if (variant->socket_type == SOCK_DGRAM)
SKIP(return, "This test only applies to SOCK_STREAM and SOCK_SEQPACKET");
/* Close server without accept()ing */
close(self->server);
n = recv(self->client, buf, sizeof(buf), 0);
ASSERT_EQ(-1, n);
ASSERT_EQ(ECONNRESET, errno);
}
TEST_HARNESS_MAIN