Skip to content

Commit b1c089c

Browse files
committed
Merge pull request brianmario#498 from sodabrew/close-race-fix
avoid redundant close from calling mysql_close
2 parents fe628cc + 6206d2f commit b1c089c

File tree

1 file changed

+61
-11
lines changed

1 file changed

+61
-11
lines changed

ext/mysql2/client.c

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
#include <errno.h>
44
#ifndef _WIN32
5+
#include <sys/types.h>
56
#include <sys/socket.h>
67
#endif
78
#include <unistd.h>
9+
#include <fcntl.h>
810
#include "wait_for_single_fd.h"
911

1012
#include "mysql_enc_name_to_ruby.h"
@@ -162,24 +164,69 @@ static void *nogvl_connect(void *ptr) {
162164
return (void *)(client ? Qtrue : Qfalse);
163165
}
164166

167+
#ifndef _WIN32
168+
/*
169+
* Redirect clientfd to a dummy socket for mysql_close to
170+
* write, shutdown, and close on as a no-op.
171+
* We do this hack because we want to call mysql_close to release
172+
* memory, but do not want mysql_close to drop connections in the
173+
* parent if the socket got shared in fork.
174+
* Returns Qtrue or Qfalse (success or failure)
175+
*/
176+
static VALUE invalidate_fd(int clientfd)
177+
{
178+
#ifdef SOCK_CLOEXEC
179+
/* Atomically set CLOEXEC on the new FD in case another thread forks */
180+
int sockfd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
181+
if (sockfd < 0) {
182+
/* Maybe SOCK_CLOEXEC is defined but not available on this kernel */
183+
int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
184+
fcntl(sockfd, F_SETFD, FD_CLOEXEC);
185+
}
186+
#else
187+
/* Well we don't have SOCK_CLOEXEC, so just set FD_CLOEXEC quickly */
188+
int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
189+
fcntl(sockfd, F_SETFD, FD_CLOEXEC);
190+
#endif
191+
192+
if (sockfd < 0) {
193+
/*
194+
* Cannot raise here, because one or both of the following may be true:
195+
* a) we have no GVL (in C Ruby)
196+
* b) are running as a GC finalizer
197+
*/
198+
return Qfalse;
199+
}
200+
201+
dup2(sockfd, clientfd);
202+
close(sockfd);
203+
204+
return Qtrue;
205+
}
206+
#endif /* _WIN32 */
207+
165208
static void *nogvl_close(void *ptr) {
166209
mysql_client_wrapper *wrapper;
167210
wrapper = ptr;
168211
if (wrapper->connected) {
169212
wrapper->active_thread = Qnil;
170213
wrapper->connected = 0;
171214
#ifndef _WIN32
172-
/* Call close() on the socket before calling mysql_close(). This prevents
215+
/* Invalidate the socket before calling mysql_close(). This prevents
173216
* mysql_close() from sending a mysql-QUIT or from calling shutdown() on
174-
* the socket. The difference is that close() will drop this process's
175-
* reference to the socket only, while a QUIT or shutdown() would render
176-
* the underlying connection unusable, interrupting other processes which
177-
* share this object across a fork().
217+
* the socket. The difference is that invalidate_fd will drop this
218+
* process's reference to the socket only, while a QUIT or shutdown()
219+
* would render the underlying connection unusable, interrupting other
220+
* processes which share this object across a fork().
178221
*/
179-
close(wrapper->client->net.fd);
222+
if (invalidate_fd(wrapper->client->net.fd) == Qfalse) {
223+
fprintf(stderr, "[WARN] mysql2 failed to invalidate FD safely, leaking some memory\n");
224+
close(wrapper->client->net.fd);
225+
return NULL;
226+
}
180227
#endif
181228

182-
mysql_close(wrapper->client);
229+
mysql_close(wrapper->client); /* only used to free memory at this point */
183230
}
184231

185232
return NULL;
@@ -442,10 +489,13 @@ static VALUE disconnect_and_raise(VALUE self, VALUE error) {
442489
wrapper->active_thread = Qnil;
443490
wrapper->connected = 0;
444491

445-
/* manually close the socket for read/write
446-
this feels dirty, but is there another way? */
447-
close(wrapper->client->net.fd);
448-
wrapper->client->net.fd = -1;
492+
/* Invalidate the MySQL socket to prevent further communication.
493+
* The GC will come along later and call mysql_close to free it.
494+
*/
495+
if (invalidate_fd(wrapper->client->net.fd) == Qfalse) {
496+
fprintf(stderr, "[WARN] mysql2 failed to invalidate FD safely, closing unsafely\n");
497+
close(wrapper->client->net.fd);
498+
}
449499

450500
rb_exc_raise(error);
451501

0 commit comments

Comments
 (0)