Files
ruby/thread_none.c
Douglas Eichelberger 44f7c6b00d Join getaddrinfo and getnameinfo threads before fork
A getaddrinfo thread calls getaddrinfo(3) inside rb_thread_prevent_fork,
but on glibc its thread exit takes the resolver configuration lock again
after the thread has released the fork lock. A fork that lands then
leaves the child with that lock held, and every later name lookup in the
child blocks forever. getnameinfo(3) attaches the same resolver state,
even for a name in /etc/hosts, and its thread ran outside
rb_thread_prevent_fork altogether.

Make the lookup threads joinable and have fork join any that may still
be exiting. As a lookup thread finishes, still inside
rb_thread_prevent_fork, it swaps itself into a single slot with an
atomic exchange and joins the thread it displaced, which has usually
exited by then, and rb_thread_acquire_fork_lock joins whatever is left
in the slot. Once lookups settle, at most one finished thread is left
unjoined. The slot holds pthread_t as a pointer, with NULL for empty,
which assumes pthread_t is scalar and never 0.

getnameinfo threads now run within rb_thread_prevent_fork and register
as they exit, as getaddrinfo threads do, and where getnameinfo runs
without a helper thread it is called within rb_thread_prevent_fork too.
So fork also waits for a reverse lookup in progress, as it already did
for a forward one. raddrinfo_pthread_create now always creates joinable
threads, which drops its pthread_detach fallback for platforms without
pthread_attr_setdetachstate.

The slot is kept across fork, so every fork path that keeps running
Ruby code has to take the fork lock. On the BSDs, Process.daemon forks
with daemon(3) and did not take it, so the daemon would inherit the
registered thread, and its next fork would abort in pthread_join with
ESRCH. Take and release the fork lock around daemon(3) as rb_fork_ruby
does. daemon(3) can fail in the child as well as in the parent, so
compare pids to tell which process returned.

[Bug #22406]
2026-10-14 10:46:00 +02:00

392 lines
7.1 KiB
C

/*
A thread interface implementation without any system thread.
Assumption:
* There is a only single thread in the ruby process
* No signal happens targeting the ruby process
Note:
* No thread switching in the VM
* No timer thread because thread switching won't happen
* No mutex guard because the VM won't be racy
*/
#ifdef THREAD_SYSTEM_DEPENDENT_IMPLEMENTATION
#include <time.h>
#if defined(__wasm__) && !defined(__EMSCRIPTEN__)
# include "wasm/machine.h"
#endif
// Do nothing for GVL
static void
thread_sched_to_running(struct rb_thread_sched *sched, rb_thread_t *th)
{
}
static void
thread_sched_to_waiting(struct rb_thread_sched *sched, rb_thread_t *th, bool yield_immediately)
{
}
#define thread_sched_to_dead(a,b) thread_sched_to_waiting(a,b,true)
static void
thread_sched_yield(struct rb_thread_sched *sched, rb_thread_t *th)
{
}
void
rb_thread_sched_init(struct rb_thread_sched *sched, bool atfork)
{
}
void
rb_thread_sched_destroy(struct rb_thread_sched *sched)
{
}
// Do nothing for mutex guard
void
rb_native_mutex_lock(rb_nativethread_lock_t *lock)
{
}
void
rb_native_mutex_unlock(rb_nativethread_lock_t *lock)
{
}
int
rb_native_mutex_trylock(rb_nativethread_lock_t *lock)
{
return 0;
}
void
rb_native_mutex_initialize(rb_nativethread_lock_t *lock)
{
}
void
rb_native_mutex_destroy(rb_nativethread_lock_t *lock)
{
}
void
rb_native_cond_initialize(rb_nativethread_cond_t *cond)
{
}
void
rb_native_cond_destroy(rb_nativethread_cond_t *cond)
{
}
void
rb_native_cond_signal(rb_nativethread_cond_t *cond)
{
}
void
rb_native_cond_broadcast(rb_nativethread_cond_t *cond)
{
}
void
rb_native_cond_wait(rb_nativethread_cond_t *cond, rb_nativethread_lock_t *mutex)
{
}
void
rb_native_cond_timedwait(rb_nativethread_cond_t *cond, rb_nativethread_lock_t *mutex, unsigned long msec)
{
}
// The only one thread in process
static rb_thread_t *ruby_native_thread;
rb_thread_t *
ruby_thread_from_native(void)
{
return ruby_native_thread;
}
int
ruby_thread_set_native(rb_thread_t *th)
{
if (th && th->ec) {
rb_ractor_set_current_ec(th->ractor, th->ec);
}
ruby_native_thread = th;
return 1; // always succeed
}
void
Init_native_thread(rb_thread_t *main_th)
{
// no TLS setup and no thread id setup
ruby_thread_set_native(main_th);
}
void
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)
{
#if defined(__wasm__) && !defined(__EMSCRIPTEN__)
th->ec->machine.stack_start = (VALUE *)rb_wasm_stack_get_base();
#endif
return 0; // success
}
static int
native_thread_create(rb_thread_t *th)
{
th->status = THREAD_KILLED;
rb_ractor_living_threads_remove(th->ractor, th);
rb_notimplement();
}
// Do nothing for handling ubf because no other thread doesn't exist and unblock anything
#define register_ubf_list(th) (void)(th)
#define unregister_ubf_list(th) (void)(th)
#define ubf_select 0
inline static void
ubf_wakeup_all_threads(void)
{
return;
}
inline static int
ubf_threads_empty(void)
{
return 1; // true
}
inline static void
ubf_list_atfork()
{
}
inline static void
ubf_timer_disarm(void)
{
}
// No timer thread because thread switching won't happen
#define TIMER_THREAD_CREATED_P() (1)
inline static void
rb_thread_create_timer_thread(void)
{
}
void
rb_thread_wakeup_timer_thread(int sig)
{
}
inline static int
native_stop_timer_thread(void)
{
return 1; // success
}
inline static void
native_reset_timer_thread(void)
{
}
// Do nothing for thread naming
inline static void
native_set_thread_name(rb_thread_t *th)
{
}
inline static void
native_set_another_thread_name(rb_nativethread_id_t thread_id, VALUE name)
{
}
// Don't expose native thread id for now to keep system's thread API agnostic
#define USE_NATIVE_THREAD_NATIVE_THREAD_ID 0
// No reserved fd for piping threads
int
rb_reserved_fd_p(int fd)
{
return 0; // not reserved
}
// Don't expose native thread info for now to keep system's thread API agnostic
rb_nativethread_id_t
rb_nativethread_self(void)
{
return NULL;
}
// Do nothing for sigwait things because of no signal assumption
// Q(katei): is this correct description?
int
rb_sigwait_fd_get(const rb_thread_t *th)
{
return -1;
}
NORETURN(void rb_sigwait_fd_put(rb_thread_t *, int));
void
rb_sigwait_fd_put(rb_thread_t *th, int fd)
{
rb_bug("not implemented, should not be called rb_sigwait_fd_put");
}
NORETURN(void rb_sigwait_sleep(const rb_thread_t *, int, const rb_hrtime_t *));
void
rb_sigwait_sleep(const rb_thread_t *th, int sigwait_fd, const rb_hrtime_t *rel)
{
rb_bug("not implemented, should not be called rb_sigwait_sleep");
}
static void
native_sleep(rb_thread_t *th, rb_hrtime_t *rel)
{
// No signal assumption allows the use of uninterruptible sleep
struct timespec ts;
(void)clock_nanosleep(CLOCK_REALTIME, 0, rb_hrtime2timespec(&ts, rel), NULL);
}
static int
native_fd_select(int n, rb_fdset_t *readfds, rb_fdset_t *writefds, rb_fdset_t *exceptfds, struct timeval *timeout, rb_thread_t *th)
{
return rb_fd_select(n, readfds, writefds, exceptfds, timeout);
}
static bool
th_has_dedicated_nt(const rb_thread_t *th)
{
return true;
}
void
rb_add_running_thread(rb_thread_t *th)
{
// do nothing
}
void
rb_del_running_thread(rb_thread_t *th)
{
// do nothing
}
void
rb_ractor_sched_wait_terminate(rb_vm_t *vm, rb_nativethread_cond_t *cond, unsigned long msec)
{
// do nothing: with no threads there is no other Ractor to wait for
}
void
rb_threadptr_sched_free(rb_thread_t *th)
{
// do nothing
}
void
rb_ractor_sched_barrier_start(rb_vm_t *vm, rb_ractor_t *cr)
{
// do nothing
}
void
rb_ractor_sched_barrier_join(rb_vm_t *vm, rb_ractor_t *cr)
{
// do nothing
}
void
rb_ractor_sched_barrier_end(rb_vm_t *vm, rb_ractor_t *cr)
{
// do nothing
}
NORETURN(void rb_ractor_sched_wait(rb_execution_context_t *ec, rb_ractor_t *cr, rb_unblock_function_t *ubf, void *ptr));
void
rb_ractor_sched_wait(rb_execution_context_t *ec, rb_ractor_t *cr, rb_unblock_function_t *ubf, void *ptr)
{
// nothing can wake this: with no threads there is nobody to send
rb_bug("unreachable");
}
void
rb_ractor_sched_wakeup(rb_ractor_t *r, rb_thread_t *th)
{
// do nothing
}
bool
rb_thread_lock_native_thread(void)
{
return false;
}
void *
rb_thread_prevent_fork(void *(*func)(void *), void *data)
{
return func(data);
}
void
rb_thread_register_exiting(void)
{
// do nothing
}
void
rb_thread_malloc_stack_set(rb_thread_t *th, void *stack, size_t stack_size)
{
// no-op
}
bool
rb_thread_event_hooks_registered_p(void)
{
return false; // hooks are not implemented on this platform
}
#endif /* THREAD_SYSTEM_DEPENDENT_IMPLEMENTATION */
void
rb_thread_sched_winding_begin(rb_vm_t *vm)
{
// nothing to count: rb_thread_sched_wait_winding below never waits
(void)vm;
}
void
rb_thread_sched_winding_end(rb_vm_t *vm)
{
(void)vm;
}
void
rb_thread_sched_wait_winding(rb_vm_t *vm)
{
// no coroutine (M:N) threads on this implementation: nothing winds down
// after leaving the living set (see thread_pthread.c)
(void)vm;
}