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 d7b0ff5a86 ("virtio/vsock: Fix accept_queue memory leak")
commit fbf7085b3a ("vsock: Fix sk_error_queue memory leak")
commit 60cf6206a1 ("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: Jakub Kicinski <kuba@kernel.org>
This commit is contained in:
Jakub Kicinski
2024-12-23 10:29:00 -08:00
4 changed files with 309 additions and 6 deletions

View File

@@ -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

View File

@@ -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;

View File

@@ -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);

View File

@@ -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();