[PATCH v3 4/4] selftests: misc: Add vmw_zerocopy selftest

From: Rishi Chhibber

Date: Mon Sep 14 2026 - 16:26:49 EST


Summary of changes:
- Add tools/testing/selftests/drivers/misc/vmw_zerocopy: a
kselftest_harness test covering the ioctl input-validation paths, plus
its Makefile and config fragment
- Add the target to tools/testing/selftests/Makefile

Cover the input-validation paths of the vmw_zerocopy ioctl added earlier in
this series: an unknown ioctl number, a bad message pointer, an unknown
message type, a non-zero reserved word, neither buffer supplied,
zero-length and oversized data and metadata buffers, a wrapping
address+length, a wrapping PAGE_ALIGN() of the end address, an unmapped
buffer, and zero-length and oversized raw payloads.

These are all rejected before any datagram is sent, so they need no
hypervisor peer. Two further tests submit a well-formed message and assert
only that any failure is not a validation error, which holds whether or not
a peer is listening; they also check that repeated sends do not depend on
per-call setup surviving. The whole test therefore runs on any guest where
the module loads, and skips cleanly when /dev/vmw_zc is absent so it does
not fail on kernels built without CONFIG_VMW_ZC.

Signed-off-by: Rishi Chhibber <rishi.chhibber@xxxxxxxxxxxx>
Reviewed-by: Alexey Makhalov <alexey.makhalov@xxxxxxxxxxxx>
Reviewed-by: Vishnu Dasa <vishnu.dasa@xxxxxxxxxxxx>
---
tools/testing/selftests/Makefile | 1 +
.../drivers/misc/vmw_zerocopy/.gitignore | 1 +
.../drivers/misc/vmw_zerocopy/Makefile | 20 ++
.../drivers/misc/vmw_zerocopy/config | 2 +
.../misc/vmw_zerocopy/test_vmw_zerocopy.c | 276 ++++++++++++++++++
5 files changed, 300 insertions(+)
create mode 100644 tools/testing/selftests/drivers/misc/vmw_zerocopy/.gitignore
create mode 100644 tools/testing/selftests/drivers/misc/vmw_zerocopy/Makefile
create mode 100644 tools/testing/selftests/drivers/misc/vmw_zerocopy/config
create mode 100644 tools/testing/selftests/drivers/misc/vmw_zerocopy/test_vmw_zerocopy.c

diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile
index 2d960626750e..325723e92bf3 100644
--- a/tools/testing/selftests/Makefile
+++ b/tools/testing/selftests/Makefile
@@ -20,6 +20,7 @@ TARGETS += devices/error_logs
TARGETS += devices/probe
TARGETS += dmabuf-heaps
TARGETS += drivers/dma-buf
+TARGETS += drivers/misc/vmw_zerocopy
TARGETS += drivers/ntsync
TARGETS += drivers/s390x/uvdevice
TARGETS += drivers/net
diff --git a/tools/testing/selftests/drivers/misc/vmw_zerocopy/.gitignore b/tools/testing/selftests/drivers/misc/vmw_zerocopy/.gitignore
new file mode 100644
index 000000000000..68941a8c0553
--- /dev/null
+++ b/tools/testing/selftests/drivers/misc/vmw_zerocopy/.gitignore
@@ -0,0 +1 @@
+test_vmw_zerocopy
diff --git a/tools/testing/selftests/drivers/misc/vmw_zerocopy/Makefile b/tools/testing/selftests/drivers/misc/vmw_zerocopy/Makefile
new file mode 100644
index 000000000000..195826f72f9c
--- /dev/null
+++ b/tools/testing/selftests/drivers/misc/vmw_zerocopy/Makefile
@@ -0,0 +1,20 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+include ../../../../../build/Build.include
+
+UNAME_M := $(shell uname -m)
+
+ifeq ($(filter $(UNAME_M),x86_64 i686 i386),)
+nothing:
+.PHONY: all clean run_tests install
+.SILENT:
+else
+
+TEST_GEN_PROGS := test_vmw_zerocopy
+
+top_srcdir ?= ../../../../../..
+
+CFLAGS += -Wall -Werror $(KHDR_INCLUDES)
+
+include ../../../lib.mk
+
+endif
diff --git a/tools/testing/selftests/drivers/misc/vmw_zerocopy/config b/tools/testing/selftests/drivers/misc/vmw_zerocopy/config
new file mode 100644
index 000000000000..03e4a430c5ff
--- /dev/null
+++ b/tools/testing/selftests/drivers/misc/vmw_zerocopy/config
@@ -0,0 +1,2 @@
+CONFIG_VMWARE_VMCI=m
+CONFIG_VMW_ZC=m
diff --git a/tools/testing/selftests/drivers/misc/vmw_zerocopy/test_vmw_zerocopy.c b/tools/testing/selftests/drivers/misc/vmw_zerocopy/test_vmw_zerocopy.c
new file mode 100644
index 000000000000..af8035c4fa0f
--- /dev/null
+++ b/tools/testing/selftests/drivers/misc/vmw_zerocopy/test_vmw_zerocopy.c
@@ -0,0 +1,276 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * selftest for the VMware zero-copy buffer sharing UAPI
+ *
+ * Copyright (c) 2026 Broadcom. All Rights Reserved. The term
+ * "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
+ *
+ * Every input check in the driver runs before the message reaches the
+ * transport, so all of the tests below are meaningful without a hypervisor
+ * peer listening: they assert that bad input is rejected with the documented
+ * errno. Tests that would require a live peer only assert "not one of the
+ * input-validation errnos", since the transport result depends on the host.
+ */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdint.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <unistd.h>
+
+#include <linux/vmw_zerocopy.h>
+
+#include "../../../kselftest_harness.h"
+
+#define VMW_ZC_PATH "/dev/" VMW_ZC_DEVICE_NAME
+
+/* Driver-private, but fixed by the UAPI's documented buffer limits. */
+#define VMW_ZC_TEST_MAX_BUFFER 65536
+
+FIXTURE(vmw_zc) {
+ int fd;
+ void *data;
+ void *metadata;
+ size_t page_size;
+};
+
+FIXTURE_SETUP(vmw_zc)
+{
+ self->page_size = (size_t)getpagesize();
+
+ self->fd = open(VMW_ZC_PATH, O_RDWR);
+ if (self->fd < 0)
+ SKIP(return, "cannot open %s: %s", VMW_ZC_PATH,
+ strerror(errno));
+
+ self->data = mmap(NULL, self->page_size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ ASSERT_NE(MAP_FAILED, self->data);
+
+ self->metadata = mmap(NULL, self->page_size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ ASSERT_NE(MAP_FAILED, self->metadata);
+
+ memset(self->data, 0xa5, self->page_size);
+}
+
+FIXTURE_TEARDOWN(vmw_zc)
+{
+ if (self->data)
+ munmap(self->data, self->page_size);
+ if (self->metadata)
+ munmap(self->metadata, self->page_size);
+ if (self->fd >= 0)
+ close(self->fd);
+}
+
+/* An unknown ioctl number must be rejected, not silently accepted. */
+TEST_F(vmw_zc, unknown_ioctl)
+{
+ struct vmw_zc_guest_message msg = { };
+
+ EXPECT_EQ(-1, ioctl(self->fd, _IOW(VMW_ZC_IOCTL_MAGIC, 0x7f,
+ struct vmw_zc_guest_message), &msg));
+ EXPECT_EQ(ENOTTY, errno);
+}
+
+TEST_F(vmw_zc, bad_message_pointer)
+{
+ EXPECT_EQ(-1, ioctl(self->fd, VMW_ZC_IOCTL_MSG, NULL));
+ EXPECT_EQ(EFAULT, errno);
+}
+
+TEST_F(vmw_zc, unknown_message_type)
+{
+ struct vmw_zc_guest_message msg = { };
+
+ msg.message_type = 0xffff;
+ EXPECT_EQ(-1, ioctl(self->fd, VMW_ZC_IOCTL_MSG, &msg));
+ EXPECT_EQ(EINVAL, errno);
+}
+
+/*
+ * The destination is owned by the kernel: @reserved is not an address and a
+ * caller that treats it as one (e.g. a binary built against the older header
+ * where this word was peer_id) must fail loudly rather than be misrouted.
+ */
+TEST_F(vmw_zc, reserved_must_be_zero)
+{
+ struct vmw_zc_guest_message msg = { };
+
+ msg.message_type = VMW_ZC_MSG_USER_BUFFER;
+ msg.reserved = 1;
+ msg.u.data.buffer = (__u64)(uintptr_t)self->data;
+ msg.u.data.buffer_length = 64;
+
+ EXPECT_EQ(-1, ioctl(self->fd, VMW_ZC_IOCTL_MSG, &msg));
+ EXPECT_EQ(EINVAL, errno);
+}
+
+TEST_F(vmw_zc, neither_buffer_nor_metadata)
+{
+ struct vmw_zc_guest_message msg = { };
+
+ msg.message_type = VMW_ZC_MSG_USER_BUFFER;
+
+ EXPECT_EQ(-1, ioctl(self->fd, VMW_ZC_IOCTL_MSG, &msg));
+ EXPECT_EQ(EINVAL, errno);
+}
+
+TEST_F(vmw_zc, zero_length_buffer)
+{
+ struct vmw_zc_guest_message msg = { };
+
+ msg.message_type = VMW_ZC_MSG_USER_BUFFER;
+ msg.u.data.buffer = (__u64)(uintptr_t)self->data;
+ msg.u.data.buffer_length = 0;
+
+ EXPECT_EQ(-1, ioctl(self->fd, VMW_ZC_IOCTL_MSG, &msg));
+ EXPECT_EQ(EINVAL, errno);
+}
+
+TEST_F(vmw_zc, oversized_buffer)
+{
+ struct vmw_zc_guest_message msg = { };
+
+ msg.message_type = VMW_ZC_MSG_USER_BUFFER;
+ msg.u.data.buffer = (__u64)(uintptr_t)self->data;
+ msg.u.data.buffer_length = VMW_ZC_TEST_MAX_BUFFER + 1;
+
+ EXPECT_EQ(-1, ioctl(self->fd, VMW_ZC_IOCTL_MSG, &msg));
+ EXPECT_EQ(EINVAL, errno);
+}
+
+TEST_F(vmw_zc, oversized_metadata)
+{
+ struct vmw_zc_guest_message msg = { };
+
+ msg.message_type = VMW_ZC_MSG_USER_BUFFER;
+ msg.u.data.metadata = (__u64)(uintptr_t)self->metadata;
+ msg.u.data.metadata_length = VMW_ZC_TEST_MAX_BUFFER + 1;
+
+ EXPECT_EQ(-1, ioctl(self->fd, VMW_ZC_IOCTL_MSG, &msg));
+ EXPECT_EQ(EINVAL, errno);
+}
+
+/*
+ * A near-top address must be rejected by the range check before any page-count
+ * arithmetic runs, whether it wraps on start + length or only on the
+ * subsequent page alignment.
+ */
+TEST_F(vmw_zc, address_length_overflow)
+{
+ struct vmw_zc_guest_message msg = { };
+
+ msg.message_type = VMW_ZC_MSG_USER_BUFFER;
+ msg.u.data.buffer = ~(__u64)0 - 15;
+ msg.u.data.buffer_length = 4096;
+
+ EXPECT_EQ(-1, ioctl(self->fd, VMW_ZC_IOCTL_MSG, &msg));
+ EXPECT_EQ(EFAULT, errno);
+}
+
+/*
+ * The page-alignment-only wrap: start + length does not overflow, but rounding
+ * the end up to a page boundary would, which previously yielded nr_pages == 1.
+ */
+TEST_F(vmw_zc, address_page_align_overflow)
+{
+ struct vmw_zc_guest_message msg = { };
+
+ msg.message_type = VMW_ZC_MSG_USER_BUFFER;
+ msg.u.data.buffer = ~(__u64)0 - 0x7ff;
+ msg.u.data.buffer_length = 0x800;
+
+ EXPECT_EQ(-1, ioctl(self->fd, VMW_ZC_IOCTL_MSG, &msg));
+ EXPECT_EQ(EFAULT, errno);
+}
+
+TEST_F(vmw_zc, unmapped_buffer)
+{
+ struct vmw_zc_guest_message msg = { };
+ void *gap;
+
+ gap = mmap(NULL, self->page_size, PROT_NONE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ ASSERT_NE(MAP_FAILED, gap);
+ ASSERT_EQ(0, munmap(gap, self->page_size));
+
+ msg.message_type = VMW_ZC_MSG_USER_BUFFER;
+ msg.u.data.buffer = (__u64)(uintptr_t)gap;
+ msg.u.data.buffer_length = 64;
+
+ EXPECT_EQ(-1, ioctl(self->fd, VMW_ZC_IOCTL_MSG, &msg));
+ EXPECT_EQ(EFAULT, errno);
+}
+
+TEST_F(vmw_zc, zero_length_raw)
+{
+ struct vmw_zc_guest_message msg = { };
+
+ msg.message_type = VMW_ZC_MSG_RAW;
+ msg.u.raw_buffer.raw_len = 0;
+
+ EXPECT_EQ(-1, ioctl(self->fd, VMW_ZC_IOCTL_MSG, &msg));
+ EXPECT_EQ(EINVAL, errno);
+}
+
+TEST_F(vmw_zc, oversized_raw)
+{
+ struct vmw_zc_guest_message msg = { };
+
+ msg.message_type = VMW_ZC_MSG_RAW;
+ msg.u.raw_buffer.raw_len = VMW_ZC_MAX_RAW_BUFFER_LEN + 1;
+
+ EXPECT_EQ(-1, ioctl(self->fd, VMW_ZC_IOCTL_MSG, &msg));
+ EXPECT_EQ(EINVAL, errno);
+}
+
+/*
+ * A well-formed send passes every input check, so it must not fail with one
+ * of the validation errnos. Whether it ultimately succeeds depends on a peer
+ * being present, which this test cannot assume.
+ */
+TEST_F(vmw_zc, well_formed_send_passes_validation)
+{
+ struct vmw_zc_guest_message msg = { };
+ int rc;
+
+ msg.message_type = VMW_ZC_MSG_USER_BUFFER;
+ msg.u.data.buffer = (__u64)(uintptr_t)self->data;
+ msg.u.data.buffer_length = 64;
+ msg.u.data.metadata = (__u64)(uintptr_t)self->metadata;
+ msg.u.data.metadata_length = 64;
+
+ rc = ioctl(self->fd, VMW_ZC_IOCTL_MSG, &msg);
+ if (rc < 0) {
+ EXPECT_NE(EINVAL, errno);
+ EXPECT_NE(EFAULT, errno);
+ EXPECT_NE(ENOTTY, errno);
+ TH_LOG("send returned %s; a hypervisor peer may not be present",
+ strerror(errno));
+ }
+}
+
+/* Repeated sends must not depend on any per-call setup surviving. */
+TEST_F(vmw_zc, repeated_raw_sends)
+{
+ struct vmw_zc_guest_message msg = { };
+ int i;
+
+ msg.message_type = VMW_ZC_MSG_RAW;
+ msg.u.raw_buffer.raw_len = VMW_ZC_MAX_RAW_BUFFER_LEN;
+ memset(msg.u.raw_buffer.raw_buffer, 0x5a,
+ sizeof(msg.u.raw_buffer.raw_buffer));
+
+ for (i = 0; i < 16; i++) {
+ if (ioctl(self->fd, VMW_ZC_IOCTL_MSG, &msg) < 0) {
+ EXPECT_NE(EINVAL, errno);
+ EXPECT_NE(ENOTTY, errno);
+ }
+ }
+}
+
+TEST_HARNESS_MAIN
--
2.52.0