diff --git a/libmysqld/launcher/CMakeLists.txt b/libmysqld/launcher/CMakeLists.txt index ab68258666e..52228a0b3ce 100644 --- a/libmysqld/launcher/CMakeLists.txt +++ b/libmysqld/launcher/CMakeLists.txt @@ -47,4 +47,6 @@ IF(TARGET mariadb_obj AND TARGET mariadbclient) # An unmodified client program: only the client API, linked with libmariadbd ADD_EXECUTABLE(embedded_app_test EXCLUDE_FROM_ALL embedded_app_test.c) TARGET_LINK_LIBRARIES(embedded_app_test libmariadbd) + ADD_EXECUTABLE(embedded_auth_test EXCLUDE_FROM_ALL embedded_auth_test.c) + TARGET_LINK_LIBRARIES(embedded_auth_test libmariadbd) ENDIF() diff --git a/libmysqld/launcher/embedded_auth_test.c b/libmysqld/launcher/embedded_auth_test.c new file mode 100644 index 00000000000..42ffbfe285b --- /dev/null +++ b/libmysqld/launcher/embedded_auth_test.c @@ -0,0 +1,113 @@ +/* Copyright (c) 2026, MariaDB plc + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; version 2 of the License. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1335 USA */ + +/* + MDEV-11111: authentication in the embedded server. The password is not + checked (the client is the process that started the server), but the + account of the user name decides the privileges; an unknown name is root. + Needs a data directory with grant tables. + Usage: embedded_auth_test [server options...] +*/ +#include +#include +#include + +static int failures; + +static void check(int ok, const char *what) +{ + printf("%s: %s\n", ok ? "ok " : "FAIL", what); + if (!ok) + failures++; +} + +static MYSQL *connect_as(const char *user, const char *pass) +{ + MYSQL *m= mysql_init(NULL); + if (!mysql_real_connect(m, NULL, user, pass, NULL, 0, NULL, 0)) + { + fprintf(stderr, " connect as %s: %s\n", user, mysql_error(m)); + mysql_close(m); + return NULL; + } + return m; +} + +static int one_string(MYSQL *m, const char *query, char *out, size_t size) +{ + MYSQL_RES *res; + MYSQL_ROW row; + if (mysql_query(m, query) || !(res= mysql_store_result(m))) + return 1; + row= mysql_fetch_row(res); + snprintf(out, size, "%s", row && row[0] ? row[0] : ""); + mysql_free_result(res); + return 0; +} + +int main(int argc, char **argv) +{ + MYSQL *m; + char buf[128]; + + if (mysql_server_init(argc, argv, NULL)) + return 1; + + m= connect_as("root", NULL); + check(m != NULL, "root connects without password"); + if (!m) + return 1; + check(!mysql_query(m, "CREATE USER IF NOT EXISTS lim@localhost " + "IDENTIFIED BY 'secret'") && + !mysql_query(m, "CREATE DATABASE IF NOT EXISTS emb_auth") && + !mysql_query(m, "CREATE TABLE IF NOT EXISTS emb_auth.t (a INT)") && + !mysql_query(m, "GRANT SELECT ON emb_auth.* TO lim@localhost"), + "GRANT and CREATE USER work (grant tables are in use)"); + mysql_close(m); + + m= connect_as("lim", "wrong password"); + check(m != NULL, "existing account connects with a wrong password"); + if (m) + { + check(!one_string(m, "SELECT CURRENT_USER()", buf, sizeof(buf)) && + !strcmp(buf, "lim@localhost"), "the session is lim@localhost"); + check(!mysql_query(m, "SELECT * FROM emb_auth.t") && + (mysql_free_result(mysql_store_result(m)), 1), + "lim may SELECT what was granted"); + check(mysql_query(m, "CREATE TABLE emb_auth.t2 (a INT)") && + mysql_errno(m) == 1142 /* ER_TABLEACCESS_DENIED_ERROR */, + "lim may not CREATE, privileges are enforced"); + + check(!mysql_change_user(m, "root", "whatever", NULL) && + !one_string(m, "SELECT CURRENT_USER()", buf, sizeof(buf)) && + !strcmp(buf, "root@localhost"), "COM_CHANGE_USER to root"); + mysql_close(m); + } + + m= connect_as("no_such_user", "x"); + check(m != NULL, "an unknown name connects"); + if (m) + { + check(!one_string(m, "SELECT CURRENT_USER()", buf, sizeof(buf)) && + !strcmp(buf, "root@localhost"), "the unknown name is root"); + mysql_query(m, "DROP USER lim@localhost"); + mysql_query(m, "DROP DATABASE emb_auth"); + mysql_close(m); + } + + mysql_server_end(); + puts(failures ? "FAIL" : "OK"); + return failures != 0; +} diff --git a/libmysqld/launcher/embedded_launcher.c b/libmysqld/launcher/embedded_launcher.c index d35faa06d97..73fdcd75bbc 100644 --- a/libmysqld/launcher/embedded_launcher.c +++ b/libmysqld/launcher/embedded_launcher.c @@ -176,12 +176,6 @@ static char **build_args(int argc, char **argv, const char *lifeline, #ifdef _WIN32 a[n++]= strdup("--enable-named-pipe"); #endif - /* Trust = whoever can reach the socket, which is only the current user: - the Unix socket lives in a 0700 directory, and the server restricts the - named pipe to its own user when started with --embedded-lifeline. - $MARIADB_EMBEDDED_GRANTS=1 turns real authentication back on. */ - if (!getenv("MARIADB_EMBEDDED_GRANTS")) - a[n++]= strdup("--skip-grant-tables"); snprintf(buf, sizeof(buf), "--embedded-lifeline=%s", lifeline); a[n++]= strdup(buf); a[n]= NULL; diff --git a/mysql-test/include/have_embedded_launcher.inc b/mysql-test/include/have_embedded_launcher.inc new file mode 100644 index 00000000000..5f9533aaeb8 --- /dev/null +++ b/mysql-test/include/have_embedded_launcher.inc @@ -0,0 +1,6 @@ +# MDEV-11111: the test needs the embedded server started by mysqltest, i.e. +# mtr --embedded-launcher +if (`SELECT '$MTR_EMBEDDED_LAUNCHER' != '1'`) +{ + --skip Needs mtr --embedded-launcher +} diff --git a/mysql-test/mariadb-test-run.pl b/mysql-test/mariadb-test-run.pl index 58a09dfdcb3..2d35ee90aa2 100755 --- a/mysql-test/mariadb-test-run.pl +++ b/mysql-test/mariadb-test-run.pl @@ -1985,8 +1985,7 @@ sub executable_setup () { if ($opt_embedded_launcher) { $ENV{MARIADB_EMBEDDED_SERVER}= find_mysqld($bindir); - # Tests use GRANT, CREATE USER etc, which --skip-grant-tables refuses - $ENV{MARIADB_EMBEDDED_GRANTS}= 1; + $ENV{MTR_EMBEDDED_LAUNCHER}= 1; } } diff --git a/mysql-test/suite/embedded/r/launcher_auth.result b/mysql-test/suite/embedded/r/launcher_auth.result new file mode 100644 index 00000000000..0f6e8388c08 --- /dev/null +++ b/mysql-test/suite/embedded/r/launcher_auth.result @@ -0,0 +1,33 @@ +# +# MDEV-11111: the embedded server does not check credentials (its only +# client is the process that has started it), but the account of the +# user name decides the privileges. An unknown name is root. +# +create user lim@localhost identified by 'secret'; +create database emb_auth; +create table emb_auth.t (a int); +grant select on emb_auth.* to lim@localhost; +# The password is wrong, and the connection is accepted +connect c_lim,localhost,lim,wrong password,; +select current_user(); +current_user() +lim@localhost +select * from emb_auth.t; +a +create table emb_auth.t2 (a int); +ERROR 42000: CREATE command denied to user 'lim'@'localhost' for table `emb_auth`.`t2` +# COM_CHANGE_USER +change_user root,whatever,; +select current_user(); +current_user() +root@localhost +disconnect c_lim; +# A name without an account is the root account +connect c_nobody,localhost,no_such_user,x,; +select current_user(); +current_user() +root@localhost +disconnect c_nobody; +connection default; +drop user lim@localhost; +drop database emb_auth; diff --git a/mysql-test/suite/embedded/t/launcher_auth.test b/mysql-test/suite/embedded/t/launcher_auth.test new file mode 100644 index 00000000000..12a4162c432 --- /dev/null +++ b/mysql-test/suite/embedded/t/launcher_auth.test @@ -0,0 +1,33 @@ +--source include/have_embedded_launcher.inc + +--echo # +--echo # MDEV-11111: the embedded server does not check credentials (its only +--echo # client is the process that has started it), but the account of the +--echo # user name decides the privileges. An unknown name is root. +--echo # + +create user lim@localhost identified by 'secret'; +create database emb_auth; +create table emb_auth.t (a int); +grant select on emb_auth.* to lim@localhost; + +--echo # The password is wrong, and the connection is accepted +connect (c_lim,localhost,lim,wrong password,); +select current_user(); +select * from emb_auth.t; +--error ER_TABLEACCESS_DENIED_ERROR +create table emb_auth.t2 (a int); + +--echo # COM_CHANGE_USER +change_user root,whatever; +select current_user(); +disconnect c_lim; + +--echo # A name without an account is the root account +connect (c_nobody,localhost,no_such_user,x,); +select current_user(); +disconnect c_nobody; + +connection default; +drop user lim@localhost; +drop database emb_auth; diff --git a/sql/mysqld.cc b/sql/mysqld.cc index 34af6cd8d11..d9e186a2714 100644 --- a/sql/mysqld.cc +++ b/sql/mysqld.cc @@ -5533,6 +5533,25 @@ static int init_server_components() us_to_ms(global_system_variables.optimizer_scan_setup_cost); } + if (opt_embedded_lifeline && !opt_noacl && !opt_bootstrap) + { + /* + Embedded server (MDEV-11111) started on an empty data directory: there + are no grant tables, and nobody to create them. Run as with + --skip-grant-tables, instead of refusing to start. + */ + char path[FN_REFLEN]; + MY_STAT stat_area; + strxnmov(path, sizeof(path) - 1, mysql_real_data_home, + "mysql/global_priv.frm", NullS); + if (!my_stat(path, &stat_area, MYF(0))) + { + sql_print_information("Embedded server: no grant tables in '%s', " + "running without them", mysql_real_data_home); + opt_noacl= 1; + } + } + /* Plugins may not be completed because system table DDLs are only run after the ddl recovery done. Therefore between the diff --git a/sql/sql_acl.cc b/sql/sql_acl.cc index 70984625879..c96230e88aa 100644 --- a/sql/sql_acl.cc +++ b/sql/sql_acl.cc @@ -15831,6 +15831,15 @@ static bool find_mpvio_user(MPVIO_EXT *mpvio) ACL_USER *user= find_user_or_anon(sctx->host, sctx->user, sctx->ip); + /* + Embedded server (MDEV-11111): the application, not a user, is connecting, + and no credentials are checked (see embedded_skip_auth()). A name without + an account is the application's own choice, so use the root account + rather than refuse the connection. + */ + if (!user && opt_embedded_lifeline) + user= find_user_or_anon(sctx->host, "root", sctx->ip); + if (user && !user->dont_accept_new_connections()) mpvio->acl_user= user->copy(mpvio->auth_info.thd->mem_root); @@ -16750,6 +16759,54 @@ static void make_ssl_info(THD *thd, LEX_CSTRING salt, char *info) #endif } +/** + Embedded server (MDEV-11111): the only client is the process that has started + the server, over a private socket or pipe, so there is nobody to + authenticate. Read the client handshake to learn the user name (the grant + tables still decide what the user may do), but do not run the plugin + dialog and do not check credentials. + + If the account could not be found, find_mpvio_user() has set make_it_fail, + and the connection is refused. +*/ +static int embedded_skip_auth(MPVIO_EXT *mpvio) +{ + if (mpvio->packets_read == 0) + { + THD *thd= mpvio->auth_info.thd; + uchar *pkt; + + /* + The same first steps as native_password_authenticate(): send the + server handshake with a scramble, which every client expects, then read + the client's reply, which tells us the user name. + */ + if (thd->scramble[SCRAMBLE_LENGTH]) + thd_create_random_password(thd, thd->scramble, SCRAMBLE_LENGTH); + if (server_mpvio_write_packet(mpvio, (uchar*) thd->scramble, + SCRAMBLE_LENGTH + 1)) + return CR_AUTH_HANDSHAKE; + + /* Without grant tables the parser reports success as a "negative" + length, with the status set; see parse_client_handshake_packet() */ + if (server_mpvio_read_packet(mpvio, &pkt) < 0 && + mpvio->status != MPVIO_EXT::SUCCESS) + return CR_ERROR; + } + if (mpvio->make_it_fail) + { + mpvio->status= MPVIO_EXT::FAILURE; + return CR_ERROR; + } + /* what server_mpvio_read_packet() does; without it the account is taken + for a proxy of someone else, in COM_CHANGE_USER where it is not called */ + if (mpvio->acl_user) + strmake_buf(mpvio->auth_info.authenticated_as, mpvio->acl_user->user.str); + mpvio->status= MPVIO_EXT::SUCCESS; + return CR_OK; +} + + static int do_auth_once(THD *thd, const LEX_CSTRING *auth_plugin_name, MPVIO_EXT *mpvio) { @@ -16760,7 +16817,14 @@ static int do_auth_once(THD *thd, const LEX_CSTRING *auth_plugin_name, mpvio->plugin= plugin; mpvio->auth_info.user_name= NULL; - if (plugin) + if (opt_embedded_lifeline) + { + /* the account's plugin is not needed, and may be not even loaded */ + res= embedded_skip_auth(mpvio); + if (plugin && unlock_plugin) + plugin_unlock(thd, plugin); + } + else if (plugin) { st_mysql_auth *info= (st_mysql_auth *) plugin_decl(plugin)->info; switch (info->interface_version >> 8) {