When VMBus sub-channels (such as synthetic SCSI, network, or vsock channels) are dynamically opened during periods of sustained memory load or memory tier eviction, `vmbus_alloc_ring()` attempts to allocate physically contiguous memory using `alloc_pages(GFP_KERNEL | __GFP_ZERO, order)`.
For standard rings (order-7, 512 KiB contiguous memory), high buddy allocator fragmentation under memory pressure frequently causes `alloc_pages()` to fail with -ENOMEM even when ample total virtual memory is available. This manifests in userspace as connection timeouts (e.g. `accept4 failed 110: Connection timed out` on WSL2 vsock control planes). This patch introduces a resilient fallback mechanism: 1. When `alloc_pages()` fails due to external fragmentation, `vmbus_alloc_ring()` falls back to `vzalloc_node()` (or `vzalloc()`) to satisfy the buffer allocation from virtually contiguous pages. 2. In `hv_ringbuffer_init()`, detects `is_vmalloc_addr(virt_addr)` and populates the `pages_wraparound` mapping array using `vmalloc_to_page()`. 3. In `vmbus_free_ring()`, tracks `ringbuffer_is_vmalloc` and calls `vfree()` safely, preserving Confidential VM (CoCo) memory re-encryption isolation checks prior to release. Signed-off-by: Emerson Busson <[email protected]> --- drivers/hv/channel.c | 46 ++++++++++++++++++++++++++++++++++----- drivers/hv/hyperv_vmbus.h | 2 +- drivers/hv/ring_buffer.c | 19 +++++++++++----- include/linux/hyperv.h | 2 ++ 4 files changed, 57 insertions(+), 12 deletions(-) diff --git a/drivers/hv/channel.c b/drivers/hv/channel.c index 162d6aeec..f0fb3dd8f 100644 --- a/drivers/hv/channel.c +++ b/drivers/hv/channel.c @@ -12,6 +12,7 @@ #include <linux/sched.h> #include <linux/wait.h> #include <linux/mm.h> +#include <linux/vmalloc.h> #include <linux/slab.h> #include <linux/module.h> #include <linux/hyperv.h> @@ -153,13 +154,20 @@ void vmbus_free_ring(struct vmbus_channel *channel) hv_ringbuffer_cleanup(&channel->outbound); hv_ringbuffer_cleanup(&channel->inbound); - if (channel->ringbuffer_page) { + if (channel->ringbuffer_is_vmalloc && channel->ringbuffer_page_virt) { + /* In a CoCo VM leak the memory if it didn't get re-encrypted */ + if (!channel->ringbuffer_gpadlhandle.decrypted) + vfree(channel->ringbuffer_page_virt); + channel->ringbuffer_page_virt = NULL; + channel->ringbuffer_is_vmalloc = false; + } else if (channel->ringbuffer_page) { /* In a CoCo VM leak the memory if it didn't get re-encrypted */ if (!channel->ringbuffer_gpadlhandle.decrypted) __free_pages(channel->ringbuffer_page, get_order(channel->ringbuffer_pagecount << PAGE_SHIFT)); channel->ringbuffer_page = NULL; + channel->ringbuffer_page_virt = NULL; } } EXPORT_SYMBOL_GPL(vmbus_free_ring); @@ -182,10 +190,26 @@ int vmbus_alloc_ring(struct vmbus_channel *newchannel, if (!page) page = alloc_pages(GFP_KERNEL|__GFP_ZERO, order); - if (!page) - return -ENOMEM; + if (!page) { + /* Fallback to virtual memory allocation under buddy fragmentation */ + void *virt_addr = vzalloc_node(send_size + recv_size, + cpu_to_node(newchannel->target_cpu)); + + if (!virt_addr) + virt_addr = vzalloc(send_size + recv_size); + + if (!virt_addr) + return -ENOMEM; + + newchannel->ringbuffer_page = NULL; + newchannel->ringbuffer_page_virt = virt_addr; + newchannel->ringbuffer_is_vmalloc = true; + } else { + newchannel->ringbuffer_page = page; + newchannel->ringbuffer_page_virt = page_address(page); + newchannel->ringbuffer_is_vmalloc = false; + } - newchannel->ringbuffer_page = page; newchannel->ringbuffer_pagecount = (send_size + recv_size) >> PAGE_SHIFT; newchannel->ringbuffer_send_offset = send_size >> PAGE_SHIFT; @@ -639,6 +663,7 @@ static int __vmbus_open(struct vmbus_channel *newchannel, struct vmbus_channel_open_channel *open_msg; struct vmbus_channel_msginfo *open_info = NULL; struct page *page = newchannel->ringbuffer_page; + void *inbound_virt = NULL; u32 send_pages, recv_pages; unsigned long flags; int err; @@ -669,6 +694,8 @@ static int __vmbus_open(struct vmbus_channel *newchannel, newchannel->ringbuffer_gpadlhandle.gpadl_handle = 0; err = __vmbus_establish_gpadl(newchannel, HV_GPADL_RING, + newchannel->ringbuffer_page_virt ? + newchannel->ringbuffer_page_virt : page_address(newchannel->ringbuffer_page), (send_pages + recv_pages) << PAGE_SHIFT, newchannel->ringbuffer_send_offset << PAGE_SHIFT, @@ -677,11 +704,18 @@ static int __vmbus_open(struct vmbus_channel *newchannel, goto error_clean_ring; err = hv_ringbuffer_init(&newchannel->outbound, - page, send_pages, 0); + page, newchannel->ringbuffer_page_virt, + send_pages, 0); if (err) goto error_free_gpadl; - err = hv_ringbuffer_init(&newchannel->inbound, &page[send_pages], + if (newchannel->ringbuffer_page_virt) + inbound_virt = newchannel->ringbuffer_page_virt + + (send_pages << PAGE_SHIFT); + + err = hv_ringbuffer_init(&newchannel->inbound, + page ? &page[send_pages] : NULL, + inbound_virt, recv_pages, newchannel->max_pkt_size); if (err) goto error_free_gpadl; diff --git a/drivers/hv/hyperv_vmbus.h b/drivers/hv/hyperv_vmbus.h index 34943de7d..ec06c30d2 100644 --- a/drivers/hv/hyperv_vmbus.h +++ b/drivers/hv/hyperv_vmbus.h @@ -182,7 +182,7 @@ extern int hv_synic_cleanup(unsigned int cpu); void hv_ringbuffer_pre_init(struct vmbus_channel *channel); int hv_ringbuffer_init(struct hv_ring_buffer_info *ring_info, - struct page *pages, u32 pagecnt, u32 max_pkt_size); + struct page *pages, void *virt_addr, u32 pagecnt, u32 max_pkt_size); void hv_ringbuffer_cleanup(struct hv_ring_buffer_info *ring_info); diff --git a/drivers/hv/ring_buffer.c b/drivers/hv/ring_buffer.c index 23ce1fb70..e6d4cf185 100644 --- a/drivers/hv/ring_buffer.c +++ b/drivers/hv/ring_buffer.c @@ -184,7 +184,7 @@ void hv_ringbuffer_pre_init(struct vmbus_channel *channel) /* Initialize the ring buffer. */ int hv_ringbuffer_init(struct hv_ring_buffer_info *ring_info, - struct page *pages, u32 page_cnt, u32 max_pkt_size) + struct page *pages, void *virt_addr, u32 page_cnt, u32 max_pkt_size) { struct page **pages_wraparound; int i; @@ -201,10 +201,19 @@ int hv_ringbuffer_init(struct hv_ring_buffer_info *ring_info, if (!pages_wraparound) return -ENOMEM; - pages_wraparound[0] = pages; - for (i = 0; i < 2 * (page_cnt - 1); i++) - pages_wraparound[i + 1] = - &pages[i % (page_cnt - 1) + 1]; + if (virt_addr && is_vmalloc_addr(virt_addr)) { + pages_wraparound[0] = vmalloc_to_page(virt_addr); + for (i = 0; i < 2 * (page_cnt - 1); i++) { + void *curr_virt = virt_addr + ((i % (page_cnt - 1) + 1) << PAGE_SHIFT); + + pages_wraparound[i + 1] = vmalloc_to_page(curr_virt); + } + } else { + pages_wraparound[0] = pages; + for (i = 0; i < 2 * (page_cnt - 1); i++) + pages_wraparound[i + 1] = + &pages[i % (page_cnt - 1) + 1]; + } ring_info->ring_buffer = (struct hv_ring_buffer *) vmap(pages_wraparound, page_cnt * 2 - 1, VM_MAP, diff --git a/include/linux/hyperv.h b/include/linux/hyperv.h index a76f556f5..63203df4d 100644 --- a/include/linux/hyperv.h +++ b/include/linux/hyperv.h @@ -807,6 +807,8 @@ struct vmbus_channel { /* Allocated memory for ring buffer */ struct page *ringbuffer_page; + void *ringbuffer_page_virt; + bool ringbuffer_is_vmalloc; u32 ringbuffer_pagecount; u32 ringbuffer_send_offset; struct hv_ring_buffer_info outbound; /* send to parent */ -- 2.43.0

