[PATCH v2 12/14] hv: netvsc: allocate RNDIS request descriptors with kvzalloc_obj()
flat view
HOTtoday
From: Emerson Busson <hidden>
Date: 2026-10-07 19:09:14
Also in:
linux-hyperv, lkml
Subsystem:
hyper-v/azure core and drivers, networking drivers, the rest · Maintainers:
Haiyang Zhang, Wei Liu, Dexuan Cui, Andrew Lunn, "David S. Miller", Eric Dumazet, Jakub Kicinski, Paolo Abeni, Linus Torvalds
struct rndis_request embeds two RNDIS_EXT_LEN protocol tails.
RNDIS_EXT_LEN is HV_HYP_PAGE_SIZE, so the two tails alone are 8 KiB and
the struct is just over KMALLOC_MAX_CACHE_SIZE. kzalloc_obj() therefore
needs an order-2 compound page for every RNDIS control request, which can
fail under buddy fragmentation.
Use kvzalloc_obj() and kvfree() so allocation can fall back to vmalloc.
RNDIS control payloads are sent as GPA-direct page buffers, however, so a
vmalloc-backed descriptor cannot be described by one virt_to_phys() PFN.
Build one hv_page_buffer per Linux/Hyper-V page and resolve vmalloc backing
with vmalloc_to_page(). Linear allocations use virt_to_page(). This keeps
the GPA list valid and preserves the existing per-range DMA mapping for
isolated guests.
Keep both protocol tails unchanged. The response is copied after
request->response_msg into response_ext, within the existing
sizeof(struct rndis_message) + RNDIS_EXT_LEN bound.
Adds a fifth named RNDIS request KUnit case. It builds GPA descriptors from
a vmalloc buffer that crosses a page boundary and checks every PFN, offset,
and length against the backing pages. The existing cases retain the
allocation-size, response-layout, zeroing, request-id, and cleanup checks.
Rollback trigger: revert if the vmalloc page-buffer KUnit case fails or an
exact-source fragmented rebind shows malformed GPA descriptors from this
path.
Fixes: 5b54dac856cb ("hyperv: Add support for virtual Receive Side Scaling (vRSS)")
Signed-off-by: Emerson Busson <redacted>
---
drivers/net/hyperv/Kconfig | 12 ++
drivers/net/hyperv/Makefile | 1 +
drivers/net/hyperv/hyperv_net.h | 43 ++++-
drivers/net/hyperv/rndis_filter.c | 117 ++++++++-----
drivers/net/hyperv/rndis_request_test.c | 216 ++++++++++++++++++++++++
5 files changed, 349 insertions(+), 40 deletions(-)
create mode 100644 drivers/net/hyperv/rndis_request_test.c
diff --git a/drivers/net/hyperv/Kconfig b/drivers/net/hyperv/Kconfig
index 982964c1a9fb..226728fd77d7 100644
--- a/drivers/net/hyperv/Kconfig
+++ b/drivers/net/hyperv/Kconfig@@ -6,3 +6,15 @@ config HYPERV_NET select NLS help Select this option to enable the Hyper-V virtual network driver. + +config HYPERV_NET_KUNIT_TEST + bool "Build Hyper-V netvsc KUnit tests" + depends on HYPERV_NET && KUNIT + default KUNIT_ALL_TESTS + help + Build the netvsc KUnit test suites into the hv_netvsc object. + Built into the module rather than as a separate one so the + cases can reach the internal helpers declared in hyperv_net.h + without exporting them. + + If unsure, say N.
diff --git a/drivers/net/hyperv/Makefile b/drivers/net/hyperv/Makefile
index 0db7ccaec4a4..6f1abc756fde 100644
--- a/drivers/net/hyperv/Makefile
+++ b/drivers/net/hyperv/Makefile@@ -2,3 +2,4 @@ obj-$(CONFIG_HYPERV_NET) += hv_netvsc.o hv_netvsc-y := netvsc_drv.o netvsc.o rndis_filter.o netvsc_trace.o netvsc_bpf.o +hv_netvsc-$(CONFIG_HYPERV_NET_KUNIT_TEST) += rndis_request_test.o
diff --git a/drivers/net/hyperv/hyperv_net.h b/drivers/net/hyperv/hyperv_net.h
index a15cb2460344..6492e7e93ded 100644
--- a/drivers/net/hyperv/hyperv_net.h
+++ b/drivers/net/hyperv/hyperv_net.h@@ -210,14 +210,25 @@ struct rndis_device { u8 rss_key[NETVSC_HASH_KEYLEN]; }; +#define RNDIS_EXT_LEN HV_HYP_PAGE_SIZE -/* Interface */ +/* Full definitions are below, after the RNDIS message format. */ +struct rndis_request; struct rndis_message; + +/* Interface */ struct ndis_offload_params; struct netvsc_device; struct netvsc_channel; struct net_device_context; +struct rndis_request *get_rndis_request(struct rndis_device *dev, + u32 msg_type, u32 msg_len); +void put_rndis_request(struct rndis_device *dev, struct rndis_request *req); +int rndis_build_page_buffers(const void *data, u32 len, + struct hv_page_buffer *page_bufs, + u32 *page_buf_cnt); + extern u32 netvsc_ring_bytes; int netvsc_workqueue_init(void);
@@ -1779,6 +1790,36 @@ struct rndis_message { union rndis_message_container msg; }; +/* + * RNDIS request descriptor. Declared here so the KUnit cases can reach + * the layout the response-copy bound in rndis_filter_receive_resp() + * relies on. The two RNDIS_EXT_LEN tails are what make the object + * larger than KMALLOC_MAX_CACHE_SIZE. + */ +struct rndis_request { + struct list_head list_ent; + struct completion wait_event; + + struct rndis_message response_msg; + /* + * The buffer for extended info after the RNDIS response message. It's + * referenced based on the data offset in the RNDIS message. Its size + * is enough for current needs, and should be sufficient for the near + * future. + */ + u8 response_ext[RNDIS_EXT_LEN]; + + /* Simplify allocation by having a netvsc packet inline */ + struct hv_netvsc_packet pkt; + + struct rndis_message request_msg; + /* + * The buffer for the extended info after the RNDIS request message. + * It is referenced and sized in a similar way as response_ext. + */ + u8 request_ext[RNDIS_EXT_LEN]; +}; + /* Handy macros */
diff --git a/drivers/net/hyperv/rndis_filter.c b/drivers/net/hyperv/rndis_filter.c
index 9b6c44979b4e..1ce40baa2d23 100644
--- a/drivers/net/hyperv/rndis_filter.c
+++ b/drivers/net/hyperv/rndis_filter.c@@ -16,6 +16,7 @@ #include <linux/if_ether.h> #include <linux/netdevice.h> #include <linux/if_vlan.h> +#include <linux/mm.h> #include <linux/nls.h> #include <linux/vmalloc.h> #include <linux/rtnetlink.h>
@@ -27,31 +28,6 @@ static void rndis_set_multicast(struct work_struct *w); -#define RNDIS_EXT_LEN HV_HYP_PAGE_SIZE -struct rndis_request { - struct list_head list_ent; - struct completion wait_event; - - struct rndis_message response_msg; - /* - * The buffer for extended info after the RNDIS response message. It's - * referenced based on the data offset in the RNDIS message. Its size - * is enough for current needs, and should be sufficient for the near - * future. - */ - u8 response_ext[RNDIS_EXT_LEN]; - - /* Simplify allocation by having a netvsc packet inline */ - struct hv_netvsc_packet pkt; - - struct rndis_message request_msg; - /* - * The buffer for the extended info after the RNDIS request message. - * It is referenced and sized in a similar way as response_ext. - */ - u8 request_ext[RNDIS_EXT_LEN]; -}; - static const u8 netvsc_hash_key[NETVSC_HASH_KEYLEN] = { 0x6d, 0x5a, 0x56, 0xda, 0x25, 0x5b, 0x0e, 0xc2, 0x41, 0x67, 0x25, 0x3d, 0x43, 0xa3, 0x8f, 0xb0,
@@ -78,16 +54,27 @@ static struct rndis_device *get_rndis_device(void) return device; } -static struct rndis_request *get_rndis_request(struct rndis_device *dev, - u32 msg_type, - u32 msg_len) +struct rndis_request *get_rndis_request(struct rndis_device *dev, + u32 msg_type, + u32 msg_len) { struct rndis_request *request; struct rndis_message *rndis_msg; struct rndis_set_request *set; unsigned long flags; - request = kzalloc_obj(struct rndis_request); + /* + * struct rndis_request embeds two RNDIS_EXT_LEN (4 KiB) protocol + * tails and is larger than KMALLOC_MAX_CACHE_SIZE, so + * kzalloc_obj() needs an order-2 compound page. That high-order + * allocation can fail under buddy fragmentation. kvzalloc_obj() + * keeps the zeroing semantics while allowing vmalloc backing when + * a suitable kmalloc allocation is unavailable. The request payload + * is sent as GPADL-direct page buffers, so rndis_filter_send_request() + * must describe each backing page instead of assuming virt_to_phys() + * yields one contiguous physical range. + */ + request = kvzalloc_obj(struct rndis_request); if (!request) return NULL;
@@ -115,8 +102,8 @@ static struct rndis_request *get_rndis_request(struct rndis_device *dev, return request; } -static void put_rndis_request(struct rndis_device *dev, - struct rndis_request *req) +void put_rndis_request(struct rndis_device *dev, + struct rndis_request *req) { unsigned long flags;
@@ -124,7 +111,8 @@ static void put_rndis_request(struct rndis_device *dev, list_del(&req->list_ent); spin_unlock_irqrestore(&dev->request_lock, flags); - kfree(req); + /* Paired with the kvzalloc_obj() in get_rndis_request(). */ + kvfree(req); } static void dump_rndis_message(struct net_device *netdev,
@@ -221,27 +209,78 @@ static void dump_rndis_message(struct net_device *netdev, } } +int rndis_build_page_buffers(const void *data, u32 len, + struct hv_page_buffer *page_bufs, + u32 *page_buf_cnt) +{ + const u8 *addr = data; + u32 count = 0; + + if (!data || !len || !page_bufs || !page_buf_cnt) + return -EINVAL; + + *page_buf_cnt = 0; + + while (len) { + struct page *page; + phys_addr_t phys; + u32 offset, chunk; + unsigned long page_offset = offset_in_page(addr); + + if (count == MAX_PAGE_BUFFER_COUNT) + return -E2BIG; + + if (is_vmalloc_addr(addr)) + page = vmalloc_to_page(addr); + else if (virt_addr_valid(addr)) + page = virt_to_page(addr); + else + return -EFAULT; + + if (!page) + return -EFAULT; + + phys = page_to_phys(page) + page_offset; + offset = offset_in_hvpage(phys); + chunk = min_t(u32, len, PAGE_SIZE - page_offset); + chunk = min_t(u32, chunk, HV_HYP_PAGE_SIZE - offset); + + page_bufs[count].pfn = phys >> HV_HYP_PAGE_SHIFT; + page_bufs[count].offset = offset; + page_bufs[count].len = chunk; + + addr += chunk; + len -= chunk; + count++; + } + + *page_buf_cnt = count; + return 0; +} + static int rndis_filter_send_request(struct rndis_device *dev, struct rndis_request *req) { struct hv_netvsc_packet *packet; - struct hv_page_buffer pb; + struct hv_page_buffer page_bufs[MAX_PAGE_BUFFER_COUNT]; + u32 page_buf_cnt; int ret; /* Setup the packet to send it */ packet = &req->pkt; packet->total_data_buflen = req->request_msg.msg_len; - packet->page_buf_cnt = 1; - - pb.pfn = virt_to_phys(&req->request_msg) >> HV_HYP_PAGE_SHIFT; - pb.len = req->request_msg.msg_len; - pb.offset = offset_in_hvpage(&req->request_msg); + ret = rndis_build_page_buffers(&req->request_msg, + req->request_msg.msg_len, + page_bufs, &page_buf_cnt); + if (ret) + return ret; + packet->page_buf_cnt = page_buf_cnt; trace_rndis_send(dev->ndev, 0, &req->request_msg); rcu_read_lock_bh(); - ret = netvsc_send(dev->ndev, packet, NULL, &pb, NULL, false); + ret = netvsc_send(dev->ndev, packet, NULL, page_bufs, NULL, false); rcu_read_unlock_bh(); return ret;
diff --git a/drivers/net/hyperv/rndis_request_test.c b/drivers/net/hyperv/rndis_request_test.c
new file mode 100644
index 000000000000..6affa916e74e
--- /dev/null
+++ b/drivers/net/hyperv/rndis_request_test.c@@ -0,0 +1,216 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * KUnit tests for RNDIS request descriptor allocation. + * + * Built into the hv_netvsc object rather than a separate module so the + * cases can reach the internal helpers declared in hyperv_net.h + * without exporting them. + */ +#include <kunit/test.h> +#include <linux/hyperv.h> +#include <linux/mm.h> +#include <linux/slab.h> +#include <linux/stddef.h> +#include <linux/vmalloc.h> + +#include "hyperv_net.h" + +static int rndis_test_device_init(struct rndis_device *dev) +{ + memset(dev, 0, sizeof(*dev)); + atomic_set(&dev->new_req_id, 0); + spin_lock_init(&dev->request_lock); + INIT_LIST_HEAD(&dev->req_list); + return 0; +} + +/* + * The premise of the kvzalloc_obj() conversion: the descriptor embeds + * two RNDIS_EXT_LEN protocol tails and sits past + * KMALLOC_MAX_CACHE_SIZE, so kzalloc_obj() reached kmalloc_large() and + * took an order-2 compound page on the subchannel open path. If the + * struct ever drops back inside the cache, the conversion stops being + * about high-order allocation and this assertion is the reminder. + */ +static void rndis_request_size_premise_test(struct kunit *test) +{ + KUNIT_EXPECT_GT(test, sizeof(struct rndis_request), + (size_t)KMALLOC_MAX_CACHE_SIZE); + /* + * The copy bound in rndis_filter_receive_resp() is + * sizeof(struct rndis_message) + RNDIS_EXT_LEN. That is exactly + * the response_msg + response_ext tail of the object. A reorder + * or a resize of either member makes the bound lie. + */ + KUNIT_EXPECT_EQ(test, + offsetofend(struct rndis_request, response_ext), + offsetof(struct rndis_request, response_msg) + + sizeof(struct rndis_message) + RNDIS_EXT_LEN); + KUNIT_EXPECT_EQ(test, + offsetofend(struct rndis_request, request_ext), + offsetof(struct rndis_request, request_msg) + + sizeof(struct rndis_message) + RNDIS_EXT_LEN); +} + +/* + * kvzalloc_obj() keeps the zeroing kzalloc_obj() provided. The two + * protocol tails are the fields a response is copied into; a stale + * tail is a stale response. + */ +static void rndis_request_alloc_zeroed_test(struct kunit *test) +{ + struct rndis_device dev; + struct rndis_request *req; + + KUNIT_ASSERT_EQ(test, rndis_test_device_init(&dev), 0); + req = get_rndis_request(&dev, RNDIS_MSG_INIT, 0x20); + KUNIT_ASSERT_NOT_NULL(test, req); + + KUNIT_EXPECT_EQ(test, req->response_ext[0], 0); + KUNIT_EXPECT_EQ(test, req->response_ext[RNDIS_EXT_LEN - 1], 0); + KUNIT_EXPECT_EQ(test, req->request_ext[0], 0); + KUNIT_EXPECT_EQ(test, req->request_ext[RNDIS_EXT_LEN - 1], 0); + KUNIT_EXPECT_EQ(test, req->response_msg.msg_len, 0U); + + put_rndis_request(&dev, req); +} + +/* Request ids come from a per-device counter and the list tracks the live set. */ +static void rndis_request_id_lifecycle_test(struct kunit *test) +{ + struct rndis_device dev; + struct rndis_request *req0, *req1; + struct rndis_request *cursor; + unsigned int n = 0; + + KUNIT_ASSERT_EQ(test, rndis_test_device_init(&dev), 0); + + req0 = get_rndis_request(&dev, RNDIS_MSG_INIT, 0x20); + KUNIT_ASSERT_NOT_NULL(test, req0); + req1 = get_rndis_request(&dev, RNDIS_MSG_QUERY, 0x20); + KUNIT_ASSERT_NOT_NULL(test, req1); + + /* + * get_rndis_request() stamps the id through the set_req template, + * which shares the union slot with every other request body. Read + * it back through the same member the producer used. + */ + KUNIT_EXPECT_EQ(test, + req0->request_msg.msg.set_req.req_id, 1U); + KUNIT_EXPECT_EQ(test, + req1->request_msg.msg.set_req.req_id, 2U); + + list_for_each_entry(cursor, &dev.req_list, list_ent) + n++; + KUNIT_EXPECT_EQ(test, n, 2U); + + /* + * put_rndis_request() is the cleanup path every error and + * timeout branch takes. It unlinks before it frees. + */ + put_rndis_request(&dev, req0); + n = 0; + list_for_each_entry(cursor, &dev.req_list, list_ent) + n++; + KUNIT_EXPECT_EQ(test, n, 1U); + KUNIT_EXPECT_TRUE(test, + list_first_entry(&dev.req_list, struct rndis_request, + list_ent) == req1); + + put_rndis_request(&dev, req1); + KUNIT_EXPECT_TRUE(test, list_empty(&dev.req_list)); +} + +/* + * A request left on the list after a completed exchange is a leak of + * the descriptor and of its slot in the outstanding set. Repeated + * get/put cycles must return to the empty list every time. + */ +static void rndis_request_put_cleanup_test(struct kunit *test) +{ + struct rndis_device dev; + struct rndis_request *req; + int i; + + KUNIT_ASSERT_EQ(test, rndis_test_device_init(&dev), 0); + + for (i = 0; i < 4; i++) { + req = get_rndis_request(&dev, RNDIS_MSG_INIT, 0x20); + KUNIT_ASSERT_NOT_NULL(test, req); + KUNIT_EXPECT_FALSE(test, list_empty(&dev.req_list)); + put_rndis_request(&dev, req); + KUNIT_EXPECT_TRUE(test, list_empty(&dev.req_list)); + } +} + +/* + * kvzalloc_obj() may return a vmalloc address under fragmentation. RNDIS + * requests are GPA-direct packets, so each backing page must be represented + * explicitly instead of deriving a single PFN with virt_to_phys(). + */ +static void rndis_request_vmalloc_page_buffers_test(struct kunit *test) +{ + struct hv_page_buffer page_bufs[MAX_PAGE_BUFFER_COUNT]; + const u32 data_len = 16; + u8 *allocation = vzalloc(2 * PAGE_SIZE); + const u8 *cursor; + phys_addr_t phys; + u32 count = 0, remaining = data_len, i; + int ret; + + if (!allocation) { + KUNIT_FAIL(test, "could not allocate vmalloc test buffer"); + return; + } + + cursor = allocation + PAGE_SIZE - 8; + ret = rndis_build_page_buffers(cursor, data_len, page_bufs, &count); + KUNIT_EXPECT_EQ(test, ret, 0); + if (ret) + goto out; + + KUNIT_EXPECT_EQ(test, count, 2U); + for (i = 0; i < count; i++) { + struct page *page = vmalloc_to_page(cursor); + unsigned long page_offset = offset_in_page(cursor); + u32 offset, chunk; + + if (!page) { + KUNIT_FAIL(test, "vmalloc test buffer has no backing page"); + goto out; + } + + phys = page_to_phys(page) + page_offset; + offset = offset_in_hvpage(phys); + chunk = min_t(u32, remaining, PAGE_SIZE - page_offset); + chunk = min_t(u32, chunk, HV_HYP_PAGE_SIZE - offset); + + KUNIT_EXPECT_EQ(test, page_bufs[i].pfn, + (u64)(phys >> HV_HYP_PAGE_SHIFT)); + KUNIT_EXPECT_EQ(test, page_bufs[i].offset, offset); + KUNIT_EXPECT_EQ(test, page_bufs[i].len, chunk); + + cursor += chunk; + remaining -= chunk; + } + KUNIT_EXPECT_EQ(test, remaining, 0U); + +out: + vfree(allocation); +} + +static struct kunit_case rndis_request_test_cases[] = { + KUNIT_CASE(rndis_request_size_premise_test), + KUNIT_CASE(rndis_request_alloc_zeroed_test), + KUNIT_CASE(rndis_request_id_lifecycle_test), + KUNIT_CASE(rndis_request_put_cleanup_test), + KUNIT_CASE(rndis_request_vmalloc_page_buffers_test), + {} +}; + +static struct kunit_suite rndis_request_test_suite = { + .name = "hyperv-rndis-request", + .test_cases = rndis_request_test_cases, +}; + +kunit_test_suite(rndis_request_test_suite);
--
2.43.0