Free rb_native_thread memory at fork

We never freed any resources of rb_native_thread at fork because it would
cause it to hang. This is because it called rb_native_cond_destroy for
condition variables.  We can't call rb_native_cond_destroy here because
according to the specs of pthread_cond_destroy:

    Attempting to destroy a condition variable upon which other threads
    are currently blocked results in undefined behavior.

Specifically, glibc's pthread_cond_destroy waits on all the other listeners.
Since after forking all the threads are dead, the condition variable's
listeners will never wake up, so it will hang forever.

This commit changes it to only free the memory and none of the condition
variables.
This commit is contained in:
Peter Zhu 2025-06-11 16:21:11 -04:00
parent 5ec9a392cd
commit 6e36841dbd
Notes: git 2025-06-12 19:24:03 +00:00
4 changed files with 35 additions and 8 deletions

View File

@ -519,12 +519,8 @@ thread_cleanup_func(void *th_ptr, int atfork)
th->locking_mutex = Qfalse;
thread_cleanup_func_before_exec(th_ptr);
/*
* Unfortunately, we can't release native threading resource at fork
* because libc may have unstable locking state therefore touching
* a threading resource may cause a deadlock.
*/
if (atfork) {
native_thread_destroy_atfork(th->nt);
th->nt = NULL;
return;
}

View File

@ -137,6 +137,12 @@ ruby_mn_threads_params(void)
{
}
static void
native_thread_destroy_atfork(struct rb_native_thread *nt)
{
/* no-op */
}
static int
native_thread_init_stack(rb_thread_t *th, void *local_in_parent_frame)
{

View File

@ -1816,6 +1816,27 @@ native_thread_assign(struct rb_native_thread *nt, rb_thread_t *th)
th->nt = nt;
}
static void
native_thread_destroy_atfork(struct rb_native_thread *nt)
{
if (nt) {
/* We can't call rb_native_cond_destroy here because according to the
* specs of pthread_cond_destroy:
*
* Attempting to destroy a condition variable upon which other threads
* are currently blocked results in undefined behavior.
*
* Specifically, glibc's pthread_cond_destroy waits on all the other
* listeners. Since after forking all the threads are dead, the condition
* variable's listeners will never wake up, so it will hang forever.
*/
RB_ALTSTACK_FREE(nt->altstack);
ruby_xfree(nt->nt_context);
ruby_xfree(nt);
}
}
static void
native_thread_destroy(struct rb_native_thread *nt)
{
@ -1826,9 +1847,7 @@ native_thread_destroy(struct rb_native_thread *nt)
rb_native_cond_destroy(&nt->cond.intr);
}
RB_ALTSTACK_FREE(nt->altstack);
ruby_xfree(nt->nt_context);
ruby_xfree(nt);
native_thread_destroy_atfork(nt);
}
}

View File

@ -617,6 +617,12 @@ native_thread_init_stack(rb_thread_t *th, void *local_in_parent_frame)
th->ec->machine.stack_maxsize = size - space;
}
static void
native_thread_destroy_atfork(struct rb_native_thread *nt)
{
/* no-op */
}
#ifndef InterlockedExchangePointer
#define InterlockedExchangePointer(t, v) \
(void *)InterlockedExchange((long *)(t), (long)(v))