root/tools/testing/selftests/net/af_unix/unix_connreset.c
// 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 <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) {
                snprintf(_metadata->results->reason,
                         sizeof(_metadata->results->reason),
                         "Test only applies to SOCK_STREAM and SOCK_SEQPACKET");
                exit(KSFT_XFAIL);
        }

        /* 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