fix(lib): stop cbuf_put/get orphaning their mutex on cancellation, hanging vtaskmgr_fini

pthread_cond_wait() reacquires its mutex before honoring a cancellation,
but mylock.c installed no cleanup handler to release it again -- a
thread cancelled mid-wait terminated holding the lock forever. vtaskmgr's
pthread_cancel()-based teardown could land there from three angles
(dispatcher cancelled while polling the shared queue, or a vcpu racing
back into it after its own shutdown signal), permanently deadlocking any
other thread that touched the same queue and hanging vtaskmgr_fini()'s
pthread_join() indefinitely.

Reproduced live as xce_test hanging on Ctrl-D exit once a connection
context was entered; gdb confirmed a vcpu thread blocked forever on a
mutex owned by an already-exited thread ID.

Fix: cbuf_put()/cbuf_get()/cbuf_timedget() now unlock via a
pthread_cleanup_push/pop handler around the wait, so a cancelled waiter
always releases the mutex. vcpu_idle()/vcpu_entry() also recognize the
teardown poison pill and exit immediately instead of needlessly
re-announcing into the already-dead shared queue.

New test_vtaskmgr_teardown.c reproduces the race deterministically
(self-pinned to one CPU, since true multi-core parallelism rarely hits
the window) and bounds vtaskmgr_fini() with a timed join so a
regression fails fast instead of hanging the suite. Logged as bug-1810.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLSNJhNoULvVcUhXEFafQC
This commit is contained in:
Rongsong Shen 2026-09-15 04:13:53 +00:00
parent b7250c2ec6
commit 0f3919f195
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
9 changed files with 335 additions and 9 deletions

View file

@ -48,6 +48,24 @@ void vcpu_idle(vcpu_t* vcpu)
vtaskmgr_t* vtaskmgr = (vtaskmgr_t*)vcpu->owner;
vcpu->task = NULL;
/* vcpu_fini()'s teardown protocol (bind_vtask(NULL, vcpu)) can queue a
* shutdown "poison pill" into this vcpu's OWN wbuf while it is busy
* running a real task -- the pill then sits there until the next
* vcpu_idle() call. Consume it directly (non-blocking) instead of
* first re-announcing this vcpu into the SHARED vtaskmgr->wbuf: by
* the time any vcpu_fini() runs, vtaskmgr_fini() has already
* cancelled+joined the dispatcher thread, the sole consumer of
* vtaskmgr->wbuf, so re-announcing here is pure wasted work at best
* (bug-1810) -- mylock.c's cbuf_put()/cbuf_get() now install a
* cancellation cleanup handler so a cancel landing there can no
* longer orphan the mutex, but skipping the needless re-announce
* entirely is still the correct, minimal behavior once a shutdown has
* already been signaled. */
if (cbuf_tryget(&(vcpu->wbuf), (void*)&(vcpu->task)) == 0) {
return;
}
cbuf_put(&(vtaskmgr->wbuf), (void*)&(vcpu->cpuid));
cbuf_get(&(vcpu->wbuf), (void*)&(vcpu->task));
return;
@ -112,6 +130,20 @@ static void* vcpu_entry(void* arg)
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
} else {
vcpu_idle(my);
/* vcpu_fini()'s bind_vtask(NULL, vcpu) is the ONLY caller
* that ever delivers a NULL task through this vcpu's own
* wbuf -- the dispatcher (vdispatcher_entry()) only calls
* bind_vtask() when pick_eligible_task() returned non-NULL.
* A NULL task observed here is therefore unambiguously the
* shutdown signal: exit immediately rather than looping back
* into vcpu_idle(), which would re-announce this vcpu into
* the shared vtaskmgr->wbuf after vtaskmgr_fini() has
* already torn down its sole consumer (the dispatcher
* thread) -- see vcpu_idle()'s own comment (bug-1810). */
if (my->task == NULL) {
break;
}
}
pthread_testcancel();
}