Skip to content

Commit d9ec59e

Browse files
FaramosCZclaude
andcommitted
MDEV-5479 Prevent stealing Unix socket of running instance
When two MariaDB server instances are configured with the same Unix socket path but different TCP ports, the second instance unconditionally unlink()s the first instance's active socket file in network_init(). This silently breaks the first instance's ability to accept local connections. Root cause: the call (void) unlink(mysqld_unix_port) in network_init() removes an existing socket file without checking whether another server is actively listening on it. Notably, the bind() error handler immediately following the unlink already warns about another running server -- but this message could never trigger because the unconditional unlink() removed the socket before bind() had a chance to fail. Fix: before unlinking an existing socket file, attempt to connect() to it: - If connect() succeeds, another server is actively using the socket. Abort startup with an error message. - If connect() fails with EACCES, the socket is active but owned by another user. Abort -- we must not unlink a socket we cannot even verify. - If connect() fails with ECONNREFUSED or similar, the socket is stale from a previous unclean shutdown. Proceed with unlink() as before. - If the file exists but is not a socket (S_ISSOCK check), remove it to preserve previous behavior. connect() was chosen over flock()-based advisory locking because flock() requires managing a separate lock file alongside the socket (creation, cleanup, and handling of orphaned lock files), and does not work reliably on network filesystems. connect() probes the socket directly with no extra files and no NFS dependency. This is the same approach PostgreSQL uses for socket conflict prevention. A recv()-with-timeout variant was also considered but rejected: it adds a multi-second startup delay when a hung server is detected, and misclassifies a hung server as stale (proceeds to steal the socket instead of aborting). Co-Authored-By: Claude AI <noreply@anthropic.com>
1 parent 6660d0b commit d9ec59e

3 files changed

Lines changed: 176 additions & 1 deletion

File tree

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#
2+
# MDEV-5479: Prevent mysqld from stealing Unix socket file
3+
# of another active instance
4+
#
5+
#
6+
# Test 1: Server must refuse to start when a listener is
7+
# already present on the socket path
8+
#
9+
FOUND 1 /\[ERROR\] Another server process is already using the socket file/ in socket_conflict.err
10+
#
11+
# Test 2: Stale socket file must be cleaned up at startup
12+
#
13+
# restart
14+
SELECT 1;
15+
1
16+
1
17+
#
18+
# End of 13.0 tests
19+
#
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
--source include/not_windows.inc
2+
--source include/not_embedded.inc
3+
4+
--echo #
5+
--echo # MDEV-5479: Prevent mysqld from stealing Unix socket file
6+
--echo # of another active instance
7+
--echo #
8+
9+
# Shut down the server once; both tests run while it is down.
10+
--source include/shutdown_mysqld.inc
11+
12+
--echo #
13+
--echo # Test 1: Server must refuse to start when a listener is
14+
--echo # already present on the socket path
15+
--echo #
16+
17+
# Create a fake listener on the default socket path.
18+
# The child process creates and holds the listening socket;
19+
# the parent must not touch the socket object at all, because
20+
# Perl's IO::Socket->close() calls shutdown(SHUT_RDWR) which
21+
# would kill the listener in the child too.
22+
perl;
23+
use IO::Socket::UNIX;
24+
my $path= $ENV{MASTER_MYSOCK};
25+
unlink $path if -e $path;
26+
my $pid= fork();
27+
die "fork: $!" unless defined $pid;
28+
if ($pid == 0)
29+
{
30+
# Detach from parent's process group and close inherited
31+
# pipe fds. mysqltest uses popen() for perl blocks and
32+
# reads stdout to completion -- the child must close its
33+
# copy of the pipe so mysqltest can proceed.
34+
setpgrp(0, 0);
35+
open(STDIN, '<', '/dev/null');
36+
open(STDOUT, '>', '/dev/null');
37+
open(STDERR, '>', '/dev/null');
38+
my $srv= IO::Socket::UNIX->new(
39+
Type => SOCK_STREAM,
40+
Local => $path,
41+
Listen => 1,
42+
) or exit 1;
43+
sleep 120;
44+
exit 0;
45+
}
46+
# Parent: wait for child to create the socket (up to 5s)
47+
for (1..50)
48+
{
49+
last if -S $path;
50+
select(undef, undef, undef, 0.1);
51+
}
52+
die "Fake listener not created at $path\n" unless -S $path;
53+
my $pidfile= "$ENV{MYSQLTEST_VARDIR}/tmp/fake_server.pid";
54+
open(my $fh, '>', $pidfile) or die "Cannot write $pidfile: $!\n";
55+
print $fh $pid;
56+
close $fh;
57+
EOF
58+
59+
--let errorlog=$MYSQL_TMP_DIR/socket_conflict.err
60+
--let SEARCH_FILE=$errorlog
61+
62+
# Use --innodb=OFF to skip InnoDB initialization, which runs
63+
# before network_init() and would otherwise take minutes on CI.
64+
--error 1
65+
--exec $MYSQLD --defaults-group-suffix=.1 --defaults-file=$MYSQLTEST_VARDIR/my.cnf --innodb=OFF --log-error=$errorlog
66+
67+
--let SEARCH_PATTERN=\[ERROR\] Another server process is already using the socket file
68+
--source include/search_pattern_in_file.inc
69+
70+
--remove_file $SEARCH_FILE
71+
72+
# Kill the fake listener and clean up its socket file
73+
perl;
74+
my $pidfile= "$ENV{MYSQLTEST_VARDIR}/tmp/fake_server.pid";
75+
open(my $fh, '<', $pidfile) or die "Cannot read $pidfile: $!\n";
76+
my $pid= <$fh>;
77+
chomp $pid;
78+
close $fh;
79+
kill 'TERM', $pid;
80+
# The child runs in its own process group (setpgrp) and was
81+
# reparented to init, so waitpid won't work here. Poll until
82+
# the process is gone.
83+
for (1..50)
84+
{
85+
last unless kill(0, $pid);
86+
select(undef, undef, undef, 0.1);
87+
}
88+
unlink $pidfile;
89+
unlink $ENV{MASTER_MYSOCK};
90+
EOF
91+
92+
--echo #
93+
--echo # Test 2: Stale socket file must be cleaned up at startup
94+
--echo #
95+
96+
# Create a stale Unix socket at the default socket path.
97+
# The socket is bound then immediately closed, leaving an
98+
# orphaned file with no listener behind it.
99+
perl;
100+
use IO::Socket::UNIX;
101+
my $path= $ENV{MASTER_MYSOCK};
102+
my $srv= IO::Socket::UNIX->new(
103+
Type => SOCK_STREAM,
104+
Local => $path,
105+
Listen => 1,
106+
) or die "Cannot create socket at $path: $!\n";
107+
$srv->close();
108+
EOF
109+
110+
# Start the server normally -- it should detect the stale
111+
# socket, remove it, and bind successfully.
112+
--source include/start_mysqld.inc
113+
114+
# Verify the server is operational
115+
SELECT 1;
116+
117+
--echo #
118+
--echo # End of 13.0 tests
119+
--echo #

sql/mysqld.cc

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2779,7 +2779,44 @@ static void network_init(void)
27792779
else
27802780
#endif
27812781
{
2782-
(void) unlink(mysqld_unix_port);
2782+
struct stat statbuf;
2783+
if (stat(mysqld_unix_port, &statbuf) == 0)
2784+
{
2785+
/* Socket file exists - check if another server is using it */
2786+
if (S_ISSOCK(statbuf.st_mode))
2787+
{
2788+
int test_fd= socket(AF_UNIX, SOCK_STREAM, 0);
2789+
if (test_fd >= 0)
2790+
{
2791+
struct sockaddr_un test_addr;
2792+
bzero((char*) &test_addr, sizeof(test_addr));
2793+
test_addr.sun_family= AF_UNIX;
2794+
strmov(test_addr.sun_path, mysqld_unix_port);
2795+
if (connect(test_fd, (struct sockaddr *) &test_addr,
2796+
sizeof(test_addr)) == 0)
2797+
{
2798+
/* Socket is active - another server is listening */
2799+
close(test_fd);
2800+
sql_print_error("Another server process is already "
2801+
"using the socket file '%s'. "
2802+
"Aborting.", mysqld_unix_port);
2803+
unireg_abort(1);
2804+
}
2805+
if (socket_errno == EACCES)
2806+
{
2807+
close(test_fd);
2808+
sql_print_error("Socket file '%s' exists and is "
2809+
"owned by another user. Cannot "
2810+
"verify or replace it. Aborting.",
2811+
mysqld_unix_port);
2812+
unireg_abort(1);
2813+
}
2814+
close(test_fd);
2815+
}
2816+
}
2817+
/* Socket file is stale or not a socket - safe to remove */
2818+
(void) unlink(mysqld_unix_port);
2819+
}
27832820
port_len= sizeof(UNIXaddr);
27842821
}
27852822
arg= 1;

0 commit comments

Comments
 (0)