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


Reply via email to