Commit 45a1cd83 authored by Sunday Adelodun's avatar Sunday Adelodun Committed by Paolo Abeni
Browse files

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: default avatarKuniyuki Iwashima <kuniyu@google.com>
Signed-off-by: default avatarSunday Adelodun <adelodunolaoluwa@yahoo.com>
Link: https://patch.msgid.link/20251113112802.44657-1-adelodunolaoluwa@yahoo.com


Signed-off-by: default avatarPaolo Abeni <pabeni@redhat.com>
parent 4e1b0afa
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -65,3 +65,4 @@ udpgso
udpgso_bench_rx
udpgso_bench_tx
unix_connect
unix_connreset
+1 −0
Original line number Diff line number Diff line
@@ -7,6 +7,7 @@ TEST_GEN_PROGS := \
	scm_pidfd \
	scm_rights \
	unix_connect \
	unix_connreset \
# end of TEST_GEN_PROGS

include ../../lib.mk
+177 −0
Original line number Diff line number Diff line
// 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