Description:
###################
## ISSUE
###################
The underlying functions used by GRANT operations against global privileges (*.*) are not safeguarded against sql_mode PAD_CHAR_TO_FULL_LENGTH. When a session running in PAD_CHAR_TO_FULL_LENGTH invokes GRANT/REVOKE ON *.* against a user, the user's authentication plugin field becomes corrupted in the ACL cache. Subsequently, the user will be unable to connect until:
- Privileges are flushed OR
- The server is restarted OR
- Another session runs GRANT/REVOKE against the same user again, this time without the PAD_CHAR_TO_FULL_LENGTH sql_mode.
This behavior is reported and was tested in the context of native_mysql_password authentication only. I did not test with other authentication methods.
###################
## AFFECTED VERSIONS
###################
- 5.6: issue exists and can break user logins
- 5.7: issue exists but doesn't break user logins due to differences in login code
See research below for more information.
###################
## RESEARCH
###################
------ 1. GRANT/REVOKE calls replace_user_table to update ACL data
Breakpoint 1, replace_user_table (thd=thd@entry=0x29ae420, table=0x1a0392a0, combo=combo@entry=0x1a1510d0, rights=rights@entry=3, revoke_grant=revoke_grant@entry=false,
    can_create_user=true, no_auto_create=false) at /home/mysql/source/mysql-5.6.27/sql/sql_acl.cc:2795
2795   	{
(gdb) bt
#0  replace_user_table (thd=thd@entry=0x29ae420, table=0x1a0392a0, combo=combo@entry=0x1a1510d0, rights=rights@entry=3, revoke_grant=revoke_grant@entry=false, can_create_user=true,
    no_auto_create=false) at /home/mysql/source/mysql-5.6.27/sql/sql_acl.cc:2795
#1  0x00000000006a66cc in mysql_grant (thd=<optimized out>, db=<optimized out>, list=..., rights=3, revoke_grant=false, is_proxy=false)
    at /home/mysql/source/mysql-5.6.27/sql/sql_acl.cc:5109
------ 2. Replace_user_table gets plugin information from cached user data:
Breakpoint 2, replace_user_table (thd=thd@entry=0x29ae420, table=0x1a0392a0, combo=combo@entry=0x1a1510d0, rights=rights@entry=3, revoke_grant=revoke_grant@entry=false,
    can_create_user=true, no_auto_create=false) at /home/mysql/source/mysql-5.6.27/sql/sql_acl.cc:2972
2972   	      get_field(thd->mem_root, table->field[MYSQL_USER_FIELD_PLUGIN]);
(gdb) l
2967   	    /*
2968   	      Get old plugin value from storage.
2969   	    */
2970
2971   	    old_plugin.str=
2972   	      get_field(thd->mem_root, table->field[MYSQL_USER_FIELD_PLUGIN]);
------ 3. Get_field gets the string value of the plugin through Field_string::val_string, which pads it with spaces:
Field_string::val_str (this=0x1a03b020, val_buffer=0x7f57e56bdfd0, val_ptr=0x7f57e56bdfd0) at /home/mysql/source/mysql-5.6.27/sql/field.cc:6797
6797   	  if (table->in_use->variables.sql_mode &
6798   	      MODE_PAD_CHAR_TO_FULL_LENGTH)
6799   	    length= my_charpos(field_charset, ptr, ptr + field_length,
6800   	                       field_length / field_charset->mbmaxlen);
6801   	  else
6802   	    length= field_charset->cset->lengthsp(field_charset, (const char*) ptr,
6803   	                                          field_length);
###################
## 5.6 vs 5.7
###################
---- See notes inline marked with "<<---"
int
acl_authenticate(THD *thd, uint com_change_user_pkt_len)
{
  int res= CR_OK;
  MPVIO_EXT mpvio;
  Thd_charset_adapter charset_adapter(thd);
  LEX_STRING auth_plugin_name= default_auth_plugin_name;
  ...
  if (command == COM_CHANGE_USER)
  {
    ...
  }
  else
  {
    ...
    <<!!!--- at this point "auth_plugin_name" is still OK ('mysql_native_password')
    res= do_auth_once(thd, &auth_plugin_name, &mpvio);
    <<!!!--- at this point mpvio.status is MPVIO_EXT::RESTART
  }
...
  /*
   retry the authentication, if - after receiving the user name -
   we found that we need to switch to a non-default plugin
  */
  if (mpvio.status == MPVIO_EXT::RESTART) <<!!!--- 5.6 enters here, but not 5.7 (in 5.7 mpvio.status is MPVIO_EXT::FAILURE)
  {
    DBUG_ASSERT(mpvio.acl_user);
    DBUG_ASSERT(command == COM_CHANGE_USER ||
                my_strcasecmp(system_charset_info, auth_plugin_name.str,
                              mpvio.acl_user->plugin.str));
    auth_plugin_name= mpvio.acl_user->plugin; <<!!!--- This is where the plugin string gets overwritten with incorrect information (padded with spaces) from ACL cache
    res= do_auth_once(thd, &auth_plugin_name, &mpvio);
How to repeat:
###################
## REPRO
###################
Note that this is a simplified repro where a user grants a privilege to himself and therefore breaks his own login ability. The repro is simplified for brevity, but this also reproduces in general case when granting/revoking privileges against arbitrary users.
root@sandbox:/home/admin# mysql -h127.0.0.1 -P5627 -uroot -p{...}
mysql> set sql_mode = 'PAD_CHAR_TO_FULL_LENGTH';
Query OK, 0 rows affected (0.00 sec)
mysql> select current_user();
+----------------+
| current_user() |
+----------------+
| root@localhost |
+----------------+
1 row in set (0.00 sec)
mysql> grant insert on *.* to 'root'@'localhost';
Query OK, 0 rows affected (0.00 sec)
mysql> \r
ERROR 1524 (HY000): Plugin 'mysql_native_password                                           ' is not loaded
Suggested fix:
###################
## SUGGESTED FIX
###################
The PAD_CHAR_TO_FULL_LENGTH mode is already being disabled in multiple places in MySQL code for the duration of privilege-related actions.
For example, this is from mysql_drop_user:
------------
bool mysql_drop_user(THD *thd, List <LEX_USER> &list)
{
  int result;
  String wrong_users;
  LEX_USER *user_name, *tmp_user_name;
  List_iterator <LEX_USER> user_list(list);
  TABLE_LIST tables[GRANT_TABLES];
  bool some_users_deleted= FALSE;
  sql_mode_t old_sql_mode= thd->variables.sql_mode;
...
  thd->variables.sql_mode&= ~MODE_PAD_CHAR_TO_FULL_LENGTH;
... (function body) ...
  thd->variables.sql_mode= old_sql_mode;
  DBUG_RETURN(result);
}
------------
Would suggest similar approach here.
  
 
 
Description: ################### ## ISSUE ################### The underlying functions used by GRANT operations against global privileges (*.*) are not safeguarded against sql_mode PAD_CHAR_TO_FULL_LENGTH. When a session running in PAD_CHAR_TO_FULL_LENGTH invokes GRANT/REVOKE ON *.* against a user, the user's authentication plugin field becomes corrupted in the ACL cache. Subsequently, the user will be unable to connect until: - Privileges are flushed OR - The server is restarted OR - Another session runs GRANT/REVOKE against the same user again, this time without the PAD_CHAR_TO_FULL_LENGTH sql_mode. This behavior is reported and was tested in the context of native_mysql_password authentication only. I did not test with other authentication methods. ################### ## AFFECTED VERSIONS ################### - 5.6: issue exists and can break user logins - 5.7: issue exists but doesn't break user logins due to differences in login code See research below for more information. ################### ## RESEARCH ################### ------ 1. GRANT/REVOKE calls replace_user_table to update ACL data Breakpoint 1, replace_user_table (thd=thd@entry=0x29ae420, table=0x1a0392a0, combo=combo@entry=0x1a1510d0, rights=rights@entry=3, revoke_grant=revoke_grant@entry=false, can_create_user=true, no_auto_create=false) at /home/mysql/source/mysql-5.6.27/sql/sql_acl.cc:2795 2795 { (gdb) bt #0 replace_user_table (thd=thd@entry=0x29ae420, table=0x1a0392a0, combo=combo@entry=0x1a1510d0, rights=rights@entry=3, revoke_grant=revoke_grant@entry=false, can_create_user=true, no_auto_create=false) at /home/mysql/source/mysql-5.6.27/sql/sql_acl.cc:2795 #1 0x00000000006a66cc in mysql_grant (thd=<optimized out>, db=<optimized out>, list=..., rights=3, revoke_grant=false, is_proxy=false) at /home/mysql/source/mysql-5.6.27/sql/sql_acl.cc:5109 ------ 2. Replace_user_table gets plugin information from cached user data: Breakpoint 2, replace_user_table (thd=thd@entry=0x29ae420, table=0x1a0392a0, combo=combo@entry=0x1a1510d0, rights=rights@entry=3, revoke_grant=revoke_grant@entry=false, can_create_user=true, no_auto_create=false) at /home/mysql/source/mysql-5.6.27/sql/sql_acl.cc:2972 2972 get_field(thd->mem_root, table->field[MYSQL_USER_FIELD_PLUGIN]); (gdb) l 2967 /* 2968 Get old plugin value from storage. 2969 */ 2970 2971 old_plugin.str= 2972 get_field(thd->mem_root, table->field[MYSQL_USER_FIELD_PLUGIN]); ------ 3. Get_field gets the string value of the plugin through Field_string::val_string, which pads it with spaces: Field_string::val_str (this=0x1a03b020, val_buffer=0x7f57e56bdfd0, val_ptr=0x7f57e56bdfd0) at /home/mysql/source/mysql-5.6.27/sql/field.cc:6797 6797 if (table->in_use->variables.sql_mode & 6798 MODE_PAD_CHAR_TO_FULL_LENGTH) 6799 length= my_charpos(field_charset, ptr, ptr + field_length, 6800 field_length / field_charset->mbmaxlen); 6801 else 6802 length= field_charset->cset->lengthsp(field_charset, (const char*) ptr, 6803 field_length); ################### ## 5.6 vs 5.7 ################### ---- See notes inline marked with "<<---" int acl_authenticate(THD *thd, uint com_change_user_pkt_len) { int res= CR_OK; MPVIO_EXT mpvio; Thd_charset_adapter charset_adapter(thd); LEX_STRING auth_plugin_name= default_auth_plugin_name; ... if (command == COM_CHANGE_USER) { ... } else { ... <<!!!--- at this point "auth_plugin_name" is still OK ('mysql_native_password') res= do_auth_once(thd, &auth_plugin_name, &mpvio); <<!!!--- at this point mpvio.status is MPVIO_EXT::RESTART } ... /* retry the authentication, if - after receiving the user name - we found that we need to switch to a non-default plugin */ if (mpvio.status == MPVIO_EXT::RESTART) <<!!!--- 5.6 enters here, but not 5.7 (in 5.7 mpvio.status is MPVIO_EXT::FAILURE) { DBUG_ASSERT(mpvio.acl_user); DBUG_ASSERT(command == COM_CHANGE_USER || my_strcasecmp(system_charset_info, auth_plugin_name.str, mpvio.acl_user->plugin.str)); auth_plugin_name= mpvio.acl_user->plugin; <<!!!--- This is where the plugin string gets overwritten with incorrect information (padded with spaces) from ACL cache res= do_auth_once(thd, &auth_plugin_name, &mpvio); How to repeat: ################### ## REPRO ################### Note that this is a simplified repro where a user grants a privilege to himself and therefore breaks his own login ability. The repro is simplified for brevity, but this also reproduces in general case when granting/revoking privileges against arbitrary users. root@sandbox:/home/admin# mysql -h127.0.0.1 -P5627 -uroot -p{...} mysql> set sql_mode = 'PAD_CHAR_TO_FULL_LENGTH'; Query OK, 0 rows affected (0.00 sec) mysql> select current_user(); +----------------+ | current_user() | +----------------+ | root@localhost | +----------------+ 1 row in set (0.00 sec) mysql> grant insert on *.* to 'root'@'localhost'; Query OK, 0 rows affected (0.00 sec) mysql> \r ERROR 1524 (HY000): Plugin 'mysql_native_password ' is not loaded Suggested fix: ################### ## SUGGESTED FIX ################### The PAD_CHAR_TO_FULL_LENGTH mode is already being disabled in multiple places in MySQL code for the duration of privilege-related actions. For example, this is from mysql_drop_user: ------------ bool mysql_drop_user(THD *thd, List <LEX_USER> &list) { int result; String wrong_users; LEX_USER *user_name, *tmp_user_name; List_iterator <LEX_USER> user_list(list); TABLE_LIST tables[GRANT_TABLES]; bool some_users_deleted= FALSE; sql_mode_t old_sql_mode= thd->variables.sql_mode; ... thd->variables.sql_mode&= ~MODE_PAD_CHAR_TO_FULL_LENGTH; ... (function body) ... thd->variables.sql_mode= old_sql_mode; DBUG_RETURN(result); } ------------ Would suggest similar approach here.