Correctly clean up keeping_mutexes before resuming any other threads. (#7460)

It's possible (but very rare) to have a race condition between setting
`mutex->fiber = NULL` and `thread_mutex_remove(th, mutex)` which results
in the following bug:

```
[BUG] invalid keeping_mutexes: Attempt to unlock a mutex which is not locked
```

Fixes <https://bugs.ruby-lang.org/issues/19480>.
This commit is contained in:
Samuel Williams 2023-03-07 20:23:00 +13:00 committed by GitHub
parent 011c08b643
commit 2c4b2053ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
Notes: git 2023-03-07 07:23:25 +00:00
Merged-By: ioquatix <samuel@codeotaku.com>
2 changed files with 28 additions and 31 deletions

View File

@ -428,7 +428,7 @@ rb_threadptr_unlock_all_locking_mutexes(rb_thread_t *th)
rb_mutex_t *mutex = th->keeping_mutexes; rb_mutex_t *mutex = th->keeping_mutexes;
th->keeping_mutexes = mutex->next_mutex; th->keeping_mutexes = mutex->next_mutex;
/* rb_warn("mutex #<%p> remains to be locked by terminated thread", (void *)mutexes); */ // rb_warn("mutex #<%p> was not unlocked by thread #<%p>", (void *)mutex, (void*)th);
const char *error_message = rb_mutex_unlock_th(mutex, th, mutex->fiber); const char *error_message = rb_mutex_unlock_th(mutex, th, mutex->fiber);
if (error_message) rb_bug("invalid keeping_mutexes: %s", error_message); if (error_message) rb_bug("invalid keeping_mutexes: %s", error_message);

View File

@ -435,46 +435,43 @@ rb_mutex_owned_p(VALUE self)
static const char * static const char *
rb_mutex_unlock_th(rb_mutex_t *mutex, rb_thread_t *th, rb_fiber_t *fiber) rb_mutex_unlock_th(rb_mutex_t *mutex, rb_thread_t *th, rb_fiber_t *fiber)
{ {
const char *err = NULL;
if (mutex->fiber == 0) { if (mutex->fiber == 0) {
err = "Attempt to unlock a mutex which is not locked"; return "Attempt to unlock a mutex which is not locked";
} }
else if (mutex->fiber != fiber) { else if (mutex->fiber != fiber) {
err = "Attempt to unlock a mutex which is locked by another thread/fiber"; return "Attempt to unlock a mutex which is locked by another thread/fiber";
} }
else {
struct sync_waiter *cur = 0, *next;
mutex->fiber = 0; struct sync_waiter *cur = 0, *next;
ccan_list_for_each_safe(&mutex->waitq, cur, next, node) {
ccan_list_del_init(&cur->node);
if (cur->th->scheduler != Qnil && cur->fiber) { mutex->fiber = 0;
rb_fiber_scheduler_unblock(cur->th->scheduler, cur->self, rb_fiberptr_self(cur->fiber)); thread_mutex_remove(th, mutex);
goto found;
} ccan_list_for_each_safe(&mutex->waitq, cur, next, node) {
else { ccan_list_del_init(&cur->node);
switch (cur->th->status) {
case THREAD_RUNNABLE: /* from someone else calling Thread#run */ if (cur->th->scheduler != Qnil && cur->fiber) {
case THREAD_STOPPED_FOREVER: /* likely (rb_mutex_lock) */ rb_fiber_scheduler_unblock(cur->th->scheduler, cur->self, rb_fiberptr_self(cur->fiber));
rb_threadptr_interrupt(cur->th); return NULL;
goto found; }
case THREAD_STOPPED: /* probably impossible */ else {
rb_bug("unexpected THREAD_STOPPED"); switch (cur->th->status) {
case THREAD_KILLED: case THREAD_RUNNABLE: /* from someone else calling Thread#run */
/* not sure about this, possible in exit GC? */ case THREAD_STOPPED_FOREVER: /* likely (rb_mutex_lock) */
rb_bug("unexpected THREAD_KILLED"); rb_threadptr_interrupt(cur->th);
continue; return NULL;
} case THREAD_STOPPED: /* probably impossible */
rb_bug("unexpected THREAD_STOPPED");
case THREAD_KILLED:
/* not sure about this, possible in exit GC? */
rb_bug("unexpected THREAD_KILLED");
continue;
} }
} }
found:
thread_mutex_remove(th, mutex);
} }
return err; // We did not find any threads to wake up, so we can just return with no error:
return NULL;
} }
/* /*