Commit d127ac8b authored by Michal Luczaj's avatar Michal Luczaj Committed by Jakub Kicinski
Browse files

vsock/test: Add test for MSG_ZEROCOPY completion memory leak



Exercise the ENOMEM error path by attempting to hit net.core.optmem_max
limit on send().

Test aims to create a memory leak, kmemleak should be employed.

Fixed by commit 60cf6206 ("virtio/vsock: Improve MSG_ZEROCOPY error
handling").

Reviewed-by: default avatarStefano Garzarella <sgarzare@redhat.com>
Signed-off-by: default avatarMichal Luczaj <mhal@rbox.co>
Link: https://patch.msgid.link/20241219-test-vsock-leaks-v4-7-a416e554d9d7@rbox.co


Signed-off-by: default avatarJakub Kicinski <kuba@kernel.org>
parent ec50efee
Loading
Loading
Loading
Loading
+152 −0
Original line number Diff line number Diff line
@@ -1561,6 +1561,153 @@ static void test_stream_msgzcopy_leak_errq_server(const struct test_opts *opts)
	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",
@@ -1701,6 +1848,11 @@ static struct test_case test_cases[] = {
		.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,
	},
	{},
};