Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions mysql-test/main/socket_conflict.result
Original file line number Diff line number Diff line change
@@ -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
#
118 changes: 118 additions & 0 deletions mysql-test/main/socket_conflict.test
Original file line number Diff line number Diff line change
@@ -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 #
56 changes: 55 additions & 1 deletion sql/mysqld.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
Loading