Skip to content

Commit 2548b9c

Browse files
FaramosCZclaude
andcommitted
MDEV-5479 Prevent stealing Unix socket of running instance
When mysqld starts up and the configured Unix socket path is already in use by another process, network_init() unconditionally unlink()s the active socket file. This silently breaks the other process's ability to accept local connections. The typical scenario is two MariaDB instances configured with the same socket path but different TCP ports, but any process listening on that path is affected. Root cause: the call (void) unlink(mysqld_unix_port) in network_init() removes an existing socket file without checking whether another process 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: add handle_stale_unix_socket() that, before unlinking an existing socket file, attempts to connect() to it: - If connect() succeeds, another process 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. Co-Authored-By: Claude AI <noreply@anthropic.com>
1 parent 6660d0b commit 2548b9c

3 files changed

Lines changed: 192 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 unlinking a Unix socket
3+
# file actively used by another process
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 process is already listening on 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: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
--source include/not_windows.inc
2+
--source include/not_embedded.inc
3+
4+
--echo #
5+
--echo # MDEV-5479: Prevent mysqld from unlinking a Unix socket
6+
--echo # file actively used by another process
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+
while (1) { sleep 60; }
44+
}
45+
# Parent: wait for child to create the socket (up to 60s)
46+
for (1..600)
47+
{
48+
last if -S $path;
49+
select(undef, undef, undef, 0.1);
50+
}
51+
die "Fake listener not created at $path\n" unless -S $path;
52+
my $pidfile= "$ENV{MYSQLTEST_VARDIR}/tmp/fake_server.pid";
53+
open(my $fh, '>', $pidfile) or die "Cannot write $pidfile: $!\n";
54+
print $fh $pid;
55+
close $fh;
56+
EOF
57+
58+
--let errorlog=$MYSQL_TMP_DIR/socket_conflict.err
59+
--let SEARCH_FILE=$errorlog
60+
61+
# Use --loose-skip-innodb to skip InnoDB initialization, which
62+
# runs before network_init() and would otherwise be slow on CI.
63+
--error 1
64+
--exec $MYSQLD --defaults-group-suffix=.1 --defaults-file=$MYSQLTEST_VARDIR/my.cnf --loose-skip-innodb --log-error=$errorlog
65+
66+
--let SEARCH_PATTERN=\[ERROR\] Another process is already listening on the socket file
67+
--source include/search_pattern_in_file.inc
68+
69+
--remove_file $SEARCH_FILE
70+
71+
# Kill the fake listener and clean up its socket file
72+
perl;
73+
my $pidfile= "$ENV{MYSQLTEST_VARDIR}/tmp/fake_server.pid";
74+
open(my $fh, '<', $pidfile) or die "Cannot read $pidfile: $!\n";
75+
my $pid= <$fh>;
76+
chomp $pid;
77+
close $fh;
78+
kill 'TERM', $pid;
79+
# The child runs in its own process group (setpgrp) and was
80+
# reparented to init, so waitpid won't work here. Poll until
81+
# the process is gone.
82+
for (1..600)
83+
{
84+
last unless kill(0, $pid);
85+
select(undef, undef, undef, 0.1);
86+
}
87+
unlink $pidfile;
88+
unlink $ENV{MASTER_MYSOCK};
89+
EOF
90+
91+
--echo #
92+
--echo # Test 2: Stale socket file must be cleaned up at startup
93+
--echo #
94+
95+
# Create a stale Unix socket at the default socket path.
96+
# The socket is bound then immediately closed, leaving an
97+
# orphaned file with no listener behind it.
98+
perl;
99+
use IO::Socket::UNIX;
100+
my $path= $ENV{MASTER_MYSOCK};
101+
my $srv= IO::Socket::UNIX->new(
102+
Type => SOCK_STREAM,
103+
Local => $path,
104+
Listen => 1,
105+
) or die "Cannot create socket at $path: $!\n";
106+
$srv->close();
107+
EOF
108+
109+
# Start the server normally -- it should detect the stale
110+
# socket, remove it, and bind successfully.
111+
--source include/start_mysqld.inc
112+
113+
# Verify the server is operational
114+
SELECT 1;
115+
116+
--echo #
117+
--echo # End of 13.0 tests
118+
--echo #

sql/mysqld.cc

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2702,6 +2702,60 @@ static void use_systemd_activated_sockets()
27022702
}
27032703

27042704

2705+
#ifdef HAVE_SYS_UN_H
2706+
/*
2707+
Check if an existing Unix socket file is actively in use.
2708+
If another process is listening on the socket, abort startup.
2709+
If the socket is stale (no listener), remove it so we can
2710+
re-bind.
2711+
*/
2712+
static void handle_stale_unix_socket(const char *path)
2713+
{
2714+
MY_STAT stat_buf;
2715+
2716+
if (!my_stat(path, &stat_buf, MYF(0)))
2717+
return; /* File does not exist */
2718+
2719+
if (S_ISSOCK(stat_buf.st_mode))
2720+
{
2721+
MYSQL_SOCKET test_sock;
2722+
test_sock= mysql_socket_socket(key_socket_unix,
2723+
AF_UNIX, SOCK_STREAM, 0);
2724+
if (mysql_socket_getfd(test_sock) >= 0)
2725+
{
2726+
struct sockaddr_un test_addr;
2727+
bzero((char*) &test_addr, sizeof(test_addr));
2728+
test_addr.sun_family= AF_UNIX;
2729+
strmov(test_addr.sun_path, path);
2730+
if (mysql_socket_connect(test_sock,
2731+
(struct sockaddr *) &test_addr,
2732+
sizeof(test_addr)) == 0)
2733+
{
2734+
/* Socket is active - another process is listening */
2735+
mysql_socket_close(test_sock);
2736+
sql_print_error("Another process is already listening "
2737+
"on the socket file '%s'. "
2738+
"Aborting.", path);
2739+
unireg_abort(1);
2740+
}
2741+
if (socket_errno == EACCES)
2742+
{
2743+
mysql_socket_close(test_sock);
2744+
sql_print_error("Socket file '%s' exists and is "
2745+
"owned by another user. Cannot "
2746+
"verify or replace it. Aborting.",
2747+
path);
2748+
unireg_abort(1);
2749+
}
2750+
mysql_socket_close(test_sock);
2751+
}
2752+
}
2753+
/* File is stale or not a socket - safe to remove */
2754+
(void) unlink(path);
2755+
}
2756+
#endif /* HAVE_SYS_UN_H */
2757+
2758+
27052759
static void network_init(void)
27062760
{
27072761
#ifdef HAVE_SYS_UN_H
@@ -2779,7 +2833,7 @@ static void network_init(void)
27792833
else
27802834
#endif
27812835
{
2782-
(void) unlink(mysqld_unix_port);
2836+
handle_stale_unix_socket(mysqld_unix_port);
27832837
port_len= sizeof(UNIXaddr);
27842838
}
27852839
arg= 1;

0 commit comments

Comments
 (0)