mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH 0/2] xen/pvcalls: fix the backend error path and a workqueue leak
@ 2026-11-09 12:23 Yehyeong Lee
  2026-11-09 12:23 ` [PATCH 1/2] xen/pvcalls: unwind pvcalls_new_active_socket() step by step Yehyeong Lee
  2026-11-09 12:23 ` [PATCH 2/2] xen/pvcalls: destroy the per-socket workqueue on release Yehyeong Lee
  0 siblings, 2 replies; 5+ messages in thread
From: Yehyeong Lee @ 2026-11-09 12:23 UTC (permalink / raw)
  To: jgross, sstabellini
  Cc: oleksandr_tyshchenko, boris.ostrovsky, xen-devel, linux-kernel, stable

1/2: every error path in pvcalls_new_active_socket() runs list_del() on a
{NULL, NULL} list_head, holding socket_lock.  2/2: the per-socket workqueue
is never destroyed.  A frontend reaches 1/2 with one bad ring_order and 2/2
by connecting and releasing sockets in a loop.  Nested-Xen lab: 5 of 5
cells oops as shipped and 0 of 5 patched, a normal connect moves the same
bytes both ways on both arms, and backend/pvcalls/<domid>/0/state after
teardown reads 4 before and 6 after.  Reported to the Xen security team
first; PVCalls is not security supported, so posting here as suggested.

Yehyeong Lee (2):
  xen/pvcalls: unwind pvcalls_new_active_socket() step by step
  xen/pvcalls: destroy the per-socket workqueue on release

 drivers/xen/pvcalls-back.c | 26 ++++++++++++++++----------
 1 file changed, 16 insertions(+), 10 deletions(-)


base-commit: 7475ea8b58616b33c04afb976cf0fbaad9dd63fb
-- 
2.43.0


^ permalink raw reply	[flat|nested] 5+ messages in thread

* [PATCH 1/2] xen/pvcalls: unwind pvcalls_new_active_socket() step by step
  2026-11-09 12:23 [PATCH 0/2] xen/pvcalls: fix the backend error path and a workqueue leak Yehyeong Lee
@ 2026-11-09 12:23 ` Yehyeong Lee
  2026-11-10  0:55   ` Stefano Stabellini
  2026-11-09 12:23 ` [PATCH 2/2] xen/pvcalls: destroy the per-socket workqueue on release Yehyeong Lee
  1 sibling, 1 reply; 5+ messages in thread
From: Yehyeong Lee @ 2026-11-09 12:23 UTC (permalink / raw)
  To: jgross, sstabellini
  Cc: oleksandr_tyshchenko, boris.ostrovsky, xen-devel, linux-kernel, stable

Every error path in pvcalls_new_active_socket() jumps to one shared label
that does list_del(&map->list) and then hands map to
pvcalls_back_release_active().  But map comes from kzalloc() and its
list_head is only published at the very end of the function, after the last
of those jumps:

	down(&fedata->socket_lock);
	list_add_tail(&map->list, &fedata->socket_mappings);
	up(&fedata->socket_lock);

so on the error path map->list is still {NULL, NULL} and __list_del()
writes through a NULL pointer.

A frontend gets there with one value it owns: the ring_order field of the
page it granted.  Anything above MAX_RING_ORDER takes the second goto, and
nothing else in the function has run by then.

The result is an oops in whichever context handled the request - the
pvcalls-back threaded IRQ handler for PVCALLS_CONNECT, mappass->wq for
PVCALLS_ACCEPT - and because it happens after down(&fedata->socket_lock)
that semaphore is never released, so every later operation on the frontend
blocks on it and the backend never completes backend_disconnect().  (With
CONFIG_DEBUG_LIST the list_del() only reports and returns, and
pvcalls_back_release_active() then runs on a map whose irq is still 0 and
whose bytes, ioworker.wq and saved_data_ready are all NULL.)

A frontend asking for ring_order 5, on a dom0 built with KASAN
(the request was issued by a test-only change to the frontend):

[  133.728877] pvcalls_new_active_socket frontend requested ring_order 5, which is > MAX (4)
[  133.732868] Oops: general protection fault, probably for non-canonical address 0xdffffc0000000001: 0000 [#1] SMP KASAN NOPTI
[  133.737910] KASAN: null-ptr-deref in range [0x0000000000000008-0x000000000000000f]
[  133.741666] CPU: 0 UID: 0 PID: 133 Comm: irq/39-pvcalls- Not tainted 7.3.0-kCTLblk2-15482-g21a7f44fd076-dirty #1 PREEMPT(full)
[  133.746986] Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
[  133.751512] RIP: 0010:pvcalls_new_active_socket+0x821/0xb90
[  133.754237] Code: 48 89 fa 48 c1 ea 03 80 3c 02 00 0f 85 f6 01 00 00 48 8d 79 08 4c 8b 6b 08 48 b8 00 00 00 00 00 fc ff df 48 89 fa 48 c1 ea 03 <80> 3c 02 00 0f 85 14 02 00 00 4c 89 ea 4c 89 69 08 48 b8 00 00 00
[  133.763126] RSP: 0018:ffffc900004afb38 EFLAGS: 00010202
[  133.765861] RAX: dffffc0000000000 RBX: ffff88810886c900 RCX: 0000000000000000
[  133.769348] RDX: 0000000000000001 RSI: 0000000000000246 RDI: 0000000000000008
[  133.772787] RBP: 1ffff92000095f6b R08: 0000000000000001 R09: fffff52000095f58
[  133.776260] R10: 0000000000000003 R11: ffff888100077320 R12: ffff88810858b410
[  133.779823] R13: 0000000000000000 R14: ffff88810858b460 R15: ffff88810858b400
[  133.783382] FS:  0000000000000000(0000) GS:ffff8881b034c000(0000) knlGS:0000000000000000
[  133.787351] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[  133.790308] CR2: 00000000006042c0 CR3: 0000000108400001 CR4: 0000000000770ef0
[  133.793822] PKRU: 55555554
[  133.795347] Call Trace:
[  133.796778]  <TASK>
[  133.798082]  ? __pfx_pvcalls_new_active_socket+0x10/0x10
[  133.800801]  ? security_socket_post_create+0x4d/0x60
[  133.803275]  ? release_sock+0x1a/0x1d0
[  133.805182]  pvcalls_back_event+0x1b09/0x23e0
[  133.807431]  ? __pfx_pvcalls_back_event+0x10/0x10
[  133.809814]  ? __pfx__raw_spin_lock_irqsave+0x10/0x10
[  133.812251]  ? __pfx_pvcalls_back_event+0x10/0x10
[  133.814554]  irq_thread_fn+0x86/0x150
[  133.816468]  irq_thread+0x2da/0x580
[  133.818363]  ? __pfx_irq_thread_fn+0x10/0x10
[  133.820586]  ? __pfx_irq_thread+0x10/0x10
[  133.822670]  ? __kthread_parkme+0x88/0x170
[  133.824803]  ? __pfx_irq_thread+0x10/0x10
[  133.827153]  ? __pfx_irq_thread+0x10/0x10
[  133.829304]  kthread+0x2c8/0x3b0
[  133.831176]  ? recalc_sigpending+0x15c/0x1e0
[  133.833570]  ? __pfx_kthread+0x10/0x10
[  133.835572]  ret_from_fork+0x373/0x5b0
[  133.837581]  ? __pfx_ret_from_fork+0x10/0x10
[  133.839978]  ? __switch_to+0x3c4/0xb40
[  133.841984]  ? __pfx_kthread+0x10/0x10
[  133.843970]  ret_from_fork_asm+0x1a/0x30
[  133.846031]  </TASK>
[  133.847420] Modules linked in:
[  133.849240] ---[ end trace 0000000000000000 ]---

Give each failure its own label, so that only resources that were actually
acquired are released and the list the map was never added to is left
alone.  The map is not published anywhere at that point, so the error path
needs no socket_lock at all.  sock_release() stays in the callee on every
error path, the way
commit 8fafac202d18 ("xen/pvcalls-back: fix double frees with pvcalls_new_active_socket()")
arranged it.

Fixes: 5db4d286a8ef ("xen/pvcalls: implement connect command")
Cc: stable@vger.kernel.org
Signed-off-by: Yehyeong Lee <yhlee@isslab.korea.ac.kr>
---
 drivers/xen/pvcalls-back.c | 25 +++++++++++++++----------
 1 file changed, 15 insertions(+), 10 deletions(-)

diff --git a/drivers/xen/pvcalls-back.c b/drivers/xen/pvcalls-back.c
index ba62651a7f338b..dcefa91a40cd05 100644
--- a/drivers/xen/pvcalls-back.c
+++ b/drivers/xen/pvcalls-back.c
@@ -337,7 +337,7 @@ static struct sock_mapping *pvcalls_new_active_socket(
 
 	ret = xenbus_map_ring_valloc(fedata->dev, &ref, 1, &page);
 	if (ret < 0)
-		goto out;
+		goto out_map;
 	map->ring = page;
 	map->ring_order = map->ring->ring_order;
 	/* first read the order, then map the data ring */
@@ -345,19 +345,19 @@ static struct sock_mapping *pvcalls_new_active_socket(
 	if (map->ring_order > MAX_RING_ORDER) {
 		pr_warn("%s frontend requested ring_order %u, which is > MAX (%u)\n",
 				__func__, map->ring_order, MAX_RING_ORDER);
-		goto out;
+		goto out_unmap_ring;
 	}
 	ret = xenbus_map_ring_valloc(fedata->dev, map->ring->ref,
 				     (1 << map->ring_order), &page);
 	if (ret < 0)
-		goto out;
+		goto out_unmap_ring;
 	map->bytes = page;
 
 	ret = bind_interdomain_evtchn_to_irqhandler_lateeoi(
 			fedata->dev, evtchn,
 			pvcalls_back_conn_event, 0, "pvcalls-backend", map);
 	if (ret < 0)
-		goto out;
+		goto out_unmap_bytes;
 	map->irq = ret;
 
 	map->data.in = map->bytes;
@@ -365,7 +365,7 @@ static struct sock_mapping *pvcalls_new_active_socket(
 
 	map->ioworker.wq = alloc_ordered_workqueue("pvcalls_io", 0);
 	if (!map->ioworker.wq)
-		goto out;
+		goto out_unbind;
 	atomic_set(&map->io, 1);
 	INIT_WORK(&map->ioworker.register_work,	pvcalls_back_ioworker);
 
@@ -381,11 +381,16 @@ static struct sock_mapping *pvcalls_new_active_socket(
 	write_unlock_bh(&map->sock->sk->sk_callback_lock);
 
 	return map;
-out:
-	down(&fedata->socket_lock);
-	list_del(&map->list);
-	pvcalls_back_release_active(fedata->dev, fedata, map);
-	up(&fedata->socket_lock);
+
+out_unbind:
+	unbind_from_irqhandler(map->irq, map);
+out_unmap_bytes:
+	xenbus_unmap_ring_vfree(fedata->dev, map->bytes);
+out_unmap_ring:
+	xenbus_unmap_ring_vfree(fedata->dev, (void *)map->ring);
+out_map:
+	sock_release(sock);
+	kfree(map);
 	return NULL;
 }
 
-- 
2.43.0


^ permalink raw reply	[flat|nested] 5+ messages in thread

* [PATCH 2/2] xen/pvcalls: destroy the per-socket workqueue on release
  2026-11-09 12:23 [PATCH 0/2] xen/pvcalls: fix the backend error path and a workqueue leak Yehyeong Lee
  2026-11-09 12:23 ` [PATCH 1/2] xen/pvcalls: unwind pvcalls_new_active_socket() step by step Yehyeong Lee
@ 2026-11-09 12:23 ` Yehyeong Lee
  2026-11-10  0:56   ` Stefano Stabellini
  1 sibling, 1 reply; 5+ messages in thread
From: Yehyeong Lee @ 2026-11-09 12:23 UTC (permalink / raw)
  To: jgross, sstabellini
  Cc: oleksandr_tyshchenko, boris.ostrovsky, xen-devel, linux-kernel, stable

pvcalls_new_active_socket() gives every active socket its own ordered
workqueue:

	map->ioworker.wq = alloc_ordered_workqueue("pvcalls_io", 0);

and nothing ever destroys it.  pvcalls_back_release_active() flushes
map->ioworker.register_work and frees map, but the workqueue and its
pool_workqueues stay allocated, so a frontend leaks a workqueue per socket
it connects and releases - a loop a guest can run as long as it likes.

Destroy it after the IRQ is unbound, so no new work can be queued, and
before map is freed.  pvcalls_back_ioworker() takes no locks, in
particular not fedata->socket_lock, which the backend_disconnect() caller
holds here, so draining it from this point is safe; the flush_work() just
above already has the same requirement.

Fixes: 5db4d286a8ef ("xen/pvcalls: implement connect command")
Cc: stable@vger.kernel.org
Signed-off-by: Yehyeong Lee <yhlee@isslab.korea.ac.kr>
---
 drivers/xen/pvcalls-back.c | 1 +
 1 file changed, 1 insertion(+)

diff --git a/drivers/xen/pvcalls-back.c b/drivers/xen/pvcalls-back.c
index dcefa91a40cd05..27aa06289a7b92 100644
--- a/drivers/xen/pvcalls-back.c
+++ b/drivers/xen/pvcalls-back.c
@@ -456,6 +456,7 @@ static int pvcalls_back_release_active(struct xenbus_device *dev,
 	xenbus_unmap_ring_vfree(dev, map->bytes);
 	xenbus_unmap_ring_vfree(dev, (void *)map->ring);
 	unbind_from_irqhandler(map->irq, map);
+	destroy_workqueue(map->ioworker.wq);
 
 	sock_release(map->sock);
 	kfree(map);
-- 
2.43.0


^ permalink raw reply	[flat|nested] 5+ messages in thread

* Re: [PATCH 1/2] xen/pvcalls: unwind pvcalls_new_active_socket() step by step
  2026-11-09 12:23 ` [PATCH 1/2] xen/pvcalls: unwind pvcalls_new_active_socket() step by step Yehyeong Lee
@ 2026-11-10  0:55   ` Stefano Stabellini
  0 siblings, 0 replies; 5+ messages in thread
From: Stefano Stabellini @ 2026-11-10  0:55 UTC (permalink / raw)
  To: Yehyeong Lee
  Cc: jgross, sstabellini, oleksandr_tyshchenko, boris.ostrovsky,
	xen-devel, linux-kernel, stable

On Mon, 9 Nov 2026, Yehyeong Lee wrote:
> Every error path in pvcalls_new_active_socket() jumps to one shared label
> that does list_del(&map->list) and then hands map to
> pvcalls_back_release_active().  But map comes from kzalloc() and its
> list_head is only published at the very end of the function, after the last
> of those jumps:
> 
> 	down(&fedata->socket_lock);
> 	list_add_tail(&map->list, &fedata->socket_mappings);
> 	up(&fedata->socket_lock);
> 
> so on the error path map->list is still {NULL, NULL} and __list_del()
> writes through a NULL pointer.
> 
> A frontend gets there with one value it owns: the ring_order field of the
> page it granted.  Anything above MAX_RING_ORDER takes the second goto, and
> nothing else in the function has run by then.
> 
> The result is an oops in whichever context handled the request - the
> pvcalls-back threaded IRQ handler for PVCALLS_CONNECT, mappass->wq for
> PVCALLS_ACCEPT - and because it happens after down(&fedata->socket_lock)
> that semaphore is never released, so every later operation on the frontend
> blocks on it and the backend never completes backend_disconnect().  (With
> CONFIG_DEBUG_LIST the list_del() only reports and returns, and
> pvcalls_back_release_active() then runs on a map whose irq is still 0 and
> whose bytes, ioworker.wq and saved_data_ready are all NULL.)
> 
> A frontend asking for ring_order 5, on a dom0 built with KASAN
> (the request was issued by a test-only change to the frontend):
> 
> [  133.728877] pvcalls_new_active_socket frontend requested ring_order 5, which is > MAX (4)
> [  133.732868] Oops: general protection fault, probably for non-canonical address 0xdffffc0000000001: 0000 [#1] SMP KASAN NOPTI
> [  133.737910] KASAN: null-ptr-deref in range [0x0000000000000008-0x000000000000000f]
> [  133.741666] CPU: 0 UID: 0 PID: 133 Comm: irq/39-pvcalls- Not tainted 7.3.0-kCTLblk2-15482-g21a7f44fd076-dirty #1 PREEMPT(full)
> [  133.746986] Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
> [  133.751512] RIP: 0010:pvcalls_new_active_socket+0x821/0xb90
> [  133.754237] Code: 48 89 fa 48 c1 ea 03 80 3c 02 00 0f 85 f6 01 00 00 48 8d 79 08 4c 8b 6b 08 48 b8 00 00 00 00 00 fc ff df 48 89 fa 48 c1 ea 03 <80> 3c 02 00 0f 85 14 02 00 00 4c 89 ea 4c 89 69 08 48 b8 00 00 00
> [  133.763126] RSP: 0018:ffffc900004afb38 EFLAGS: 00010202
> [  133.765861] RAX: dffffc0000000000 RBX: ffff88810886c900 RCX: 0000000000000000
> [  133.769348] RDX: 0000000000000001 RSI: 0000000000000246 RDI: 0000000000000008
> [  133.772787] RBP: 1ffff92000095f6b R08: 0000000000000001 R09: fffff52000095f58
> [  133.776260] R10: 0000000000000003 R11: ffff888100077320 R12: ffff88810858b410
> [  133.779823] R13: 0000000000000000 R14: ffff88810858b460 R15: ffff88810858b400
> [  133.783382] FS:  0000000000000000(0000) GS:ffff8881b034c000(0000) knlGS:0000000000000000
> [  133.787351] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
> [  133.790308] CR2: 00000000006042c0 CR3: 0000000108400001 CR4: 0000000000770ef0
> [  133.793822] PKRU: 55555554
> [  133.795347] Call Trace:
> [  133.796778]  <TASK>
> [  133.798082]  ? __pfx_pvcalls_new_active_socket+0x10/0x10
> [  133.800801]  ? security_socket_post_create+0x4d/0x60
> [  133.803275]  ? release_sock+0x1a/0x1d0
> [  133.805182]  pvcalls_back_event+0x1b09/0x23e0
> [  133.807431]  ? __pfx_pvcalls_back_event+0x10/0x10
> [  133.809814]  ? __pfx__raw_spin_lock_irqsave+0x10/0x10
> [  133.812251]  ? __pfx_pvcalls_back_event+0x10/0x10
> [  133.814554]  irq_thread_fn+0x86/0x150
> [  133.816468]  irq_thread+0x2da/0x580
> [  133.818363]  ? __pfx_irq_thread_fn+0x10/0x10
> [  133.820586]  ? __pfx_irq_thread+0x10/0x10
> [  133.822670]  ? __kthread_parkme+0x88/0x170
> [  133.824803]  ? __pfx_irq_thread+0x10/0x10
> [  133.827153]  ? __pfx_irq_thread+0x10/0x10
> [  133.829304]  kthread+0x2c8/0x3b0
> [  133.831176]  ? recalc_sigpending+0x15c/0x1e0
> [  133.833570]  ? __pfx_kthread+0x10/0x10
> [  133.835572]  ret_from_fork+0x373/0x5b0
> [  133.837581]  ? __pfx_ret_from_fork+0x10/0x10
> [  133.839978]  ? __switch_to+0x3c4/0xb40
> [  133.841984]  ? __pfx_kthread+0x10/0x10
> [  133.843970]  ret_from_fork_asm+0x1a/0x30
> [  133.846031]  </TASK>
> [  133.847420] Modules linked in:
> [  133.849240] ---[ end trace 0000000000000000 ]---
> 
> Give each failure its own label, so that only resources that were actually
> acquired are released and the list the map was never added to is left
> alone.  The map is not published anywhere at that point, so the error path
> needs no socket_lock at all.  sock_release() stays in the callee on every
> error path, the way
> commit 8fafac202d18 ("xen/pvcalls-back: fix double frees with pvcalls_new_active_socket()")
> arranged it.
> 
> Fixes: 5db4d286a8ef ("xen/pvcalls: implement connect command")
> Cc: stable@vger.kernel.org
> Signed-off-by: Yehyeong Lee <yhlee@isslab.korea.ac.kr>

Reviewed-by: Stefano Stabellini <sstabellini@kernel.org>


^ permalink raw reply	[flat|nested] 5+ messages in thread

* Re: [PATCH 2/2] xen/pvcalls: destroy the per-socket workqueue on release
  2026-11-09 12:23 ` [PATCH 2/2] xen/pvcalls: destroy the per-socket workqueue on release Yehyeong Lee
@ 2026-11-10  0:56   ` Stefano Stabellini
  0 siblings, 0 replies; 5+ messages in thread
From: Stefano Stabellini @ 2026-11-10  0:56 UTC (permalink / raw)
  To: Yehyeong Lee
  Cc: jgross, sstabellini, oleksandr_tyshchenko, boris.ostrovsky,
	xen-devel, linux-kernel, stable

On Mon, 9 Nov 2026, Yehyeong Lee wrote:
> pvcalls_new_active_socket() gives every active socket its own ordered
> workqueue:
> 
> 	map->ioworker.wq = alloc_ordered_workqueue("pvcalls_io", 0);
> 
> and nothing ever destroys it.  pvcalls_back_release_active() flushes
> map->ioworker.register_work and frees map, but the workqueue and its
> pool_workqueues stay allocated, so a frontend leaks a workqueue per socket
> it connects and releases - a loop a guest can run as long as it likes.
> 
> Destroy it after the IRQ is unbound, so no new work can be queued, and
> before map is freed.  pvcalls_back_ioworker() takes no locks, in
> particular not fedata->socket_lock, which the backend_disconnect() caller
> holds here, so draining it from this point is safe; the flush_work() just
> above already has the same requirement.
> 
> Fixes: 5db4d286a8ef ("xen/pvcalls: implement connect command")
> Cc: stable@vger.kernel.org
> Signed-off-by: Yehyeong Lee <yhlee@isslab.korea.ac.kr>
> ---
>  drivers/xen/pvcalls-back.c | 1 +
>  1 file changed, 1 insertion(+)
> 
> diff --git a/drivers/xen/pvcalls-back.c b/drivers/xen/pvcalls-back.c
> index dcefa91a40cd05..27aa06289a7b92 100644
> --- a/drivers/xen/pvcalls-back.c
> +++ b/drivers/xen/pvcalls-back.c
> @@ -456,6 +456,7 @@ static int pvcalls_back_release_active(struct xenbus_device *dev,
>  	xenbus_unmap_ring_vfree(dev, map->bytes);
>  	xenbus_unmap_ring_vfree(dev, (void *)map->ring);
>  	unbind_from_irqhandler(map->irq, map);
> +	destroy_workqueue(map->ioworker.wq);

Please move the call to destroy_workqueue() just after flush_work().
With that:

Reviewed-by: Stefano Stabellini <sstabellini@kernel.org>


>  	sock_release(map->sock);
>  	kfree(map);
> -- 
> 2.43.0
> 

^ permalink raw reply	[flat|nested] 5+ messages in thread

end of thread, other threads:[~2026-11-10  0:56 UTC | newest]

Thread overview: 5+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-11-09 12:23 [PATCH 0/2] xen/pvcalls: fix the backend error path and a workqueue leak Yehyeong Lee
2026-11-09 12:23 ` [PATCH 1/2] xen/pvcalls: unwind pvcalls_new_active_socket() step by step Yehyeong Lee
2026-11-10  0:55   ` Stefano Stabellini
2026-11-09 12:23 ` [PATCH 2/2] xen/pvcalls: destroy the per-socket workqueue on release Yehyeong Lee
2026-11-10  0:56   ` Stefano Stabellini

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®