Correction d'un Bug de Gestion des Noms de Requêtes Préparées dans MySQL

Lors d'un développement récent impliquant l'utilisation de requêtes préparées dans MySQL, un problème inattendu est survenu : une requête préparée, une fois définie avec PREPARE, devenait occasionnellement introuvable lors de l'appel à EXECUTE, malgré l'utilisation correcte de son nom. Ce comportement intermittent a déclenché une investigation approfondie pour en identifier la cause.

Voici un exemple illustratif du scénario qui provoquait l'erreur :

CREATE TABLE products (
    id INT PRIMARY KEY,
    name VARCHAR(100) NOT NULL
);

PREPARE add_product_stmt FROM 'INSERT INTO products (id, name) VALUES (10, ''Clavier'')';
EXECUTE add_product_stmt;

-- Erreur potentielle observée :
-- SQL Error [1243] [HY000]: Gestionnaire de requête préparée inconnu (add_product_stmt??...) donné à EXECUTE

Investigation du Problème

1. Analyse de l'Erreur d'Exécution

L'erreur ER_UNKNOWN_STMT_HANDLER indique clairement que le moteur MySQL ne parvient pas à localiser la requête préparée par son nom. En examinant le code source de MySQL, la fonction MySQL_sql_stmt_execute est le point d'entrée pour l'exécution. Cette fonction utilise une structure LEX_CSTRING, qui contient à la fois un pointeur vers la chaîne de caractères (str) et sa longueur (length), pour identifier la requête. Lors de l'erreur, il a été constaté que la valeur de name.str était suivie de caractères aléatoires, et plus préoccupant, la valeur de name.length était elle-même corrompue (par exemple, affichant 22 au lieu de 14 pour "add_product_stmt").

2. Examen de l'Attribution du Nom

Le nom de la requête préparée est initialement défini lorsque la commande PREPARE est traitée. La fonction membre Prepared_statement::set_name est responsable de la copie du nom fourni par l'utilisateur dans la structure interne de l'instruction préparée.

Le code source de la fonction set_name se présentait comme suit :

bool Prepared_statement::set_name(const LEX_CSTRING &input_name) {
  // Stocke la longueur du nom
  statement_name_.length = input_name.length;
  // Alloue de la mémoire et copie le nom, mais la taille est exactement 'input_name.length'
  statement_name_.str = static_cast<char *>(
      memdup_root(memory_arena_, input_name.str, input_name.length));
  return statement_name_.str == nullptr;
}

En effectuant un suivi avec un débogueur (GDB) après l'appel à set_name, il est apparu que le contenu de statement_name_.str dans la structure Prepared_statement était effectivement la chaîne attednue (ex: "add_product_stmt"), mais n'était pas suivie d'un caractère nul (\0). Le problème critique n'était pas seulement l'absence du terminateur nul en soi, mais le fait que l'allocation mémoire de input_name.length octets seulement pouvait entraîner un débordement de tampon si le code suivant tentait de lire au-delà de cette limite. Ce débordement affectait les données adjacentes en mémoire, notamment le champ length de la structure LEX_CSTRING utilisée ultérieurement par EXECUTE.

Solution Proposée

Le diagnostic a révélé que la cause principale était une allocation mémoire insuffisante lors de la copie du nom de l'instruction préparée. La fonction memdup_root copiait précisément input_name.length octets, sans allouer d'espace supplémentaire pour le caractère nul de terminaison. Dans certaines configurations mémoire, cela entraînait un débordement de tampon qui corrompait la valeur de length dans la structure LEX_CSTRING ou une structure adjacente utilisée lors de l'exécution.

La solution consiste à allouer un octet supplémnetaire pour le terminateur nul. La modification du code de la fonction set_name est la suivante :

bool Prepared_statement::set_name(const LEX_CSTRING &input_name) {
  statement_name_.length = input_name.length;
  // Correction : Allouer input_name.length + 1 octets pour inclure le terminateur nul.
  statement_name_.str = static_cast<char *>(
      memdup_root(memory_arena_, input_name.str, input_name.length + 1));

  // S'assurer que la chaîne est correctement terminée par un nul.
  if (statement_name_.str != nullptr) {
      statement_name_.str[statement_name_.length] = '\0';
  }
  return statement_name_.str == nullptr;
}

Après avoir appliqué cette modification et recompilé le code, le problème de corruption du nom des requêtes préparées a été résolu, et les instructions EXECUTE ont fonctionné de manière fiable.

Conclusion

Ce cas souligne l'importance cruciale de la gestion correcte de la mémoire et des chaînes de caractères en C/C++. L'omission d'un seul octet pour le terminateur nul (\0) peut entraîner des débordements de tampon et une corruption de données adjacentes, conduisant à des cmoportements erratiques et difficiles à diagnostiquer, surtout dans des environnements complexes comme un moteur de base de données.

Étiquettes: MySQL C++ Requêtes Préparées Gestion de Chaînes Null Terminator

Publié le 27 juillet à 03h39