[PATCH v2 13/14] hv: netvsc: handle a NULL request address on empty completions
flat view
COOLING12d
From: Emerson Busson <hidden>
Date: 2026-10-07 19:09:20
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
netvsc_send_pkt() registers (ulong)skb as the VMBus request address.
Control RNDIS sends carry no skb -- rndis_filter.c calls netvsc_send()
with skb == NULL -- so the registered address is NULL. That is
intentional: there is no guest object to hand back, and the request
slot is still allocated so the completion can reclaim it.
netvsc_send_tx_complete() already tolerates that with if (likely(skb)).
netvsc_send_completion()'s empty-payload branch does not. It casts the
request address to struct nvsp_message * and reads hdr.msg_type
unconditionally, so a completion that resolves to NULL is a fatal NULL
dereference in NAPI/softirq context.
The empty-payload branch exists for NVSP_MSG4_TYPE_SWITCH_DATA_PATH,
which netvsc_switch_datapath() sends with a real nvsp_message address.
A NULL request address is not that message and must not be
dereferenced. Validate VMBUS_NO_RQSTOR alongside VMBUS_RQST_ERROR in
both completion paths -- request_addr_callback() returns it when the
channel has no requestor -- and account a NULL-address completion the
same way the payload path accounts a NULL skb. The request id is
consumed by the lookup, so that completion is the one that owns the
queue_sends decrement and the possible queue wake.
This is reached by the runtime drill in this series once channel open
survives buddy fragmentation: control RNDIS traffic proceeds where it
used to fail with -ENOMEM, and an empty completion for one of those
requests takes the unguarded path.
Revert this patch if an empty completion with a NULL request address
dereferences again, if a netvsc TX stall or an "Invalid transaction
ID" flood appears under this patch, or if a SWITCH_DATA_PATH
completion is shown to be dropped instead of taken through its
nvsp_message path.
Adds eleven named netvsc completion KUnit cases covering the decision
layer of the empty-payload branch: a NULL request address owning the
queue_sends decrement, both invalid sentinels rejected without
accounting, SWITCH_DATA_PATH completing channel_init_wait without
accounting, an unexpected message type taking neither path, a
replayed transaction id accounting exactly once, N completions
decrementing the queue exactly N times, the destroy drain wait waking
only at zero, and netvsc_send_tx_complete() applying the same
sentinel rejection while still accounting a NULL skb. Two skb-backed
payload cases exercise the production completion entry with success
and error status, a published send slot and queue 1. They verify
synchronous skb consumption, exact packet/byte statistics, selected
queue accounting and duplicate transaction rejection. Confidential
DMA unmapping remains platform-specific and is not exercised here.
netvsc_send_acct() is extracted so a test can observe the decrement
and the drain wake directly; it is not a behaviour change. The
helpers lose static and are declared in hyperv_net.h so the cases
reach them without a new EXPORT_SYMBOL_GPL, matching how the VMBus
buffer tests are built into hv_vmbus.
Fixes: 8b31f8c982b7 ("hv_netvsc: Wait for completion on request SWITCH_DATA_PATH")
Signed-off-by: Emerson Busson <redacted>
---
drivers/net/hyperv/Makefile | 1 +
drivers/net/hyperv/hyperv_net.h | 15 +
drivers/net/hyperv/netvsc.c | 81 ++--
drivers/net/hyperv/netvsc_completion_test.c | 439 ++++++++++++++++++++
4 files changed, 507 insertions(+), 29 deletions(-)
create mode 100644 drivers/net/hyperv/netvsc_completion_test.c
diff --git a/drivers/net/hyperv/Makefile b/drivers/net/hyperv/Makefile
index 6f1abc756fde..b2d22855fd8a 100644
--- a/drivers/net/hyperv/Makefile
+++ b/drivers/net/hyperv/Makefile@@ -2,4 +2,5 @@ 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) += netvsc_completion_test.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 6492e7e93ded..be3a63a168cf 100644
--- a/drivers/net/hyperv/hyperv_net.h
+++ b/drivers/net/hyperv/hyperv_net.h@@ -229,6 +229,21 @@ int rndis_build_page_buffers(const void *data, u32 len, struct hv_page_buffer *page_bufs, u32 *page_buf_cnt); +void netvsc_send_acct(struct net_device *ndev, + struct netvsc_device *net_device, + struct vmbus_channel *channel, + u16 q_idx); +void netvsc_send_tx_complete(struct net_device *ndev, + struct netvsc_device *net_device, + struct vmbus_channel *channel, + const struct vmpacket_descriptor *desc, + int budget); +void netvsc_send_completion(struct net_device *ndev, + struct netvsc_device *net_device, + struct vmbus_channel *incoming_channel, + const struct vmpacket_descriptor *desc, + int budget); + extern u32 netvsc_ring_bytes; int netvsc_workqueue_init(void);
diff --git a/drivers/net/hyperv/netvsc.c b/drivers/net/hyperv/netvsc.c
index fd8aa7a3dcb3..0d017f836b9e 100644
--- a/drivers/net/hyperv/netvsc.c
+++ b/drivers/net/hyperv/netvsc.c@@ -764,20 +764,45 @@ static inline void netvsc_free_send_slot(struct netvsc_device *net_device, sync_change_bit(index, net_device->send_section_map); } -static void netvsc_send_tx_complete(struct net_device *ndev, - struct netvsc_device *net_device, - struct vmbus_channel *channel, - const struct vmpacket_descriptor *desc, - int budget) +void netvsc_send_acct(struct net_device *ndev, + struct netvsc_device *net_device, + struct vmbus_channel *channel, + u16 q_idx) +{ + struct net_device_context *ndev_ctx = netdev_priv(ndev); + int queue_sends; + + queue_sends = + atomic_dec_return(&net_device->chan_table[q_idx].queue_sends); + + if (unlikely(net_device->destroy)) { + if (queue_sends == 0) + wake_up(&net_device->wait_drain); + } else { + struct netdev_queue *txq = netdev_get_tx_queue(ndev, q_idx); + + if (netif_tx_queue_stopped(txq) && !net_device->tx_disable && + (hv_get_avail_to_write_percent(&channel->outbound) > + RING_AVAIL_PERCENT_HIWATER || queue_sends < 1)) { + netif_tx_wake_queue(txq); + ndev_ctx->eth_stats.wake_queue++; + } + } +} + +void netvsc_send_tx_complete(struct net_device *ndev, + struct netvsc_device *net_device, + struct vmbus_channel *channel, + const struct vmpacket_descriptor *desc, + int budget) { struct net_device_context *ndev_ctx = netdev_priv(ndev); struct sk_buff *skb; u16 q_idx = 0; - int queue_sends; u64 cmd_rqst; cmd_rqst = channel->request_addr_callback(channel, desc->trans_id); - if (cmd_rqst == VMBUS_RQST_ERROR) { + if (cmd_rqst == VMBUS_RQST_ERROR || cmd_rqst == VMBUS_NO_RQSTOR) { netdev_err(ndev, "Invalid transaction ID %llx\n", desc->trans_id); return; }
@@ -806,29 +831,14 @@ static void netvsc_send_tx_complete(struct net_device *ndev, napi_consume_skb(skb, budget); } - queue_sends = - atomic_dec_return(&net_device->chan_table[q_idx].queue_sends); - - if (unlikely(net_device->destroy)) { - if (queue_sends == 0) - wake_up(&net_device->wait_drain); - } else { - struct netdev_queue *txq = netdev_get_tx_queue(ndev, q_idx); - - if (netif_tx_queue_stopped(txq) && !net_device->tx_disable && - (hv_get_avail_to_write_percent(&channel->outbound) > - RING_AVAIL_PERCENT_HIWATER || queue_sends < 1)) { - netif_tx_wake_queue(txq); - ndev_ctx->eth_stats.wake_queue++; - } - } + netvsc_send_acct(ndev, net_device, channel, q_idx); } -static void netvsc_send_completion(struct net_device *ndev, - struct netvsc_device *net_device, - struct vmbus_channel *incoming_channel, - const struct vmpacket_descriptor *desc, - int budget) +void netvsc_send_completion(struct net_device *ndev, + struct netvsc_device *net_device, + struct vmbus_channel *incoming_channel, + const struct vmpacket_descriptor *desc, + int budget) { const struct nvsp_message *nvsp_packet; u32 msglen = hv_pkt_datalen(desc);
@@ -840,11 +850,24 @@ static void netvsc_send_completion(struct net_device *ndev, if (!msglen) { cmd_rqst = incoming_channel->request_addr_callback(incoming_channel, desc->trans_id); - if (cmd_rqst == VMBUS_RQST_ERROR) { + if (cmd_rqst == VMBUS_RQST_ERROR || cmd_rqst == VMBUS_NO_RQSTOR) { netdev_err(ndev, "Invalid transaction ID %llx\n", desc->trans_id); return; } + /* + * netvsc_send_pkt() registers (ulong)skb as the request + * address. Control RNDIS sends carry no skb, so the + * registered address is NULL and there is no nvsp_message + * to inspect. The request id is consumed above, so this + * completion owns the send accounting -- the same thing + * netvsc_send_tx_complete() does when it sees a NULL skb. + */ + if (!cmd_rqst) { + netvsc_send_acct(ndev, net_device, incoming_channel, 0); + return; + } + pkt_rqst = (struct nvsp_message *)(uintptr_t)cmd_rqst; switch (pkt_rqst->hdr.msg_type) { case NVSP_MSG4_TYPE_SWITCH_DATA_PATH:
diff --git a/drivers/net/hyperv/netvsc_completion_test.c b/drivers/net/hyperv/netvsc_completion_test.c
new file mode 100644
index 000000000000..2614b1ece232
--- /dev/null
+++ b/drivers/net/hyperv/netvsc_completion_test.c@@ -0,0 +1,439 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * KUnit tests for empty and skb-backed netvsc completion handling. + * + * 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/etherdevice.h> +#include <linux/hyperv.h> +#include <linux/netdevice.h> +#include <linux/skbuff.h> +#include <linux/slab.h> +#include <linux/wait.h> + +#include "hyperv_net.h" + +/* + * Scripted requestor. request_addr_callback() only receives the channel + * and the transaction id, so the fixture is hung off a file-scope + * pointer. KUnit runs suite cases serially; one active fixture is + * enough and keeps the callback signature untouched. + */ +struct netvsc_completion_fixture { + struct net_device *ndev; + struct netvsc_device *nvdev; + struct vmbus_channel *channel; + /* Address returned on first use, then VMBUS_NO_RQSTOR: the id is + * consumed exactly once, as vmbus_request_addr_match() does. + */ + u64 once_addr; + unsigned int calls; + u64 seen_ids[8]; + unsigned int skb_frees; +}; + +static struct netvsc_completion_fixture *active_fx; + +static u64 test_request_addr(struct vmbus_channel *channel, u64 rqst_id) +{ + struct netvsc_completion_fixture *fx = active_fx; + + if (fx->calls < ARRAY_SIZE(fx->seen_ids)) + fx->seen_ids[fx->calls] = rqst_id; + fx->calls++; + + if (fx->calls == 1) + return fx->once_addr; + return VMBUS_NO_RQSTOR; +} + +/* + * A completion carrying no payload: hv_pkt_datalen() == 0. The + * empty-completion path looks the request up by trans_id and reads its + * message type, so the request itself is a separate object the fixture + * hands back through request_addr_callback(). + */ +struct netvsc_empty_desc { + struct vmpacket_descriptor desc; +} __packed; + +static void make_empty_desc(struct netvsc_empty_desc *pkt, u64 trans_id) +{ + memset(pkt, 0, sizeof(*pkt)); + pkt->desc.offset8 = sizeof(pkt->desc) / 8; + pkt->desc.len8 = pkt->desc.offset8; + pkt->desc.trans_id = trans_id; +} + +/* + * The production path never dereferences a net_device queue while + * tx_disable is set: netif_tx_queue_stopped() && !tx_disable + * short-circuits before hv_get_avail_to_write_percent() reads the + * ring. Set it here so a test needs no live outbound ring. + */ +static int netvsc_completion_fixture_init(struct netvsc_completion_fixture *fx) +{ + fx->ndev = alloc_netdev_mqs(sizeof(struct net_device_context), + "hvcompl%d", NET_NAME_UNKNOWN, + ether_setup, 2, 2); + if (!fx->ndev) + return -ENOMEM; + + fx->nvdev = kzalloc_obj(*fx->nvdev, GFP_KERNEL); + if (!fx->nvdev) { + free_netdev(fx->ndev); + return -ENOMEM; + } + + fx->channel = kzalloc_obj(*fx->channel, GFP_KERNEL); + if (!fx->channel) { + kfree(fx->nvdev); + free_netdev(fx->ndev); + return -ENOMEM; + } + + init_waitqueue_head(&fx->nvdev->wait_drain); + init_completion(&fx->nvdev->channel_init_wait); + fx->nvdev->tx_disable = true; + fx->channel->request_addr_callback = test_request_addr; + fx->once_addr = 0; + fx->calls = 0; + memset(fx->seen_ids, 0, sizeof(fx->seen_ids)); + active_fx = fx; + return 0; +} + +static void netvsc_completion_fixture_exit(struct netvsc_completion_fixture *fx) +{ + active_fx = NULL; + kfree(fx->channel); + kfree(fx->nvdev); + free_netdev(fx->ndev); +} + +static int netvsc_send_sends(struct netvsc_completion_fixture *fx, u16 q_idx) +{ + return atomic_read(&fx->nvdev->chan_table[q_idx].queue_sends); +} + +/* Empty completion, request address NULL: owns the send accounting. */ +static void netvsc_completion_null_address_acct_test(struct kunit *test) +{ + struct netvsc_completion_fixture fx = {}; + struct netvsc_empty_desc pkt; + + KUNIT_ASSERT_EQ(test, netvsc_completion_fixture_init(&fx), 0); + atomic_set(&fx.nvdev->chan_table[0].queue_sends, 3); + make_empty_desc(&pkt, 0x11); + /* once_addr = 0 is the control-path NULL skb case. */ + fx.once_addr = 0; + + netvsc_send_completion(fx.ndev, fx.nvdev, fx.channel, &pkt.desc, 0); + + KUNIT_EXPECT_EQ(test, netvsc_send_sends(&fx, 0), 2); + KUNIT_EXPECT_FALSE(test, completion_done(&fx.nvdev->channel_init_wait)); + KUNIT_EXPECT_EQ(test, fx.calls, 1U); + KUNIT_EXPECT_EQ(test, fx.seen_ids[0], 0x11U); + + netvsc_completion_fixture_exit(&fx); +} + +/* VMBUS_RQST_ERROR is rejected: no accounting, no completion. */ +static void netvsc_completion_rqst_error_sentinel_test(struct kunit *test) +{ + struct netvsc_completion_fixture fx = {}; + struct netvsc_empty_desc pkt; + + KUNIT_ASSERT_EQ(test, netvsc_completion_fixture_init(&fx), 0); + atomic_set(&fx.nvdev->chan_table[0].queue_sends, 3); + make_empty_desc(&pkt, 0x22); + fx.once_addr = VMBUS_RQST_ERROR; + + netvsc_send_completion(fx.ndev, fx.nvdev, fx.channel, &pkt.desc, 0); + + KUNIT_EXPECT_EQ(test, netvsc_send_sends(&fx, 0), 3); + KUNIT_EXPECT_FALSE(test, completion_done(&fx.nvdev->channel_init_wait)); + + netvsc_completion_fixture_exit(&fx); +} + +/* VMBUS_NO_RQSTOR is rejected the same way. */ +static void netvsc_completion_no_rqstor_sentinel_test(struct kunit *test) +{ + struct netvsc_completion_fixture fx = {}; + struct netvsc_empty_desc pkt; + + KUNIT_ASSERT_EQ(test, netvsc_completion_fixture_init(&fx), 0); + atomic_set(&fx.nvdev->chan_table[0].queue_sends, 3); + make_empty_desc(&pkt, 0x33); + fx.once_addr = VMBUS_NO_RQSTOR; + + netvsc_send_completion(fx.ndev, fx.nvdev, fx.channel, &pkt.desc, 0); + + KUNIT_EXPECT_EQ(test, netvsc_send_sends(&fx, 0), 3); + KUNIT_EXPECT_FALSE(test, completion_done(&fx.nvdev->channel_init_wait)); + + netvsc_completion_fixture_exit(&fx); +} + +/* + * A real request address whose message type is SWITCH_DATA_PATH + * completes the channel-init wait. It is not send accounting: the + * control request was not a send. + */ +static void netvsc_completion_switch_data_path_test(struct kunit *test) +{ + struct netvsc_completion_fixture fx = {}; + struct netvsc_empty_desc pkt; + struct nvsp_message req = {}; + + KUNIT_ASSERT_EQ(test, netvsc_completion_fixture_init(&fx), 0); + atomic_set(&fx.nvdev->chan_table[0].queue_sends, 3); + make_empty_desc(&pkt, 0x44); + req.hdr.msg_type = NVSP_MSG4_TYPE_SWITCH_DATA_PATH; + fx.once_addr = (u64)(unsigned long)&req; + + netvsc_send_completion(fx.ndev, fx.nvdev, fx.channel, &pkt.desc, 0); + + KUNIT_EXPECT_TRUE(test, completion_done(&fx.nvdev->channel_init_wait)); + KUNIT_EXPECT_EQ(test, netvsc_send_sends(&fx, 0), 3); + + netvsc_completion_fixture_exit(&fx); +} + +/* An unexpected message type takes neither the complete nor the acct path. */ +static void netvsc_completion_unknown_msg_type_test(struct kunit *test) +{ + struct netvsc_completion_fixture fx = {}; + struct netvsc_empty_desc pkt; + struct nvsp_message req = {}; + + KUNIT_ASSERT_EQ(test, netvsc_completion_fixture_init(&fx), 0); + atomic_set(&fx.nvdev->chan_table[0].queue_sends, 3); + make_empty_desc(&pkt, 0x55); + req.hdr.msg_type = 0xdead; + fx.once_addr = (u64)(unsigned long)&req; + + netvsc_send_completion(fx.ndev, fx.nvdev, fx.channel, &pkt.desc, 0); + + KUNIT_EXPECT_FALSE(test, completion_done(&fx.nvdev->channel_init_wait)); + KUNIT_EXPECT_EQ(test, netvsc_send_sends(&fx, 0), 3); + + netvsc_completion_fixture_exit(&fx); +} + +/* + * A repeated completion for an already-consumed transaction id must not + * account twice. The requestor returns VMBUS_NO_RQSTOR on the second + * use and the empty-completion path rejects it. + */ +static void netvsc_completion_duplicate_completion_test(struct kunit *test) +{ + struct netvsc_completion_fixture fx = {}; + struct netvsc_empty_desc pkt; + + KUNIT_ASSERT_EQ(test, netvsc_completion_fixture_init(&fx), 0); + atomic_set(&fx.nvdev->chan_table[0].queue_sends, 3); + make_empty_desc(&pkt, 0x66); + fx.once_addr = 0; + + netvsc_send_completion(fx.ndev, fx.nvdev, fx.channel, &pkt.desc, 0); + KUNIT_EXPECT_EQ(test, netvsc_send_sends(&fx, 0), 2); + KUNIT_EXPECT_EQ(test, fx.calls, 1U); + + /* Second completion for the same id: already consumed. */ + netvsc_send_completion(fx.ndev, fx.nvdev, fx.channel, &pkt.desc, 0); + KUNIT_EXPECT_EQ(test, netvsc_send_sends(&fx, 0), 2); + KUNIT_EXPECT_EQ(test, fx.calls, 2U); + KUNIT_EXPECT_EQ(test, fx.seen_ids[0], 0x66U); + KUNIT_EXPECT_EQ(test, fx.seen_ids[1], 0x66U); + + netvsc_completion_fixture_exit(&fx); +} + +/* N completions decrement the queue exactly N times. */ +static void netvsc_completion_single_decrement_test(struct kunit *test) +{ + struct netvsc_completion_fixture fx = {}; + struct netvsc_empty_desc pkt; + int i; + + KUNIT_ASSERT_EQ(test, netvsc_completion_fixture_init(&fx), 0); + atomic_set(&fx.nvdev->chan_table[0].queue_sends, 5); + + for (i = 0; i < 5; i++) { + make_empty_desc(&pkt, 0x100 + i); + fx.once_addr = 0; + fx.calls = 0; + netvsc_send_completion(fx.ndev, fx.nvdev, fx.channel, + &pkt.desc, 0); + KUNIT_EXPECT_EQ(test, netvsc_send_sends(&fx, 0), 4 - i); + KUNIT_EXPECT_EQ(test, fx.calls, 1U); + } + KUNIT_EXPECT_EQ(test, netvsc_send_sends(&fx, 0), 0); + + netvsc_completion_fixture_exit(&fx); +} + +static int netvsc_test_wake(struct wait_queue_entry *wq_entry, + unsigned int mode, int flags, void *key) +{ + unsigned int *woken = wq_entry->private; + + (*woken)++; + return 0; +} + +/* destroy + a decrement that reaches zero wakes the drain wait. */ +static void netvsc_send_acct_drain_wake_test(struct kunit *test) +{ + struct netvsc_completion_fixture fx = {}; + struct wait_queue_entry entry; + unsigned int woken = 0; + + KUNIT_ASSERT_EQ(test, netvsc_completion_fixture_init(&fx), 0); + fx.nvdev->destroy = true; + + memset(&entry, 0, sizeof(entry)); + entry.func = netvsc_test_wake; + entry.private = &woken; + add_wait_queue(&fx.nvdev->wait_drain, &entry); + + /* Non-zero remainder: the drain wait is not woken. */ + atomic_set(&fx.nvdev->chan_table[0].queue_sends, 2); + netvsc_send_acct(fx.ndev, fx.nvdev, fx.channel, 0); + KUNIT_EXPECT_EQ(test, woken, 0U); + KUNIT_EXPECT_EQ(test, netvsc_send_sends(&fx, 0), 1); + + /* Exactly zero: the drain wait is woken once. */ + netvsc_send_acct(fx.ndev, fx.nvdev, fx.channel, 0); + KUNIT_EXPECT_EQ(test, woken, 1U); + KUNIT_EXPECT_EQ(test, netvsc_send_sends(&fx, 0), 0); + + remove_wait_queue(&fx.nvdev->wait_drain, &entry); + netvsc_completion_fixture_exit(&fx); +} + +/* + * netvsc_send_tx_complete() applies the same sentinel rejection and + * still accounts a NULL skb. + */ +static void netvsc_tx_complete_null_skb_acct_test(struct kunit *test) +{ + struct netvsc_completion_fixture fx = {}; + struct netvsc_empty_desc pkt; + + KUNIT_ASSERT_EQ(test, netvsc_completion_fixture_init(&fx), 0); + atomic_set(&fx.nvdev->chan_table[0].queue_sends, 3); + make_empty_desc(&pkt, 0x77); + fx.once_addr = 0; + + netvsc_send_tx_complete(fx.ndev, fx.nvdev, fx.channel, &pkt.desc, 0); + KUNIT_EXPECT_EQ(test, netvsc_send_sends(&fx, 0), 2); + + /* The sentinel is rejected before any accounting. */ + make_empty_desc(&pkt, 0x78); + fx.once_addr = VMBUS_RQST_ERROR; + fx.calls = 0; + netvsc_send_tx_complete(fx.ndev, fx.nvdev, fx.channel, &pkt.desc, 0); + KUNIT_EXPECT_EQ(test, netvsc_send_sends(&fx, 0), 2); + + netvsc_completion_fixture_exit(&fx); +} + +static void netvsc_test_skb_destructor(struct sk_buff *skb) +{ + active_fx->skb_frees++; +} + +static void netvsc_tx_complete_skb(struct kunit *test, u32 status) +{ + struct netvsc_completion_fixture fx = {}; + struct { + struct vmpacket_descriptor desc; + struct nvsp_message message; + } pkt = {}; + unsigned long send_slots = BIT(3); + struct hv_netvsc_packet *packet; + struct netvsc_stats_tx *stats; + struct sk_buff *skb; + + KUNIT_ASSERT_EQ(test, netvsc_completion_fixture_init(&fx), 0); + skb = alloc_skb(64, GFP_KERNEL); + if (!skb) { + KUNIT_FAIL(test, "skb allocation failed"); + goto out; + } + skb->destructor = netvsc_test_skb_destructor; + packet = (struct hv_netvsc_packet *)skb->cb; + memset(packet, 0, sizeof(*packet)); + packet->send_buf_index = 3; + packet->q_idx = 1; + packet->total_packets = 2; + packet->total_bytes = 64; + fx.nvdev->send_section_map = &send_slots; + stats = &fx.nvdev->chan_table[1].tx_stats; + u64_stats_init(&stats->syncp); + atomic_set(&fx.nvdev->chan_table[0].queue_sends, 7); + atomic_set(&fx.nvdev->chan_table[1].queue_sends, 3); + fx.once_addr = (u64)(unsigned long)skb; + pkt.desc.offset8 = sizeof(pkt.desc) / 8; + pkt.desc.len8 = DIV_ROUND_UP(sizeof(pkt), 8); + pkt.desc.trans_id = 0x79; + pkt.message.hdr.msg_type = NVSP_MSG1_TYPE_SEND_RNDIS_PKT_COMPLETE; + pkt.message.msg.v1_msg.send_rndis_pkt_complete.status = status; + + /* Budget zero consumes the real skb synchronously, outside NAPI. */ + netvsc_send_completion(fx.ndev, fx.nvdev, fx.channel, &pkt.desc, 0); + KUNIT_EXPECT_EQ(test, send_slots, 0UL); + KUNIT_EXPECT_EQ(test, fx.skb_frees, 1U); + KUNIT_EXPECT_EQ(test, stats->packets, 2ULL); + KUNIT_EXPECT_EQ(test, stats->bytes, 64ULL); + KUNIT_EXPECT_EQ(test, netvsc_send_sends(&fx, 0), 7); + KUNIT_EXPECT_EQ(test, netvsc_send_sends(&fx, 1), 2); + + /* The consumed transaction cannot free or account the skb twice. */ + netvsc_send_completion(fx.ndev, fx.nvdev, fx.channel, &pkt.desc, 0); + KUNIT_EXPECT_EQ(test, send_slots, 0UL); + KUNIT_EXPECT_EQ(test, fx.skb_frees, 1U); + KUNIT_EXPECT_EQ(test, stats->packets, 2ULL); + KUNIT_EXPECT_EQ(test, stats->bytes, 64ULL); + KUNIT_EXPECT_EQ(test, netvsc_send_sends(&fx, 1), 2); +out: + netvsc_completion_fixture_exit(&fx); +} + +static void netvsc_tx_complete_skb_success_test(struct kunit *test) +{ + netvsc_tx_complete_skb(test, NVSP_STAT_SUCCESS); +} + +static void netvsc_tx_complete_skb_error_test(struct kunit *test) +{ + netvsc_tx_complete_skb(test, NVSP_STAT_FAIL); +} + +static struct kunit_case netvsc_completion_test_cases[] = { + KUNIT_CASE(netvsc_completion_null_address_acct_test), + KUNIT_CASE(netvsc_completion_rqst_error_sentinel_test), + KUNIT_CASE(netvsc_completion_no_rqstor_sentinel_test), + KUNIT_CASE(netvsc_completion_switch_data_path_test), + KUNIT_CASE(netvsc_completion_unknown_msg_type_test), + KUNIT_CASE(netvsc_completion_duplicate_completion_test), + KUNIT_CASE(netvsc_completion_single_decrement_test), + KUNIT_CASE(netvsc_send_acct_drain_wake_test), + KUNIT_CASE(netvsc_tx_complete_null_skb_acct_test), + KUNIT_CASE(netvsc_tx_complete_skb_success_test), + KUNIT_CASE(netvsc_tx_complete_skb_error_test), + {} +}; + +static struct kunit_suite netvsc_completion_test_suite = { + .name = "hyperv-netvsc-completion", + .test_cases = netvsc_completion_test_cases, +}; + +kunit_test_suite(netvsc_completion_test_suite);
--
2.43.0