* [PATCH v3 0/1] coroutine: fix lost wakeup in qemu_co_sleep_wake()
@ 2026-06-10 11:58 Denis V. Lunev via qemu development
2026-06-10 11:58 ` [PATCH v3 1/1] " Denis V. Lunev via qemu development
` (4 more replies)
0 siblings, 5 replies; 10+ messages in thread
From: Denis V. Lunev via qemu development @ 2026-06-10 11:58 UTC (permalink / raw)
To: qemu-devel
Cc: qemu-block, qemu-stable, Kevin Wolf, Stefan Hajnoczi, Hanna Reitz,
Denis V. Lunev
Changes since v2
----------------
* Drop the redundant fast-path check in qemu_co_sleep(); the publish
cmpxchg already consumes a pending wake (Kevin).
* Add a deterministic regression test, no threads or timers needed.
Changes since v1
----------------
* Patch 1 (graph-lock) applied as e3082ab3b3, dropped here.
* Fix the qemu_co_sleep_wake() primitive instead of working around
it in qcow2 (Kevin).
Problem
-------
The qemu shutdown / blockdev-close path can deadlock permanently on
upstream master. The main thread enters ppoll(timeout=-1) holding
BQL, no other thread has a wake source that points back at it, and
qemu has to be SIGKILLed. The hang has no timeout -- it is a hard
deadlock, not a slow operation; behind BQL, RCU, VCPUs and every
iothread path that needs BQL stall with it.
The race exposed in qcow2's cache_clean_timer cancellation path:
ppoll -> aio_poll -> cache_clean_timer_del_and_wait -> qcow2_close
The race diagram and the exact stale-state read are in the patch's
commit message.
Reproducer
----------
Environment: 4-vCPU VM guest, kernel 6.12.x, upstream master at
de5d8bfd61. On modern bare-metal the window is narrow enough that the
hang rarely reproduces without a VM -- a VM guest under full CPU
saturation is what makes the timing reliable.
# reproducer
stress-ng --cpu "$(nproc)" --timeout 0 &
for r in $(seq 20); do
timeout 120 ./build/tests/qemu-iotests/check -qcow2 iothreads-create
done
kill %1
With `stress-ng --cpu $(nproc)` the race surfaces. With
`stress-ng --cpu $(($(nproc) - 1))` or without a stressor it does
not reproduce reliably across 20 iterations.
The new unit test reproduces the same lost wakeup deterministically,
without a VM or a stressor.
Results
-------
Same guest, 20 iterations of the loop above, master at de5d8bfd61:
without this patch: reproduces reliably (qcow2_close in ppoll)
with this patch: 20/20 PASS
Signed-off-by: Denis V. Lunev <den@openvz.org>
Cc: Kevin Wolf <kwolf@redhat.com>
Cc: Hanna Reitz <hreitz@redhat.com>
Denis V. Lunev (1):
coroutine: fix lost wakeup in qemu_co_sleep_wake()
include/qemu/coroutine.h | 17 +++++++++---
tests/unit/test-coroutine.c | 53 +++++++++++++++++++++++++++++++++++++
util/qemu-coroutine-sleep.c | 53 ++++++++++++++++++++++++++-----------
3 files changed, 104 insertions(+), 19 deletions(-)
--
2.53.0
^ permalink raw reply [flat|nested] 10+ messages in thread
* [PATCH v3 1/1] coroutine: fix lost wakeup in qemu_co_sleep_wake()
2026-06-10 11:58 [PATCH v3 0/1] coroutine: fix lost wakeup in qemu_co_sleep_wake() Denis V. Lunev via qemu development
@ 2026-06-10 11:58 ` Denis V. Lunev via qemu development
2026-07-09 18:55 ` Philippe Mathieu-Daudé
2026-06-18 17:57 ` [PATCH v3 0/1] " Denis V. Lunev
` (3 subsequent siblings)
4 siblings, 1 reply; 10+ messages in thread
From: Denis V. Lunev via qemu development @ 2026-06-10 11:58 UTC (permalink / raw)
To: qemu-devel
Cc: qemu-block, qemu-stable, Kevin Wolf, Stefan Hajnoczi, Hanna Reitz,
Denis V. Lunev
cache_clean_timer_del_and_wait() cancels the cache-cleaner coroutine
by setting s->cache_clean_interval = 0 and calling qemu_co_sleep_wake()
to cut short its qemu_co_sleep_ns_wakeable(). qemu_co_sleep_wake() is
fire-and-forget: it reads w->to_wake and silently returns when it is
NULL. A sleeper that is between two iterations -- has just released
s->lock but has not yet set w->to_wake inside qemu_co_sleep() -- loses
the wake:
iothread0 timer coroutine main thread (qcow2 close)
------------------------- -------------------------
while-body (holding s->lock):
read interval = 600
wait_ns = 600 * NS
release s->lock
take s->lock
interval = 0
qemu_co_sleep_wake(w):
w->to_wake == NULL -> skip
return
qemu_co_queue_wait(exit, s->lock):
release s->lock
yield
qemu_co_sleep_ns_wakeable:
aio_timer_init(+600 s)
qemu_co_sleep:
cas scheduled NULL -> "qsns"
w->to_wake = co
yield [sleeps 600 s]
cache_clean_timer_del_and_wait() then blocks on cache_clean_timer_exit
until the original 600 s expiry fires, and qcow2_close() holds BQL the
whole time so the VM stalls behind it.
block_copy_kick() has the same shape. Fix the primitive once instead
of working around it in each caller.
Use a tri-state for QemuCoSleep::to_wake:
NULL - idle
co - sleeper parked
PENDING - wake delivered, no sleeper yet (sticky)
qemu_co_sleep_wake() xchgs PENDING into to_wake: a real sleeper is
woken, NULL/PENDING is left untouched so the wake stays sticky.
qemu_co_sleep() cmpxchg-publishes itself as the sleeper; if a wake
was delivered before it got there or races the publish, the cmpxchg
observes PENDING and returns without yielding. On normal resume
qemu_co_sleep() clears the PENDING the waker left behind so the next
sleep starts clean.
A double-fire (real wake plus timer callback) is harmless: the first
xchg returns the coroutine and wakes it; the second returns PENDING
and is a no-op. Cancellation latency through qemu_co_sleep_wake() is
now bounded by aio_co_wake() rather than by the sleep duration.
Fixes: f86dde9a15 ("qcow2: Fix cache_clean_timer")
Signed-off-by: Denis V. Lunev <den@openvz.org>
Cc: Hanna Czenczek <hreitz@redhat.com>
Cc: Kevin Wolf <kwolf@redhat.com>
---
include/qemu/coroutine.h | 17 +++++++++---
tests/unit/test-coroutine.c | 53 +++++++++++++++++++++++++++++++++++++
util/qemu-coroutine-sleep.c | 53 ++++++++++++++++++++++++++-----------
3 files changed, 104 insertions(+), 19 deletions(-)
diff --git a/include/qemu/coroutine.h b/include/qemu/coroutine.h
index e545bbf620..1c31de60f9 100644
--- a/include/qemu/coroutine.h
+++ b/include/qemu/coroutine.h
@@ -260,10 +260,19 @@ int coroutine_fn qemu_co_timeout(CoroutineEntry *entry, void *opaque,
uint64_t timeout_ns, CleanupFunc clean);
/**
- * Wake a coroutine if it is sleeping in qemu_co_sleep_ns. The timer will be
- * deleted. @sleep_state must be the variable whose address was given to
- * qemu_co_sleep_ns() and should be checked to be non-NULL before calling
- * qemu_co_sleep_wake().
+ * Wake a coroutine sleeping in qemu_co_sleep() or qemu_co_sleep_ns_wakeable().
+ * The timer set up by the latter is deleted on wakeup.
+ *
+ * The wake is sticky: if no sleeper is parked on @w at the time of the call,
+ * the wake is recorded on @w and consumed by the next qemu_co_sleep() on the
+ * same @w, which then returns without yielding. This closes the lost-wakeup
+ * window between two sleeps and is the documented behavior callers should
+ * rely on -- e.g. a cancellation signal raised between iterations of a
+ * sleep/work loop will shorten the next sleep instead of being dropped.
+ *
+ * The state persists until consumed: if no further qemu_co_sleep() is ever
+ * called on @w, the pending wake is harmlessly discarded when @w goes away.
+ * Multiple wakes coalesce -- the next sleep consumes at most one.
*/
void qemu_co_sleep_wake(QemuCoSleep *w);
diff --git a/tests/unit/test-coroutine.c b/tests/unit/test-coroutine.c
index 49d4d9b251..aa1f719b08 100644
--- a/tests/unit/test-coroutine.c
+++ b/tests/unit/test-coroutine.c
@@ -421,6 +421,57 @@ static void test_co_rwlock_downgrade(void)
g_assert(c1_done);
}
+/*
+ * Check that a wake delivered before the sleeper parks is not lost.
+ *
+ * qemu_co_sleep_wake() is fire-and-forget: a caller cancelling a
+ * sleep/work loop may call it in the window after the sleeper has
+ * decided to sleep but before it has published itself inside
+ * qemu_co_sleep(). The wake must be sticky and shorten the next sleep
+ * rather than being dropped (which would block until the full sleep
+ * duration expired).
+ *
+ * No threads, timers or AioContext are needed: coroutines are
+ * cooperative, so ordering the wake before the sleep deterministically
+ * reproduces the state the racing waker would otherwise produce.
+ */
+
+typedef struct {
+ QemuCoSleep w;
+ bool completed;
+} CoSleepWakeData;
+
+static void coroutine_fn co_sleep_wake_entry(void *opaque)
+{
+ CoSleepWakeData *d = opaque;
+
+ /*
+ * The wake was already delivered before we got here. qemu_co_sleep()
+ * must consume it and return without yielding.
+ */
+ qemu_co_sleep(&d->w);
+ d->completed = true;
+}
+
+static void test_co_sleep_wake_before_sleep(void)
+{
+ CoSleepWakeData d = { .w = { 0 }, .completed = false };
+ Coroutine *co = qemu_coroutine_create(co_sleep_wake_entry, &d);
+
+ /* Waker runs first, while no sleeper is parked on w. */
+ qemu_co_sleep_wake(&d.w);
+
+ /*
+ * Entering runs qemu_co_sleep(), which consumes the pending wake and
+ * returns without yielding, so the coroutine runs straight to
+ * completion in this single enter. With the pre-fix primitive the wake
+ * is dropped, qemu_co_sleep() parks, and completed stays false.
+ */
+ qemu_coroutine_enter(co);
+
+ g_assert(d.completed);
+}
+
/*
* Check that creation, enter, and return work
*/
@@ -660,6 +711,8 @@ int main(int argc, char **argv)
g_test_add_func("/locking/co-mutex/lockable", test_co_mutex_lockable);
g_test_add_func("/locking/co-rwlock/upgrade", test_co_rwlock_upgrade);
g_test_add_func("/locking/co-rwlock/downgrade", test_co_rwlock_downgrade);
+ g_test_add_func("/locking/co-sleep/wake-before-sleep",
+ test_co_sleep_wake_before_sleep);
if (g_test_perf()) {
g_test_add_func("/perf/lifecycle", perf_lifecycle);
g_test_add_func("/perf/nesting", perf_nesting);
diff --git a/util/qemu-coroutine-sleep.c b/util/qemu-coroutine-sleep.c
index edef117284..19ded0b6fd 100644
--- a/util/qemu-coroutine-sleep.c
+++ b/util/qemu-coroutine-sleep.c
@@ -18,20 +18,29 @@
static const char *qemu_co_sleep_ns__scheduled = "qemu_co_sleep_ns";
+/*
+ * Sentinel stored in QemuCoSleep::to_wake by qemu_co_sleep_wake() when no
+ * sleeper has parked yet. The next qemu_co_sleep() consumes it and returns
+ * without yielding, so a wake that races the arming of a sleep is never
+ * lost.
+ */
+#define QEMU_CO_SLEEP_PENDING ((Coroutine *)(uintptr_t)1)
+
void qemu_co_sleep_wake(QemuCoSleep *w)
{
Coroutine *co;
- co = w->to_wake;
- w->to_wake = NULL;
- if (co) {
- /* Write of schedule protected by barrier write in aio_co_schedule */
- const char *scheduled = qatomic_cmpxchg(&co->scheduled,
- qemu_co_sleep_ns__scheduled, NULL);
-
- assert(scheduled == qemu_co_sleep_ns__scheduled);
- aio_co_wake(co);
+ co = qatomic_xchg(&w->to_wake, QEMU_CO_SLEEP_PENDING);
+ if (co == NULL || co == QEMU_CO_SLEEP_PENDING) {
+ /* No sleeper, or a wake is already pending. */
+ return;
}
+
+ /* Write of scheduled protected by barrier write in aio_co_schedule */
+ const char *scheduled = qatomic_cmpxchg(&co->scheduled,
+ qemu_co_sleep_ns__scheduled, NULL);
+ assert(scheduled == qemu_co_sleep_ns__scheduled);
+ aio_co_wake(co);
}
static void co_sleep_cb(void *opaque)
@@ -43,6 +52,7 @@ static void co_sleep_cb(void *opaque)
void coroutine_fn qemu_co_sleep(QemuCoSleep *w)
{
Coroutine *co = qemu_coroutine_self();
+ Coroutine *prev;
const char *scheduled = qatomic_cmpxchg(&co->scheduled, NULL,
qemu_co_sleep_ns__scheduled);
@@ -53,11 +63,23 @@ void coroutine_fn qemu_co_sleep(QemuCoSleep *w)
abort();
}
- w->to_wake = co;
+ /*
+ * Publish ourselves as the sleeper. A wake delivered before we got here,
+ * or one racing this publish, leaves QEMU_CO_SLEEP_PENDING in to_wake;
+ * the cmpxchg then fails and we consume the wake without yielding.
+ */
+ prev = qatomic_cmpxchg(&w->to_wake, NULL, co);
+ if (prev == QEMU_CO_SLEEP_PENDING) {
+ qatomic_set(&w->to_wake, NULL);
+ qatomic_set(&co->scheduled, NULL);
+ return;
+ }
+ assert(prev == NULL);
+
qemu_coroutine_yield();
- /* w->to_wake is cleared before resuming this coroutine. */
- assert(w->to_wake == NULL);
+ /* The waker left QEMU_CO_SLEEP_PENDING; clear it for the next sleep. */
+ qatomic_set(&w->to_wake, NULL);
}
void coroutine_fn qemu_co_sleep_ns_wakeable(QemuCoSleep *w,
@@ -70,9 +92,10 @@ void coroutine_fn qemu_co_sleep_ns_wakeable(QemuCoSleep *w,
timer_mod(&ts, qemu_clock_get_ns(type) + ns);
/*
- * The timer will fire in the current AiOContext, so the callback
- * must happen after qemu_co_sleep yields and there is no race
- * between timer_mod and qemu_co_sleep.
+ * A wake racing with the arming of the sleep -- including the timer
+ * we just armed firing in another AioContext before qemu_co_sleep()
+ * publishes itself -- is captured by the sticky PENDING state in
+ * qemu_co_sleep_wake() and consumed here without yielding.
*/
qemu_co_sleep(w);
timer_del(&ts);
--
2.53.0
^ permalink raw reply related [flat|nested] 10+ messages in thread
* Re: [PATCH v3 0/1] coroutine: fix lost wakeup in qemu_co_sleep_wake()
2026-06-10 11:58 [PATCH v3 0/1] coroutine: fix lost wakeup in qemu_co_sleep_wake() Denis V. Lunev via qemu development
2026-06-10 11:58 ` [PATCH v3 1/1] " Denis V. Lunev via qemu development
@ 2026-06-18 17:57 ` Denis V. Lunev
2026-06-25 23:23 ` Denis V. Lunev
` (2 subsequent siblings)
4 siblings, 0 replies; 10+ messages in thread
From: Denis V. Lunev @ 2026-06-18 17:57 UTC (permalink / raw)
To: Denis V. Lunev, qemu-devel
Cc: qemu-block, qemu-stable, Kevin Wolf, Stefan Hajnoczi, Hanna Reitz
On 6/10/26 13:58, Denis V. Lunev wrote:
> Changes since v2
> ----------------
> * Drop the redundant fast-path check in qemu_co_sleep(); the publish
> cmpxchg already consumes a pending wake (Kevin).
> * Add a deterministic regression test, no threads or timers needed.
>
> Changes since v1
> ----------------
> * Patch 1 (graph-lock) applied as e3082ab3b3, dropped here.
> * Fix the qemu_co_sleep_wake() primitive instead of working around
> it in qcow2 (Kevin).
>
> Problem
> -------
>
> The qemu shutdown / blockdev-close path can deadlock permanently on
> upstream master. The main thread enters ppoll(timeout=-1) holding
> BQL, no other thread has a wake source that points back at it, and
> qemu has to be SIGKILLed. The hang has no timeout -- it is a hard
> deadlock, not a slow operation; behind BQL, RCU, VCPUs and every
> iothread path that needs BQL stall with it.
>
> The race exposed in qcow2's cache_clean_timer cancellation path:
>
> ppoll -> aio_poll -> cache_clean_timer_del_and_wait -> qcow2_close
>
> The race diagram and the exact stale-state read are in the patch's
> commit message.
>
> Reproducer
> ----------
>
> Environment: 4-vCPU VM guest, kernel 6.12.x, upstream master at
> de5d8bfd61. On modern bare-metal the window is narrow enough that the
> hang rarely reproduces without a VM -- a VM guest under full CPU
> saturation is what makes the timing reliable.
>
> # reproducer
> stress-ng --cpu "$(nproc)" --timeout 0 &
> for r in $(seq 20); do
> timeout 120 ./build/tests/qemu-iotests/check -qcow2 iothreads-create
> done
> kill %1
>
> With `stress-ng --cpu $(nproc)` the race surfaces. With
> `stress-ng --cpu $(($(nproc) - 1))` or without a stressor it does
> not reproduce reliably across 20 iterations.
>
> The new unit test reproduces the same lost wakeup deterministically,
> without a VM or a stressor.
>
> Results
> -------
>
> Same guest, 20 iterations of the loop above, master at de5d8bfd61:
>
> without this patch: reproduces reliably (qcow2_close in ppoll)
> with this patch: 20/20 PASS
>
> Signed-off-by: Denis V. Lunev <den@openvz.org>
> Cc: Kevin Wolf <kwolf@redhat.com>
> Cc: Hanna Reitz <hreitz@redhat.com>
>
> Denis V. Lunev (1):
> coroutine: fix lost wakeup in qemu_co_sleep_wake()
>
> include/qemu/coroutine.h | 17 +++++++++---
> tests/unit/test-coroutine.c | 53 +++++++++++++++++++++++++++++++++++++
> util/qemu-coroutine-sleep.c | 53 ++++++++++++++++++++++++++-----------
> 3 files changed, 104 insertions(+), 19 deletions(-)
>
ping
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [PATCH v3 0/1] coroutine: fix lost wakeup in qemu_co_sleep_wake()
2026-06-10 11:58 [PATCH v3 0/1] coroutine: fix lost wakeup in qemu_co_sleep_wake() Denis V. Lunev via qemu development
2026-06-10 11:58 ` [PATCH v3 1/1] " Denis V. Lunev via qemu development
2026-06-18 17:57 ` [PATCH v3 0/1] " Denis V. Lunev
@ 2026-06-25 23:23 ` Denis V. Lunev
2026-07-02 17:47 ` Denis V. Lunev
2026-07-09 12:56 ` Denis V. Lunev
4 siblings, 0 replies; 10+ messages in thread
From: Denis V. Lunev @ 2026-06-25 23:23 UTC (permalink / raw)
To: Denis V. Lunev, qemu-devel
Cc: qemu-block, qemu-stable, Kevin Wolf, Stefan Hajnoczi, Hanna Reitz
On 6/10/26 13:58, Denis V. Lunev wrote:
> Changes since v2
> ----------------
> * Drop the redundant fast-path check in qemu_co_sleep(); the publish
> cmpxchg already consumes a pending wake (Kevin).
> * Add a deterministic regression test, no threads or timers needed.
>
> Changes since v1
> ----------------
> * Patch 1 (graph-lock) applied as e3082ab3b3, dropped here.
> * Fix the qemu_co_sleep_wake() primitive instead of working around
> it in qcow2 (Kevin).
>
> Problem
> -------
>
> The qemu shutdown / blockdev-close path can deadlock permanently on
> upstream master. The main thread enters ppoll(timeout=-1) holding
> BQL, no other thread has a wake source that points back at it, and
> qemu has to be SIGKILLed. The hang has no timeout -- it is a hard
> deadlock, not a slow operation; behind BQL, RCU, VCPUs and every
> iothread path that needs BQL stall with it.
>
> The race exposed in qcow2's cache_clean_timer cancellation path:
>
> ppoll -> aio_poll -> cache_clean_timer_del_and_wait -> qcow2_close
>
> The race diagram and the exact stale-state read are in the patch's
> commit message.
>
> Reproducer
> ----------
>
> Environment: 4-vCPU VM guest, kernel 6.12.x, upstream master at
> de5d8bfd61. On modern bare-metal the window is narrow enough that the
> hang rarely reproduces without a VM -- a VM guest under full CPU
> saturation is what makes the timing reliable.
>
> # reproducer
> stress-ng --cpu "$(nproc)" --timeout 0 &
> for r in $(seq 20); do
> timeout 120 ./build/tests/qemu-iotests/check -qcow2 iothreads-create
> done
> kill %1
>
> With `stress-ng --cpu $(nproc)` the race surfaces. With
> `stress-ng --cpu $(($(nproc) - 1))` or without a stressor it does
> not reproduce reliably across 20 iterations.
>
> The new unit test reproduces the same lost wakeup deterministically,
> without a VM or a stressor.
>
> Results
> -------
>
> Same guest, 20 iterations of the loop above, master at de5d8bfd61:
>
> without this patch: reproduces reliably (qcow2_close in ppoll)
> with this patch: 20/20 PASS
>
> Signed-off-by: Denis V. Lunev <den@openvz.org>
> Cc: Kevin Wolf <kwolf@redhat.com>
> Cc: Hanna Reitz <hreitz@redhat.com>
>
> Denis V. Lunev (1):
> coroutine: fix lost wakeup in qemu_co_sleep_wake()
>
> include/qemu/coroutine.h | 17 +++++++++---
> tests/unit/test-coroutine.c | 53 +++++++++++++++++++++++++++++++++++++
> util/qemu-coroutine-sleep.c | 53 ++++++++++++++++++++++++++-----------
> 3 files changed, 104 insertions(+), 19 deletions(-)
>
ping v2
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [PATCH v3 0/1] coroutine: fix lost wakeup in qemu_co_sleep_wake()
2026-06-10 11:58 [PATCH v3 0/1] coroutine: fix lost wakeup in qemu_co_sleep_wake() Denis V. Lunev via qemu development
` (2 preceding siblings ...)
2026-06-25 23:23 ` Denis V. Lunev
@ 2026-07-02 17:47 ` Denis V. Lunev
2026-07-09 12:56 ` Denis V. Lunev
4 siblings, 0 replies; 10+ messages in thread
From: Denis V. Lunev @ 2026-07-02 17:47 UTC (permalink / raw)
To: Denis V. Lunev, qemu-devel
Cc: qemu-block, qemu-stable, Kevin Wolf, Stefan Hajnoczi, Hanna Reitz
On 6/10/26 13:58, Denis V. Lunev wrote:
> Changes since v2
> ----------------
> * Drop the redundant fast-path check in qemu_co_sleep(); the publish
> cmpxchg already consumes a pending wake (Kevin).
> * Add a deterministic regression test, no threads or timers needed.
>
> Changes since v1
> ----------------
> * Patch 1 (graph-lock) applied as e3082ab3b3, dropped here.
> * Fix the qemu_co_sleep_wake() primitive instead of working around
> it in qcow2 (Kevin).
>
> Problem
> -------
>
> The qemu shutdown / blockdev-close path can deadlock permanently on
> upstream master. The main thread enters ppoll(timeout=-1) holding
> BQL, no other thread has a wake source that points back at it, and
> qemu has to be SIGKILLed. The hang has no timeout -- it is a hard
> deadlock, not a slow operation; behind BQL, RCU, VCPUs and every
> iothread path that needs BQL stall with it.
>
> The race exposed in qcow2's cache_clean_timer cancellation path:
>
> ppoll -> aio_poll -> cache_clean_timer_del_and_wait -> qcow2_close
>
> The race diagram and the exact stale-state read are in the patch's
> commit message.
>
> Reproducer
> ----------
>
> Environment: 4-vCPU VM guest, kernel 6.12.x, upstream master at
> de5d8bfd61. On modern bare-metal the window is narrow enough that the
> hang rarely reproduces without a VM -- a VM guest under full CPU
> saturation is what makes the timing reliable.
>
> # reproducer
> stress-ng --cpu "$(nproc)" --timeout 0 &
> for r in $(seq 20); do
> timeout 120 ./build/tests/qemu-iotests/check -qcow2 iothreads-create
> done
> kill %1
>
> With `stress-ng --cpu $(nproc)` the race surfaces. With
> `stress-ng --cpu $(($(nproc) - 1))` or without a stressor it does
> not reproduce reliably across 20 iterations.
>
> The new unit test reproduces the same lost wakeup deterministically,
> without a VM or a stressor.
>
> Results
> -------
>
> Same guest, 20 iterations of the loop above, master at de5d8bfd61:
>
> without this patch: reproduces reliably (qcow2_close in ppoll)
> with this patch: 20/20 PASS
>
> Signed-off-by: Denis V. Lunev <den@openvz.org>
> Cc: Kevin Wolf <kwolf@redhat.com>
> Cc: Hanna Reitz <hreitz@redhat.com>
>
> Denis V. Lunev (1):
> coroutine: fix lost wakeup in qemu_co_sleep_wake()
>
> include/qemu/coroutine.h | 17 +++++++++---
> tests/unit/test-coroutine.c | 53 +++++++++++++++++++++++++++++++++++++
> util/qemu-coroutine-sleep.c | 53 ++++++++++++++++++++++++++-----------
> 3 files changed, 104 insertions(+), 19 deletions(-)
>
ping v3
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [PATCH v3 0/1] coroutine: fix lost wakeup in qemu_co_sleep_wake()
2026-06-10 11:58 [PATCH v3 0/1] coroutine: fix lost wakeup in qemu_co_sleep_wake() Denis V. Lunev via qemu development
` (3 preceding siblings ...)
2026-07-02 17:47 ` Denis V. Lunev
@ 2026-07-09 12:56 ` Denis V. Lunev
4 siblings, 0 replies; 10+ messages in thread
From: Denis V. Lunev @ 2026-07-09 12:56 UTC (permalink / raw)
To: Denis V. Lunev, qemu-devel
Cc: qemu-block, qemu-stable, Kevin Wolf, Stefan Hajnoczi, Hanna Reitz
On 6/10/26 13:58, Denis V. Lunev wrote:
> Changes since v2
> ----------------
> * Drop the redundant fast-path check in qemu_co_sleep(); the publish
> cmpxchg already consumes a pending wake (Kevin).
> * Add a deterministic regression test, no threads or timers needed.
>
> Changes since v1
> ----------------
> * Patch 1 (graph-lock) applied as e3082ab3b3, dropped here.
> * Fix the qemu_co_sleep_wake() primitive instead of working around
> it in qcow2 (Kevin).
>
> Problem
> -------
>
> The qemu shutdown / blockdev-close path can deadlock permanently on
> upstream master. The main thread enters ppoll(timeout=-1) holding
> BQL, no other thread has a wake source that points back at it, and
> qemu has to be SIGKILLed. The hang has no timeout -- it is a hard
> deadlock, not a slow operation; behind BQL, RCU, VCPUs and every
> iothread path that needs BQL stall with it.
>
> The race exposed in qcow2's cache_clean_timer cancellation path:
>
> ppoll -> aio_poll -> cache_clean_timer_del_and_wait -> qcow2_close
>
> The race diagram and the exact stale-state read are in the patch's
> commit message.
>
> Reproducer
> ----------
>
> Environment: 4-vCPU VM guest, kernel 6.12.x, upstream master at
> de5d8bfd61. On modern bare-metal the window is narrow enough that the
> hang rarely reproduces without a VM -- a VM guest under full CPU
> saturation is what makes the timing reliable.
>
> # reproducer
> stress-ng --cpu "$(nproc)" --timeout 0 &
> for r in $(seq 20); do
> timeout 120 ./build/tests/qemu-iotests/check -qcow2 iothreads-create
> done
> kill %1
>
> With `stress-ng --cpu $(nproc)` the race surfaces. With
> `stress-ng --cpu $(($(nproc) - 1))` or without a stressor it does
> not reproduce reliably across 20 iterations.
>
> The new unit test reproduces the same lost wakeup deterministically,
> without a VM or a stressor.
>
> Results
> -------
>
> Same guest, 20 iterations of the loop above, master at de5d8bfd61:
>
> without this patch: reproduces reliably (qcow2_close in ppoll)
> with this patch: 20/20 PASS
>
> Signed-off-by: Denis V. Lunev <den@openvz.org>
> Cc: Kevin Wolf <kwolf@redhat.com>
> Cc: Hanna Reitz <hreitz@redhat.com>
>
> Denis V. Lunev (1):
> coroutine: fix lost wakeup in qemu_co_sleep_wake()
>
> include/qemu/coroutine.h | 17 +++++++++---
> tests/unit/test-coroutine.c | 53 +++++++++++++++++++++++++++++++++++++
> util/qemu-coroutine-sleep.c | 53 ++++++++++++++++++++++++++-----------
> 3 files changed, 104 insertions(+), 19 deletions(-)
>
ping v4
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [PATCH v3 1/1] coroutine: fix lost wakeup in qemu_co_sleep_wake()
2026-06-10 11:58 ` [PATCH v3 1/1] " Denis V. Lunev via qemu development
@ 2026-07-09 18:55 ` Philippe Mathieu-Daudé
2026-07-09 19:00 ` Denis V. Lunev
0 siblings, 1 reply; 10+ messages in thread
From: Philippe Mathieu-Daudé @ 2026-07-09 18:55 UTC (permalink / raw)
To: Denis V. Lunev, qemu-devel, Peter Xu, Akihiko Odaki
Cc: qemu-block, qemu-stable, Kevin Wolf, Stefan Hajnoczi, Hanna Reitz
Hi Denis,
I tried to review, but not my area and way out of my confort zone...
Cc'ing Akihiko & Peter who might have more familiarity :)
On 10/6/26 13:58, Denis V. Lunev via qemu development wrote:
> cache_clean_timer_del_and_wait() cancels the cache-cleaner coroutine
> by setting s->cache_clean_interval = 0 and calling qemu_co_sleep_wake()
> to cut short its qemu_co_sleep_ns_wakeable(). qemu_co_sleep_wake() is
> fire-and-forget: it reads w->to_wake and silently returns when it is
> NULL. A sleeper that is between two iterations -- has just released
> s->lock but has not yet set w->to_wake inside qemu_co_sleep() -- loses
> the wake:
>
> iothread0 timer coroutine main thread (qcow2 close)
> ------------------------- -------------------------
> while-body (holding s->lock):
> read interval = 600
> wait_ns = 600 * NS
> release s->lock
> take s->lock
> interval = 0
> qemu_co_sleep_wake(w):
> w->to_wake == NULL -> skip
> return
> qemu_co_queue_wait(exit, s->lock):
> release s->lock
> yield
> qemu_co_sleep_ns_wakeable:
> aio_timer_init(+600 s)
> qemu_co_sleep:
> cas scheduled NULL -> "qsns"
> w->to_wake = co
> yield [sleeps 600 s]
>
> cache_clean_timer_del_and_wait() then blocks on cache_clean_timer_exit
> until the original 600 s expiry fires, and qcow2_close() holds BQL the
> whole time so the VM stalls behind it.
>
> block_copy_kick() has the same shape. Fix the primitive once instead
> of working around it in each caller.
>
> Use a tri-state for QemuCoSleep::to_wake:
>
> NULL - idle
> co - sleeper parked
> PENDING - wake delivered, no sleeper yet (sticky)
>
> qemu_co_sleep_wake() xchgs PENDING into to_wake: a real sleeper is
> woken, NULL/PENDING is left untouched so the wake stays sticky.
> qemu_co_sleep() cmpxchg-publishes itself as the sleeper; if a wake
> was delivered before it got there or races the publish, the cmpxchg
> observes PENDING and returns without yielding. On normal resume
> qemu_co_sleep() clears the PENDING the waker left behind so the next
> sleep starts clean.
>
> A double-fire (real wake plus timer callback) is harmless: the first
> xchg returns the coroutine and wakes it; the second returns PENDING
> and is a no-op. Cancellation latency through qemu_co_sleep_wake() is
> now bounded by aio_co_wake() rather than by the sleep duration.
>
> Fixes: f86dde9a15 ("qcow2: Fix cache_clean_timer")
> Signed-off-by: Denis V. Lunev <den@openvz.org>
> Cc: Hanna Czenczek <hreitz@redhat.com>
> Cc: Kevin Wolf <kwolf@redhat.com>
> ---
> include/qemu/coroutine.h | 17 +++++++++---
> tests/unit/test-coroutine.c | 53 +++++++++++++++++++++++++++++++++++++
> util/qemu-coroutine-sleep.c | 53 ++++++++++++++++++++++++++-----------
> 3 files changed, 104 insertions(+), 19 deletions(-)
>
> diff --git a/include/qemu/coroutine.h b/include/qemu/coroutine.h
> index e545bbf620..1c31de60f9 100644
> --- a/include/qemu/coroutine.h
> +++ b/include/qemu/coroutine.h
> @@ -260,10 +260,19 @@ int coroutine_fn qemu_co_timeout(CoroutineEntry *entry, void *opaque,
> uint64_t timeout_ns, CleanupFunc clean);
>
> /**
> - * Wake a coroutine if it is sleeping in qemu_co_sleep_ns. The timer will be
> - * deleted. @sleep_state must be the variable whose address was given to
> - * qemu_co_sleep_ns() and should be checked to be non-NULL before calling
> - * qemu_co_sleep_wake().
> + * Wake a coroutine sleeping in qemu_co_sleep() or qemu_co_sleep_ns_wakeable().
> + * The timer set up by the latter is deleted on wakeup.
> + *
> + * The wake is sticky: if no sleeper is parked on @w at the time of the call,
> + * the wake is recorded on @w and consumed by the next qemu_co_sleep() on the
> + * same @w, which then returns without yielding. This closes the lost-wakeup
> + * window between two sleeps and is the documented behavior callers should
> + * rely on -- e.g. a cancellation signal raised between iterations of a
> + * sleep/work loop will shorten the next sleep instead of being dropped.
> + *
> + * The state persists until consumed: if no further qemu_co_sleep() is ever
> + * called on @w, the pending wake is harmlessly discarded when @w goes away.
> + * Multiple wakes coalesce -- the next sleep consumes at most one.
> */
> void qemu_co_sleep_wake(QemuCoSleep *w);
>
> diff --git a/tests/unit/test-coroutine.c b/tests/unit/test-coroutine.c
> index 49d4d9b251..aa1f719b08 100644
> --- a/tests/unit/test-coroutine.c
> +++ b/tests/unit/test-coroutine.c
> @@ -421,6 +421,57 @@ static void test_co_rwlock_downgrade(void)
> g_assert(c1_done);
> }
>
> +/*
> + * Check that a wake delivered before the sleeper parks is not lost.
> + *
> + * qemu_co_sleep_wake() is fire-and-forget: a caller cancelling a
> + * sleep/work loop may call it in the window after the sleeper has
> + * decided to sleep but before it has published itself inside
> + * qemu_co_sleep(). The wake must be sticky and shorten the next sleep
> + * rather than being dropped (which would block until the full sleep
> + * duration expired).
> + *
> + * No threads, timers or AioContext are needed: coroutines are
> + * cooperative, so ordering the wake before the sleep deterministically
> + * reproduces the state the racing waker would otherwise produce.
> + */
> +
> +typedef struct {
> + QemuCoSleep w;
> + bool completed;
> +} CoSleepWakeData;
> +
> +static void coroutine_fn co_sleep_wake_entry(void *opaque)
> +{
> + CoSleepWakeData *d = opaque;
> +
> + /*
> + * The wake was already delivered before we got here. qemu_co_sleep()
> + * must consume it and return without yielding.
> + */
> + qemu_co_sleep(&d->w);
> + d->completed = true;
> +}
> +
> +static void test_co_sleep_wake_before_sleep(void)
> +{
> + CoSleepWakeData d = { .w = { 0 }, .completed = false };
> + Coroutine *co = qemu_coroutine_create(co_sleep_wake_entry, &d);
> +
> + /* Waker runs first, while no sleeper is parked on w. */
> + qemu_co_sleep_wake(&d.w);
> +
> + /*
> + * Entering runs qemu_co_sleep(), which consumes the pending wake and
> + * returns without yielding, so the coroutine runs straight to
> + * completion in this single enter. With the pre-fix primitive the wake
> + * is dropped, qemu_co_sleep() parks, and completed stays false.
> + */
> + qemu_coroutine_enter(co);
> +
> + g_assert(d.completed);
> +}
> +
> /*
> * Check that creation, enter, and return work
> */
> @@ -660,6 +711,8 @@ int main(int argc, char **argv)
> g_test_add_func("/locking/co-mutex/lockable", test_co_mutex_lockable);
> g_test_add_func("/locking/co-rwlock/upgrade", test_co_rwlock_upgrade);
> g_test_add_func("/locking/co-rwlock/downgrade", test_co_rwlock_downgrade);
> + g_test_add_func("/locking/co-sleep/wake-before-sleep",
> + test_co_sleep_wake_before_sleep);
> if (g_test_perf()) {
> g_test_add_func("/perf/lifecycle", perf_lifecycle);
> g_test_add_func("/perf/nesting", perf_nesting);
> diff --git a/util/qemu-coroutine-sleep.c b/util/qemu-coroutine-sleep.c
> index edef117284..19ded0b6fd 100644
> --- a/util/qemu-coroutine-sleep.c
> +++ b/util/qemu-coroutine-sleep.c
> @@ -18,20 +18,29 @@
>
> static const char *qemu_co_sleep_ns__scheduled = "qemu_co_sleep_ns";
>
> +/*
> + * Sentinel stored in QemuCoSleep::to_wake by qemu_co_sleep_wake() when no
> + * sleeper has parked yet. The next qemu_co_sleep() consumes it and returns
> + * without yielding, so a wake that races the arming of a sleep is never
> + * lost.
> + */
> +#define QEMU_CO_SLEEP_PENDING ((Coroutine *)(uintptr_t)1)
> +
> void qemu_co_sleep_wake(QemuCoSleep *w)
> {
> Coroutine *co;
>
> - co = w->to_wake;
> - w->to_wake = NULL;
> - if (co) {
> - /* Write of schedule protected by barrier write in aio_co_schedule */
> - const char *scheduled = qatomic_cmpxchg(&co->scheduled,
> - qemu_co_sleep_ns__scheduled, NULL);
> -
> - assert(scheduled == qemu_co_sleep_ns__scheduled);
> - aio_co_wake(co);
> + co = qatomic_xchg(&w->to_wake, QEMU_CO_SLEEP_PENDING);
> + if (co == NULL || co == QEMU_CO_SLEEP_PENDING) {
> + /* No sleeper, or a wake is already pending. */
> + return;
> }
> +
> + /* Write of scheduled protected by barrier write in aio_co_schedule */
> + const char *scheduled = qatomic_cmpxchg(&co->scheduled,
> + qemu_co_sleep_ns__scheduled, NULL);
> + assert(scheduled == qemu_co_sleep_ns__scheduled);
> + aio_co_wake(co);
> }
>
> static void co_sleep_cb(void *opaque)
> @@ -43,6 +52,7 @@ static void co_sleep_cb(void *opaque)
> void coroutine_fn qemu_co_sleep(QemuCoSleep *w)
> {
> Coroutine *co = qemu_coroutine_self();
> + Coroutine *prev;
>
> const char *scheduled = qatomic_cmpxchg(&co->scheduled, NULL,
> qemu_co_sleep_ns__scheduled);
> @@ -53,11 +63,23 @@ void coroutine_fn qemu_co_sleep(QemuCoSleep *w)
> abort();
> }
>
> - w->to_wake = co;
> + /*
> + * Publish ourselves as the sleeper. A wake delivered before we got here,
> + * or one racing this publish, leaves QEMU_CO_SLEEP_PENDING in to_wake;
> + * the cmpxchg then fails and we consume the wake without yielding.
> + */
> + prev = qatomic_cmpxchg(&w->to_wake, NULL, co);
> + if (prev == QEMU_CO_SLEEP_PENDING) {
> + qatomic_set(&w->to_wake, NULL);
> + qatomic_set(&co->scheduled, NULL);
> + return;
> + }
> + assert(prev == NULL);
> +
> qemu_coroutine_yield();
>
> - /* w->to_wake is cleared before resuming this coroutine. */
> - assert(w->to_wake == NULL);
> + /* The waker left QEMU_CO_SLEEP_PENDING; clear it for the next sleep. */
> + qatomic_set(&w->to_wake, NULL);
> }
>
> void coroutine_fn qemu_co_sleep_ns_wakeable(QemuCoSleep *w,
> @@ -70,9 +92,10 @@ void coroutine_fn qemu_co_sleep_ns_wakeable(QemuCoSleep *w,
> timer_mod(&ts, qemu_clock_get_ns(type) + ns);
>
> /*
> - * The timer will fire in the current AiOContext, so the callback
> - * must happen after qemu_co_sleep yields and there is no race
> - * between timer_mod and qemu_co_sleep.
> + * A wake racing with the arming of the sleep -- including the timer
> + * we just armed firing in another AioContext before qemu_co_sleep()
> + * publishes itself -- is captured by the sticky PENDING state in
> + * qemu_co_sleep_wake() and consumed here without yielding.
> */
> qemu_co_sleep(w);
> timer_del(&ts);
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [PATCH v3 1/1] coroutine: fix lost wakeup in qemu_co_sleep_wake()
2026-07-09 18:55 ` Philippe Mathieu-Daudé
@ 2026-07-09 19:00 ` Denis V. Lunev
2026-07-09 19:43 ` Peter Xu
0 siblings, 1 reply; 10+ messages in thread
From: Denis V. Lunev @ 2026-07-09 19:00 UTC (permalink / raw)
To: Philippe Mathieu-Daudé, Denis V. Lunev, qemu-devel, Peter Xu,
Akihiko Odaki
Cc: qemu-block, qemu-stable, Kevin Wolf, Stefan Hajnoczi, Hanna Reitz
On 7/9/26 20:55, Philippe Mathieu-Daudé wrote:
> Hi Denis,
>
> I tried to review, but not my area and way out of my confort zone...
> Cc'ing Akihiko & Peter who might have more familiarity :)
>
I thought that v3 should go fast, this is v3. Not too much
changes + unit test. I was wrong :(
Den
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [PATCH v3 1/1] coroutine: fix lost wakeup in qemu_co_sleep_wake()
2026-07-09 19:00 ` Denis V. Lunev
@ 2026-07-09 19:43 ` Peter Xu
2026-07-09 20:54 ` Denis V. Lunev
0 siblings, 1 reply; 10+ messages in thread
From: Peter Xu @ 2026-07-09 19:43 UTC (permalink / raw)
To: Denis V. Lunev, Hanna Czenczek
Cc: Philippe Mathieu-Daudé, Denis V. Lunev, qemu-devel,
Akihiko Odaki, qemu-block, qemu-stable, Kevin Wolf,
Stefan Hajnoczi, Hanna Reitz, Paolo Bonzini
On Thu, Jul 09, 2026 at 09:00:52PM +0200, Denis V. Lunev wrote:
> On 7/9/26 20:55, Philippe Mathieu-Daudé wrote:
> > Hi Denis,
> >
> > I tried to review, but not my area and way out of my confort zone...
> > Cc'ing Akihiko & Peter who might have more familiarity :)
I'm always scared to work on coroutines.. :)
>
> I thought that v3 should go fast, this is v3. Not too much
> changes + unit test. I was wrong :(
This does look like to fix a real issue. I'm also surprised it got
overlooked until now. Said that, I still think some block people should
look at this..
Kevin and Stefan are both on PTO, getting back either in 1 or 2 weeks.
Let's see if Hanna will start to notice? If there's still no update in 2
weeks, I can help to reach out to them.
PS: I copied Paolo too; normally he's flooded, but sometimes he'll reply.
Thanks,
--
Peter Xu
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [PATCH v3 1/1] coroutine: fix lost wakeup in qemu_co_sleep_wake()
2026-07-09 19:43 ` Peter Xu
@ 2026-07-09 20:54 ` Denis V. Lunev
0 siblings, 0 replies; 10+ messages in thread
From: Denis V. Lunev @ 2026-07-09 20:54 UTC (permalink / raw)
To: Peter Xu, Hanna Czenczek
Cc: Philippe Mathieu-Daudé, Denis V. Lunev, qemu-devel,
Akihiko Odaki, qemu-block, qemu-stable, Kevin Wolf,
Stefan Hajnoczi, Paolo Bonzini
On 7/9/26 21:43, Peter Xu wrote:
> On Thu, Jul 09, 2026 at 09:00:52PM +0200, Denis V. Lunev wrote:
>> On 7/9/26 20:55, Philippe Mathieu-Daudé wrote:
>>> Hi Denis,
>>>
>>> I tried to review, but not my area and way out of my confort zone...
>>> Cc'ing Akihiko & Peter who might have more familiarity :)
> I'm always scared to work on coroutines.. :)
>
>> I thought that v3 should go fast, this is v3. Not too much
>> changes + unit test. I was wrong :(
> This does look like to fix a real issue. I'm also surprised it got
> overlooked until now. Said that, I still think some block people should
> look at this..
>
> Kevin and Stefan are both on PTO, getting back either in 1 or 2 weeks.
> Let's see if Hanna will start to notice? If there's still no update in 2
> weeks, I can help to reach out to them.
>
> PS: I copied Paolo too; normally he's flooded, but sometimes he'll reply.
>
> Thanks,
>
me too. We have got a bunch of scare issues once tried to
reproduce disc corruption with discard and faced several
real hangs. This one is the last of them and I was able
to repro one before and not able to repro one with the
patch.
Though I could be definitely wrong here :-)
Den
^ permalink raw reply [flat|nested] 10+ messages in thread
end of thread, other threads:[~2026-07-09 20:55 UTC | newest]
Thread overview: 10+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-10 11:58 [PATCH v3 0/1] coroutine: fix lost wakeup in qemu_co_sleep_wake() Denis V. Lunev via qemu development
2026-06-10 11:58 ` [PATCH v3 1/1] " Denis V. Lunev via qemu development
2026-07-09 18:55 ` Philippe Mathieu-Daudé
2026-07-09 19:00 ` Denis V. Lunev
2026-07-09 19:43 ` Peter Xu
2026-07-09 20:54 ` Denis V. Lunev
2026-06-18 17:57 ` [PATCH v3 0/1] " Denis V. Lunev
2026-06-25 23:23 ` Denis V. Lunev
2026-07-02 17:47 ` Denis V. Lunev
2026-07-09 12:56 ` Denis V. Lunev
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.