diff --git a/configure.ac b/configure.ac index c2b1a078a0..1a6c9a314a 100644 --- a/configure.ac +++ b/configure.ac @@ -1152,6 +1152,7 @@ AC_CHECK_LIB(crypt, crypt) # glibc (GNU/Linux, GNU/Hurd, GNU/kFreeBSD) AC_CHECK_LIB(dl, dlopen) # Dynamic linking for SunOS/Solaris and SYSV AC_CHECK_LIB(dld, shl_load) # Dynamic linking for HP-UX AC_CHECK_LIB(socket, shutdown) # SunOS/Solaris +AC_CHECK_LIB(anl, getaddrinfo_a) dnl Checks for header files. AC_HEADER_DIRENT @@ -1941,6 +1942,7 @@ AC_CHECK_FUNCS(fsync) AC_CHECK_FUNCS(ftruncate) AC_CHECK_FUNCS(ftruncate64) # used for Win32 platform AC_CHECK_FUNCS(getattrlist) +AC_CHECK_FUNCS(getaddrinfo_a) AC_CHECK_FUNCS(getcwd) AC_CHECK_FUNCS(getgidx) AC_CHECK_FUNCS(getgrnam) diff --git a/ext/socket/raddrinfo.c b/ext/socket/raddrinfo.c index 211f05c7eb..282c58b952 100644 --- a/ext/socket/raddrinfo.c +++ b/ext/socket/raddrinfo.c @@ -340,6 +340,129 @@ rb_getaddrinfo(const char *node, const char *service, } #ifdef HAVE_GETADDRINFO_A +struct gaicbs { + struct gaicbs *next; + struct gaicb *gaicb; +}; + +/* linked list to retain all outstanding and ongoing requests */ +static struct gaicbs *requests = NULL; + +static void +gaicbs_add(struct gaicb *req) +{ + struct gaicbs *request; + + if (!req) return; + request = (struct gaicbs *)xmalloc(sizeof(struct gaicbs)); + request->gaicb = req; + request->next = requests; + + requests = request; +} + +static void +gaicbs_remove(struct gaicb *req) +{ + struct gaicbs *request = requests; + struct gaicbs *prev = NULL; + + if (!req) return; + + while (request) { + if (request->gaicb == req) break; + prev = request; + request = request->next; + } + + if (request) { + if (prev) { + prev->next = request->next; + } else { + requests = request->next; + } + xfree(request->gaicb); + xfree(request); + } +} + +static void +gaicbs_cancel_all(void) +{ + struct gaicbs *request = requests; + struct gaicbs *tmp, *prev = NULL; + int ret; + + while (request) { + ret = gai_cancel(request->gaicb); + if (ret == EAI_NOTCANCELED) { + // continue to next request + prev = request; + request = request->next; + } else { + // remove the request from the list + if (prev) { + prev->next = request->next; + } else { + requests = request->next; + } + tmp = request; + request = request->next; + xfree(tmp->gaicb); + xfree(tmp); + } + } +} + +static void +gaicbs_wait_all(void) +{ + struct gaicbs *tmp, *request = requests; + int size = 0; + + // count gaicbs + while (request) { + size++; + request = request->next; + } + + // create list to wait + const struct gaicb *reqs[size]; + request = requests; + for (int i=0; request; i++) { + reqs[i] = request->gaicb; + request = request->next; + } + + // wait requests + gai_suspend(reqs, size, NULL); // ignore result intentionally + + // cleanup + request = requests; + while (request) { + tmp = request; + request = request->next; + xfree(tmp->gaicb); + xfree(tmp); + } +} + +/* A mitigation for [Bug #17220]. + It cancels all outstanding requests and waits for ongoing requests. + Then, it waits internal worker threads in getaddrinfo_a(3) to be finished. */ +static VALUE +rb_getaddrinfo_a_before_exec(VALUE self) +{ + gaicbs_cancel_all(); + gaicbs_wait_all(); + + /* wait worker threads in getaddrinfo_a(3) to be finished. + worker threads will finish after 1 second sleep. */ + rb_thread_sleep(1); + + return Qnil; +} + int rb_getaddrinfo_a(const char *node, const char *service, const struct addrinfo *hints, @@ -355,21 +478,23 @@ rb_getaddrinfo_a(const char *node, const char *service, else { struct gai_suspend_arg arg; struct gaicb *reqs[1]; - struct gaicb req; + struct gaicb *req = (struct gaicb *)xmalloc(sizeof(struct gaicb)); - req.ar_name = node; - req.ar_service = service; - req.ar_request = hints; + req->ar_name = node; + req->ar_service = service; + req->ar_request = hints; - reqs[0] = &req; + reqs[0] = req; ret = getaddrinfo_a(GAI_NOWAIT, reqs, 1, NULL); if (ret) return ret; + gaicbs_add(req); - arg.req = &req; + arg.req = req; arg.timeout = timeout; ret = (int)(VALUE)rb_thread_call_without_gvl(nogvl_gai_suspend, &arg, RUBY_UBF_IO, 0); if (ret && ret != EAI_ALLDONE) { + gaicbs_remove(req); /* EAI_ALLDONE indicates that the request already completed and gai_suspend was redundant */ /* on Ubuntu 18.04 (or other systems), gai_suspend(3) returns EAI_SYSTEM/ENOENT on timeout */ if (ret == EAI_SYSTEM && errno == ENOENT) { @@ -379,9 +504,9 @@ rb_getaddrinfo_a(const char *node, const char *service, } } - ret = gai_error(reqs[0]); ai = reqs[0]->ar_result; + gaicbs_remove(req); } if (ret == 0) { @@ -2710,6 +2835,11 @@ rsock_init_addrinfo(void) rb_define_singleton_method(rb_cAddrinfo, "unix", addrinfo_s_unix, -1); #endif +#ifdef HAVE_GETADDRINFO_A + // internal method. it will be called from process.c before fork(). + rb_define_singleton_method(rb_cAddrinfo, "__getaddrinfo_a_before_exec", rb_getaddrinfo_a_before_exec, 0); +#endif + rb_define_method(rb_cAddrinfo, "afamily", addrinfo_afamily, 0); rb_define_method(rb_cAddrinfo, "pfamily", addrinfo_pfamily, 0); rb_define_method(rb_cAddrinfo, "socktype", addrinfo_socktype, 0); diff --git a/process.c b/process.c index 612d319947..c483380d1c 100644 --- a/process.c +++ b/process.c @@ -1536,6 +1536,28 @@ proc_detach(VALUE obj, VALUE pid) return rb_detach_process(NUM2PIDT(pid)); } + +#ifdef HAVE_GETADDRINFO_A +static VALUE +call_getaddrinfo_a_before_exec(VALUE unused) +{ + ID addrinfo = rb_intern("Addrinfo"); + if (rb_const_defined(rb_cObject, addrinfo)) { + VALUE rb_cAddrinfo = rb_const_get(rb_cObject, addrinfo); + if (RB_TYPE_P(rb_cAddrinfo, T_CLASS)) { + return rb_funcallv(rb_cAddrinfo, rb_intern("__getaddrinfo_a_before_exec"), 0, 0); + } + } + return Qnil; +} + +static void +getaddrinfo_a_before_exec(void) +{ + rb_protect(call_getaddrinfo_a_before_exec, Qnil, NULL); +} +#endif + /* This function should be async-signal-safe. Actually it is. */ static void before_exec_async_signal_safe(void) @@ -1545,6 +1567,11 @@ before_exec_async_signal_safe(void) static void before_exec_non_async_signal_safe(void) { +#ifdef HAVE_GETADDRINFO_A + /* A mitigation for [Bug #17220]. See ext/socket/raddrinfo.c */ + getaddrinfo_a_before_exec(); +#endif + /* * On Mac OS X 10.5.x (Leopard) or earlier, exec() may return ENOTSUP * if the process have multiple threads. Therefore we have to kill diff --git a/test/ruby/test_process.rb b/test/ruby/test_process.rb index f88324fbbc..fead911cab 100644 --- a/test/ruby/test_process.rb +++ b/test/ruby/test_process.rb @@ -2498,4 +2498,14 @@ def test_exec_failure_leaves_no_child end assert_empty(Process.waitall) end + + def test_getaddrinfo_after_fork + skip "fork not supported" unless Process.respond_to?(:fork) + assert_normal_exit(<<-"end;", '[ruby-core:100329] [Bug #17220]') + require "socket" + Socket.getaddrinfo("localhost", nil) + pid = fork { Socket.getaddrinfo("localhost", nil) } + assert_equal pid, Timeout.timeout(30) { Process.wait(pid) } + end; + end end