Commit b4cbbf07 authored by Jakub Kicinski's avatar Jakub Kicinski
Browse files

Merge branch 'vsock-test-tests-for-memory-leaks'

Michal Luczaj says:

====================
vsock/test: Tests for memory leaks

Series adds tests for recently fixed memory leaks[1]:

commit d7b0ff5a ("virtio/vsock: Fix accept_queue memory leak")
commit fbf7085b ("vsock: Fix sk_error_queue memory leak")
commit 60cf6206 ("virtio/vsock: Improve MSG_ZEROCOPY error handling")

Patch 1 is a non-functional preparatory cleanup.
Patch 2 is a test suite extension for picking specific tests.
Patch 3 explains the need of kmemleak scans.
Patch 4 adapts utility functions to handle MSG_ZEROCOPY.
Patches 5-6-7 add the tests.

NOTE: Test in the last patch ("vsock/test: Add test for MSG_ZEROCOPY
completion memory leak") may stop working even before this series is
merged. See changes proposed in [2]. The failslab variant would be
unaffected.

[1] https://lore.kernel.org/20241107-vsock-mem-leaks-v2-0-4e21bfcfc818@rbox.co
[2] https://lore.kernel.org/CANn89i+oL+qoPmbbGvE_RT3_3OWgeck7cCPcTafeehKrQZ8kyw@mail.gmail.com

v3: https://lore.kernel.org/20241218-test-vsock-leaks-v3-0-f1a4dcef9228@rbox.co
v2: https://lore.kernel.org/20241216-test-vsock-leaks-v2-0-55e1405742fc@rbox.co
v1: https://lore.kernel.org/20241206-test-vsock-leaks-v1-0-c31e8c875797@rbox.co
====================

Link: https://patch.msgid.link/20241219-test-vsock-leaks-v4-0-a416e554d9d7@rbox.co


Signed-off-by: default avatarJakub Kicinski <kuba@kernel.org>
parents aa4ad7c3 d127ac8b
Loading
Loading
Loading
Loading
+15 −0
Original line number Diff line number Diff line
@@ -36,6 +36,21 @@ Invoke test binaries in both directions as follows:
                       --control-port=1234 \
                       --peer-cid=3

Some tests are designed to produce kernel memory leaks. Leaks detection,
however, is deferred to Kernel Memory Leak Detector. It is recommended to enable
kmemleak (CONFIG_DEBUG_KMEMLEAK=y) and explicitly trigger a scan after each test
suite run, e.g.

  # echo clear > /sys/kernel/debug/kmemleak
  # $TEST_BINARY ...
  # echo "wait for any grace periods" && sleep 2
  # echo scan > /sys/kernel/debug/kmemleak
  # echo "wait for kmemleak" && sleep 5
  # echo scan > /sys/kernel/debug/kmemleak
  # cat /sys/kernel/debug/kmemleak

For more information see Documentation/dev-tools/kmemleak.rst.

vsock_perf utility
-------------------
'vsock_perf' is a simple tool to measure vsock performance. It works in
+29 −4
Original line number Diff line number Diff line
@@ -401,7 +401,7 @@ void recv_buf(int fd, void *buf, size_t len, int flags, ssize_t expected_ret)
 */
void send_byte(int fd, int expected_ret, int flags)
{
	const uint8_t byte = 'A';
	static const uint8_t byte = 'A';

	send_buf(fd, &byte, sizeof(byte), flags, expected_ret);
}
@@ -420,7 +420,7 @@ void recv_byte(int fd, int expected_ret, int flags)
	recv_buf(fd, &byte, sizeof(byte), flags, expected_ret);

	if (byte != 'A') {
		fprintf(stderr, "unexpected byte read %c\n", byte);
		fprintf(stderr, "unexpected byte read 0x%02x\n", byte);
		exit(EXIT_FAILURE);
	}
}
@@ -486,8 +486,7 @@ void list_tests(const struct test_case *test_cases)
	exit(EXIT_FAILURE);
}

void skip_test(struct test_case *test_cases, size_t test_cases_len,
	       const char *test_id_str)
static unsigned long parse_test_id(const char *test_id_str, size_t test_cases_len)
{
	unsigned long test_id;
	char *endptr = NULL;
@@ -505,9 +504,35 @@ void skip_test(struct test_case *test_cases, size_t test_cases_len,
		exit(EXIT_FAILURE);
	}

	return test_id;
}

void skip_test(struct test_case *test_cases, size_t test_cases_len,
	       const char *test_id_str)
{
	unsigned long test_id = parse_test_id(test_id_str, test_cases_len);
	test_cases[test_id].skip = true;
}

void pick_test(struct test_case *test_cases, size_t test_cases_len,
	       const char *test_id_str)
{
	static bool skip_all = true;
	unsigned long test_id;

	if (skip_all) {
		unsigned long i;

		for (i = 0; i < test_cases_len; ++i)
			test_cases[i].skip = true;

		skip_all = false;
	}

	test_id = parse_test_id(test_id_str, test_cases_len);
	test_cases[test_id].skip = false;
}

unsigned long hash_djb2(const void *data, size_t len)
{
	unsigned long hash = 5381;
+2 −0
Original line number Diff line number Diff line
@@ -62,6 +62,8 @@ void run_tests(const struct test_case *test_cases,
void list_tests(const struct test_case *test_cases);
void skip_test(struct test_case *test_cases, size_t test_cases_len,
	       const char *test_id_str);
void pick_test(struct test_case *test_cases, size_t test_cases_len,
	       const char *test_id_str);
unsigned long hash_djb2(const void *data, size_t len);
size_t iovec_bytes(const struct iovec *iov, size_t iovnum);
unsigned long iovec_hash_djb2(const struct iovec *iov, size_t iovnum);
+263 −2
Original line number Diff line number Diff line
@@ -22,12 +22,17 @@
#include <signal.h>
#include <sys/ioctl.h>
#include <linux/sockios.h>
#include <linux/time64.h>

#include "vsock_test_zerocopy.h"
#include "timeout.h"
#include "control.h"
#include "util.h"

/* Basic messages for control_writeulong(), control_readulong() */
#define CONTROL_CONTINUE	1
#define CONTROL_DONE		0

static void test_stream_connection_reset(const struct test_opts *opts)
{
	union {
@@ -559,7 +564,7 @@ static time_t current_nsec(void)
		exit(EXIT_FAILURE);
	}

	return (ts.tv_sec * 1000000000ULL) + ts.tv_nsec;
	return (ts.tv_sec * NSEC_PER_SEC) + ts.tv_nsec;
}

#define RCVTIMEO_TIMEOUT_SEC 1
@@ -599,7 +604,7 @@ static void test_seqpacket_timeout_client(const struct test_opts *opts)
	}

	read_overhead_ns = current_nsec() - read_enter_ns -
			1000000000ULL * RCVTIMEO_TIMEOUT_SEC;
			   NSEC_PER_SEC * RCVTIMEO_TIMEOUT_SEC;

	if (read_overhead_ns > READ_OVERHEAD_NSEC) {
		fprintf(stderr,
@@ -1473,6 +1478,236 @@ static void test_stream_cred_upd_on_set_rcvlowat(const struct test_opts *opts)
	test_stream_credit_update_test(opts, false);
}

/* The goal of test leak_acceptq is to stress the race between connect() and
 * close(listener). Implementation of client/server loops boils down to:
 *
 * client                server
 * ------                ------
 * write(CONTINUE)
 *                       expect(CONTINUE)
 *                       listen()
 *                       write(LISTENING)
 * expect(LISTENING)
 * connect()             close()
 */
#define ACCEPTQ_LEAK_RACE_TIMEOUT 2 /* seconds */

static void test_stream_leak_acceptq_client(const struct test_opts *opts)
{
	time_t tout;
	int fd;

	tout = current_nsec() + ACCEPTQ_LEAK_RACE_TIMEOUT * NSEC_PER_SEC;
	do {
		control_writeulong(CONTROL_CONTINUE);

		fd = vsock_stream_connect(opts->peer_cid, opts->peer_port);
		if (fd >= 0)
			close(fd);
	} while (current_nsec() < tout);

	control_writeulong(CONTROL_DONE);
}

/* Test for a memory leak. User is expected to run kmemleak scan, see README. */
static void test_stream_leak_acceptq_server(const struct test_opts *opts)
{
	int fd;

	while (control_readulong() == CONTROL_CONTINUE) {
		fd = vsock_stream_listen(VMADDR_CID_ANY, opts->peer_port);
		control_writeln("LISTENING");
		close(fd);
	}
}

/* Test for a memory leak. User is expected to run kmemleak scan, see README. */
static void test_stream_msgzcopy_leak_errq_client(const struct test_opts *opts)
{
	struct pollfd fds = { 0 };
	int fd;

	fd = vsock_stream_connect(opts->peer_cid, opts->peer_port);
	if (fd < 0) {
		perror("connect");
		exit(EXIT_FAILURE);
	}

	enable_so_zerocopy_check(fd);
	send_byte(fd, 1, MSG_ZEROCOPY);

	fds.fd = fd;
	fds.events = 0;
	if (poll(&fds, 1, -1) < 0) {
		perror("poll");
		exit(EXIT_FAILURE);
	}

	close(fd);
}

static void test_stream_msgzcopy_leak_errq_server(const struct test_opts *opts)
{
	int fd;

	fd = vsock_stream_accept(VMADDR_CID_ANY, opts->peer_port, NULL);
	if (fd < 0) {
		perror("accept");
		exit(EXIT_FAILURE);
	}

	recv_byte(fd, 1, 0);
	vsock_wait_remote_close(fd);
	close(fd);
}

/* Test msgzcopy_leak_zcskb is meant to exercise sendmsg() error handling path,
 * that might leak an skb. The idea is to fail virtio_transport_init_zcopy_skb()
 * by hitting net.core.optmem_max limit in sock_omalloc(), specifically
 *
 *   vsock_connectible_sendmsg
 *     virtio_transport_stream_enqueue
 *       virtio_transport_send_pkt_info
 *         virtio_transport_init_zcopy_skb
 *         . msg_zerocopy_realloc
 *         .   msg_zerocopy_alloc
 *         .     sock_omalloc
 *         .       sk_omem_alloc + size > sysctl_optmem_max
 *         return -ENOMEM
 *
 * We abuse the implementation detail of net/socket.c:____sys_sendmsg().
 * sk_omem_alloc can be precisely bumped by sock_kmalloc(), as it is used to
 * fetch user-provided control data.
 *
 * While this approach works for now, it relies on assumptions regarding the
 * implementation and configuration (for example, order of net.core.optmem_max
 * can not exceed MAX_PAGE_ORDER), which may not hold in the future. A more
 * resilient testing could be implemented by leveraging the Fault injection
 * framework (CONFIG_FAULT_INJECTION), e.g.
 *
 *   client# echo N > /sys/kernel/debug/failslab/ignore-gfp-wait
 *   client# echo 0 > /sys/kernel/debug/failslab/verbose
 *
 *   void client(const struct test_opts *opts)
 *   {
 *       char buf[16];
 *       int f, s, i;
 *
 *       f = open("/proc/self/fail-nth", O_WRONLY);
 *
 *       for (i = 1; i < 32; i++) {
 *           control_writeulong(CONTROL_CONTINUE);
 *
 *           s = vsock_stream_connect(opts->peer_cid, opts->peer_port);
 *           enable_so_zerocopy_check(s);
 *
 *           sprintf(buf, "%d", i);
 *           write(f, buf, strlen(buf));
 *
 *           send(s, &(char){ 0 }, 1, MSG_ZEROCOPY);
 *
 *           write(f, "0", 1);
 *           close(s);
 *       }
 *
 *       control_writeulong(CONTROL_DONE);
 *       close(f);
 *   }
 *
 *   void server(const struct test_opts *opts)
 *   {
 *       int fd;
 *
 *       while (control_readulong() == CONTROL_CONTINUE) {
 *           fd = vsock_stream_accept(VMADDR_CID_ANY, opts->peer_port, NULL);
 *           vsock_wait_remote_close(fd);
 *           close(fd);
 *       }
 *   }
 *
 * Refer to Documentation/fault-injection/fault-injection.rst.
 */
#define MAX_PAGE_ORDER	10	/* usually */
#define PAGE_SIZE	4096

/* Test for a memory leak. User is expected to run kmemleak scan, see README. */
static void test_stream_msgzcopy_leak_zcskb_client(const struct test_opts *opts)
{
	size_t optmem_max, ctl_len, chunk_size;
	struct msghdr msg = { 0 };
	struct iovec iov;
	char *chunk;
	int fd, res;
	FILE *f;

	f = fopen("/proc/sys/net/core/optmem_max", "r");
	if (!f) {
		perror("fopen(optmem_max)");
		exit(EXIT_FAILURE);
	}

	if (fscanf(f, "%zu", &optmem_max) != 1) {
		fprintf(stderr, "fscanf(optmem_max) failed\n");
		exit(EXIT_FAILURE);
	}

	fclose(f);

	fd = vsock_stream_connect(opts->peer_cid, opts->peer_port);
	if (fd < 0) {
		perror("connect");
		exit(EXIT_FAILURE);
	}

	enable_so_zerocopy_check(fd);

	ctl_len = optmem_max - 1;
	if (ctl_len > PAGE_SIZE << MAX_PAGE_ORDER) {
		fprintf(stderr, "Try with net.core.optmem_max = 100000\n");
		exit(EXIT_FAILURE);
	}

	chunk_size = CMSG_SPACE(ctl_len);
	chunk = malloc(chunk_size);
	if (!chunk) {
		perror("malloc");
		exit(EXIT_FAILURE);
	}
	memset(chunk, 0, chunk_size);

	iov.iov_base = &(char){ 0 };
	iov.iov_len = 1;

	msg.msg_iov = &iov;
	msg.msg_iovlen = 1;
	msg.msg_control = chunk;
	msg.msg_controllen = ctl_len;

	errno = 0;
	res = sendmsg(fd, &msg, MSG_ZEROCOPY);
	if (res >= 0 || errno != ENOMEM) {
		fprintf(stderr, "Expected ENOMEM, got errno=%d res=%d\n",
			errno, res);
		exit(EXIT_FAILURE);
	}

	close(fd);
}

static void test_stream_msgzcopy_leak_zcskb_server(const struct test_opts *opts)
{
	int fd;

	fd = vsock_stream_accept(VMADDR_CID_ANY, opts->peer_port, NULL);
	if (fd < 0) {
		perror("accept");
		exit(EXIT_FAILURE);
	}

	vsock_wait_remote_close(fd);
	close(fd);
}

static struct test_case test_cases[] = {
	{
		.name = "SOCK_STREAM connection reset",
@@ -1603,6 +1838,21 @@ static struct test_case test_cases[] = {
		.run_client = test_seqpacket_unsent_bytes_client,
		.run_server = test_seqpacket_unsent_bytes_server,
	},
	{
		.name = "SOCK_STREAM leak accept queue",
		.run_client = test_stream_leak_acceptq_client,
		.run_server = test_stream_leak_acceptq_server,
	},
	{
		.name = "SOCK_STREAM MSG_ZEROCOPY leak MSG_ERRQUEUE",
		.run_client = test_stream_msgzcopy_leak_errq_client,
		.run_server = test_stream_msgzcopy_leak_errq_server,
	},
	{
		.name = "SOCK_STREAM MSG_ZEROCOPY leak completion skb",
		.run_client = test_stream_msgzcopy_leak_zcskb_client,
		.run_server = test_stream_msgzcopy_leak_zcskb_server,
	},
	{},
};

@@ -1643,6 +1893,11 @@ static const struct option longopts[] = {
		.has_arg = required_argument,
		.val = 's',
	},
	{
		.name = "pick",
		.has_arg = required_argument,
		.val = 't',
	},
	{
		.name = "help",
		.has_arg = no_argument,
@@ -1680,6 +1935,8 @@ static void usage(void)
		"  --peer-cid <cid>       CID of the other side\n"
		"  --peer-port <port>     AF_VSOCK port used for the test [default: %d]\n"
		"  --list                 List of tests that will be executed\n"
		"  --pick <test_id>       Test ID to execute selectively;\n"
		"                         use multiple --pick options to select more tests\n"
		"  --skip <test_id>       Test ID to skip;\n"
		"                         use multiple --skip options to skip more tests\n",
		DEFAULT_PEER_PORT
@@ -1736,6 +1993,10 @@ int main(int argc, char **argv)
			skip_test(test_cases, ARRAY_SIZE(test_cases) - 1,
				  optarg);
			break;
		case 't':
			pick_test(test_cases, ARRAY_SIZE(test_cases) - 1,
				  optarg);
			break;
		case '?':
		default:
			usage();