|
2 | 2 |
|
3 | 3 | #include <errno.h>
|
4 | 4 | #ifndef _WIN32
|
| 5 | +#include <sys/types.h> |
5 | 6 | #include <sys/socket.h>
|
6 | 7 | #endif
|
7 | 8 | #include <unistd.h>
|
| 9 | +#include <fcntl.h> |
8 | 10 | #include "wait_for_single_fd.h"
|
9 | 11 |
|
10 | 12 | #include "mysql_enc_name_to_ruby.h"
|
@@ -162,24 +164,69 @@ static void *nogvl_connect(void *ptr) {
|
162 | 164 | return (void *)(client ? Qtrue : Qfalse);
|
163 | 165 | }
|
164 | 166 |
|
| 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 | + |
165 | 208 | static void *nogvl_close(void *ptr) {
|
166 | 209 | mysql_client_wrapper *wrapper;
|
167 | 210 | wrapper = ptr;
|
168 | 211 | if (wrapper->connected) {
|
169 | 212 | wrapper->active_thread = Qnil;
|
170 | 213 | wrapper->connected = 0;
|
171 | 214 | #ifndef _WIN32
|
172 |
| - /* Call close() on the socket before calling mysql_close(). This prevents |
| 215 | + /* Invalidate the socket before calling mysql_close(). This prevents |
173 | 216 | * 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(). |
178 | 221 | */
|
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 | + } |
180 | 227 | #endif
|
181 | 228 |
|
182 |
| - mysql_close(wrapper->client); |
| 229 | + mysql_close(wrapper->client); /* only used to free memory at this point */ |
183 | 230 | }
|
184 | 231 |
|
185 | 232 | return NULL;
|
@@ -442,10 +489,13 @@ static VALUE disconnect_and_raise(VALUE self, VALUE error) {
|
442 | 489 | wrapper->active_thread = Qnil;
|
443 | 490 | wrapper->connected = 0;
|
444 | 491 |
|
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 | + } |
449 | 499 |
|
450 | 500 | rb_exc_raise(error);
|
451 | 501 |
|
|
0 commit comments