diff --git a/mysql-test/main/socket_conflict.result b/mysql-test/main/socket_conflict.result new file mode 100644 index 0000000000000..45f3c0e163da2 --- /dev/null +++ b/mysql-test/main/socket_conflict.result @@ -0,0 +1,19 @@ +# +# MDEV-5479: Prevent mysqld from unlinking a Unix socket +# file actively used by another process +# +# +# Test 1: Server must refuse to start when a listener is +# already present on the socket path +# +FOUND 1 /\[ERROR\] Another process is already listening on the socket file/ in socket_conflict.err +# +# Test 2: Stale socket file must be cleaned up at startup +# +# restart +SELECT 1; +1 +1 +# +# End of 13.0 tests +# diff --git a/mysql-test/main/socket_conflict.test b/mysql-test/main/socket_conflict.test new file mode 100644 index 0000000000000..02e502b1c3b68 --- /dev/null +++ b/mysql-test/main/socket_conflict.test @@ -0,0 +1,118 @@ +--source include/not_windows.inc +--source include/not_embedded.inc + +--echo # +--echo # MDEV-5479: Prevent mysqld from unlinking a Unix socket +--echo # file actively used by another process +--echo # + +# Shut down the server once; both tests run while it is down. +--source include/shutdown_mysqld.inc + +--echo # +--echo # Test 1: Server must refuse to start when a listener is +--echo # already present on the socket path +--echo # + +# Create a fake listener on the default socket path. +# The child process creates and holds the listening socket; +# the parent must not touch the socket object at all, because +# Perl's IO::Socket->close() calls shutdown(SHUT_RDWR) which +# would kill the listener in the child too. +perl; + use IO::Socket::UNIX; + my $path= $ENV{MASTER_MYSOCK}; + unlink $path if -e $path; + my $pid= fork(); + die "fork: $!" unless defined $pid; + if ($pid == 0) + { + # Detach from parent's process group and close inherited + # pipe fds. mysqltest uses popen() for perl blocks and + # reads stdout to completion -- the child must close its + # copy of the pipe so mysqltest can proceed. + setpgrp(0, 0); + open(STDIN, '<', '/dev/null'); + open(STDOUT, '>', '/dev/null'); + open(STDERR, '>', '/dev/null'); + my $srv= IO::Socket::UNIX->new( + Type => SOCK_STREAM, + Local => $path, + Listen => 1, + ) or exit 1; + while (1) { sleep 60; } + } + # Parent: wait for child to create the socket (up to 60s) + for (1..600) + { + last if -S $path; + select(undef, undef, undef, 0.1); + } + die "Fake listener not created at $path\n" unless -S $path; + my $pidfile= "$ENV{MYSQLTEST_VARDIR}/tmp/fake_server.pid"; + open(my $fh, '>', $pidfile) or die "Cannot write $pidfile: $!\n"; + print $fh $pid; + close $fh; +EOF + +--let errorlog=$MYSQL_TMP_DIR/socket_conflict.err +--let SEARCH_FILE=$errorlog + +# Use --loose-skip-innodb to skip InnoDB initialization, which +# runs before network_init() and would otherwise be slow on CI. +--error 1 +--exec $MYSQLD --defaults-group-suffix=.1 --defaults-file=$MYSQLTEST_VARDIR/my.cnf --loose-skip-innodb --log-error=$errorlog + +--let SEARCH_PATTERN=\[ERROR\] Another process is already listening on the socket file +--source include/search_pattern_in_file.inc + +--remove_file $SEARCH_FILE + +# Kill the fake listener and clean up its socket file +perl; + my $pidfile= "$ENV{MYSQLTEST_VARDIR}/tmp/fake_server.pid"; + open(my $fh, '<', $pidfile) or die "Cannot read $pidfile: $!\n"; + my $pid= <$fh>; + chomp $pid; + close $fh; + kill 'TERM', $pid; + # The child runs in its own process group (setpgrp) and was + # reparented to init, so waitpid won't work here. Poll until + # the process is gone. + for (1..600) + { + last unless kill(0, $pid); + select(undef, undef, undef, 0.1); + } + unlink $pidfile; + unlink $ENV{MASTER_MYSOCK}; +EOF + +--echo # +--echo # Test 2: Stale socket file must be cleaned up at startup +--echo # + +# Create a stale Unix socket at the default socket path. +# The socket is bound then immediately closed, leaving an +# orphaned file with no listener behind it. +perl; + use IO::Socket::UNIX; + my $path= $ENV{MASTER_MYSOCK}; + my $srv= IO::Socket::UNIX->new( + Type => SOCK_STREAM, + Local => $path, + Listen => 1, + ) or die "Cannot create socket at $path: $!\n"; + $srv->close(); +EOF + +# Start the server normally -- it should detect the stale +# socket, remove it, and bind successfully. +--source include/start_mysqld.inc + +# Verify the server is operational +SELECT 1; + +--echo # +--echo # End of 13.0 tests +--echo # diff --git a/sql/mysqld.cc b/sql/mysqld.cc index 22ed876d63eb4..fa3669244abe7 100644 --- a/sql/mysqld.cc +++ b/sql/mysqld.cc @@ -2702,6 +2702,60 @@ static void use_systemd_activated_sockets() } +#ifdef HAVE_SYS_UN_H +/* + Check if an existing Unix socket file is actively in use. + If another process is listening on the socket, abort startup. + If the socket is stale (no listener), remove it so we can + re-bind. +*/ +static void handle_stale_unix_socket(const char *path) +{ + MY_STAT stat_buf; + + if (!my_stat(path, &stat_buf, MYF(0))) + return; /* File does not exist */ + + if (S_ISSOCK(stat_buf.st_mode)) + { + MYSQL_SOCKET test_sock; + test_sock= mysql_socket_socket(key_socket_unix, + AF_UNIX, SOCK_STREAM, 0); + if (mysql_socket_getfd(test_sock) >= 0) + { + struct sockaddr_un test_addr; + bzero((char*) &test_addr, sizeof(test_addr)); + test_addr.sun_family= AF_UNIX; + strmov(test_addr.sun_path, path); + if (mysql_socket_connect(test_sock, + (struct sockaddr *) &test_addr, + sizeof(test_addr)) == 0) + { + /* Socket is active - another process is listening */ + mysql_socket_close(test_sock); + sql_print_error("Another process is already listening " + "on the socket file '%s'. " + "Aborting.", path); + unireg_abort(1); + } + if (socket_errno == EACCES) + { + mysql_socket_close(test_sock); + sql_print_error("Socket file '%s' exists and is " + "owned by another user. Cannot " + "verify or replace it. Aborting.", + path); + unireg_abort(1); + } + mysql_socket_close(test_sock); + } + } + /* File is stale or not a socket - safe to remove */ + (void) unlink(path); +} +#endif /* HAVE_SYS_UN_H */ + + static void network_init(void) { #ifdef HAVE_SYS_UN_H @@ -2779,7 +2833,7 @@ static void network_init(void) else #endif { - (void) unlink(mysqld_unix_port); + handle_stale_unix_socket(mysqld_unix_port); port_len= sizeof(UNIXaddr); } arg= 1;