Bug #4519 SQLForeignKeys crash with 19 or more foreign keys
Submitted: 12 Jul 2004 16:41 Modified: 8 Sep 2005 9:54
Reporter: Jochen Willinghöfer Email Updates:
Status: Closed Impact on me:
None 
Category:Connector / ODBC Severity:S2 (Serious)
Version:3.52 OS:Windows (win32)
Assigned to: Bogdan Degtyariov CPU Architecture:Any

[12 Jul 2004 16:41] Jochen Willinghöfer
Description:
SQLForeignKeys crash with 19 or more foreign keys

How to repeat:
Create table with many foreign keys (>= 19).
Function SQLForeignKeys use the value stmt->result->field_count, which is always 18 from mysql_table_status, for size of stmt->result_array. If now many foreign keys (>=19) are written in the stmt->result_array the driver crash.

Suggested fix:
It is not easy to set the correct size for stmt->result_array.

I duplicate the while loop, the first one counts, the second one set the values.

Here the code:
SQLRETURN SQL_API SQLForeignKeys(SQLHSTMT hstmt,
                                 SQLCHAR FAR *szPkTableQualifier,
                                 SQLSMALLINT cbPkTableQualifier,
                                 SQLCHAR FAR *szPkTableOwner,
                                 SQLSMALLINT cbPkTableOwner,
                                 SQLCHAR FAR *szPkTableName,
                                 SQLSMALLINT cbPkTableName,
                                 SQLCHAR FAR *szFkTableQualifier,
                                 SQLSMALLINT cbFkTableQualifier,
                                 SQLCHAR FAR *szFkTableOwner,
                                 SQLSMALLINT cbFkTableOwner,
                                 SQLCHAR FAR *szFkTableName,
                                 SQLSMALLINT cbFkTableName)
{
  STMT FAR *stmt=(STMT FAR*) hstmt;
  uint row_count= 0;

  DBUG_ENTER("SQLForeignKeys");

  DBUG_PRINT("enter",("PKQualifier: '%s'(%d)  PKOwner: '%s'(%d)  PKTable: '%s'(%d) \
          FKQualifier: '%s'(%d)  FKOwner: '%s'(%d)  FKTable: '%s'(%d)",
          szPkTableQualifier ? (char*) szPkTableQualifier : "null", cbPkTableQualifier,
          szPkTableOwner ? (char*) szPkTableOwner : "null", cbPkTableOwner,
          szPkTableName ? (char*) szPkTableName : "null", cbPkTableName,
          szFkTableQualifier ? (char*) szFkTableQualifier : "null", cbFkTableQualifier,
          szFkTableOwner ? (char*) szFkTableOwner : "null", cbFkTableOwner,
          szFkTableName ? (char*) szFkTableName : "null", cbFkTableName));

  if (is_minimum_version(stmt->dbc->mysql.server_version,"3.23",4))
  {
    STMT FAR  *stmt=(STMT FAR*) hstmt;
    MEM_ROOT  *alloc;
    MYSQL_ROW row;
    char      **data;
    char      PkQualifier_buff[NAME_LEN+1],PkName_buff[NAME_LEN+1],
              *PkTableQualifier,*PkTableName;
    char      FkQualifier_buff[NAME_LEN+1],FkName_buff[NAME_LEN+1],
              *FkTableQualifier,*FkTableName;  
    uint       comment_id;

    PkTableQualifier= myodbc_get_valid_buffer((char FAR *)PkQualifier_buff,szPkTableQualifier,
             cbPkTableQualifier);
    PkTableName= myodbc_get_valid_buffer((char FAR*) PkName_buff,szPkTableName,cbPkTableName);
    FkTableQualifier= myodbc_get_valid_buffer((char FAR *)FkQualifier_buff,szFkTableQualifier,
             cbFkTableQualifier);
    FkTableName= myodbc_get_valid_buffer((char FAR*)FkName_buff,szFkTableName,cbFkTableName);
    
    if (FkTableQualifier && !FkTableQualifier[0])
      FkTableQualifier= stmt->dbc->database;

    CLEAR_STMT_ERROR(hstmt);

    pthread_mutex_lock(&stmt->dbc->lock);
    if (!(stmt->result= mysql_table_status(stmt,FkTableQualifier,FkTableName)))
    {    
      DBUG_PRINT("error",("%d:%s", mysql_errno(&stmt->dbc->mysql),
                           mysql_error(&stmt->dbc->mysql)));
      pthread_mutex_unlock(&stmt->dbc->lock);
      goto empty_set;
    }    
    pthread_mutex_unlock(&stmt->dbc->lock);

    comment_id= stmt->result->field_count-1;

	while ((row= mysql_fetch_row(stmt->result)))
    {
      if ((row[1] && strcmp(row[1],"InnoDB")==0))
      {
        const char *token,*pktoken,*fk_cols_start,*pk_cols_start;
        char       *comment_token, ref_token[NAME_LEN+1];
        char       *pkcomment,*fkcomment;
        uint       key_seq,pk_length,fk_length;
        
        if (!(comment_token= strchr(row[comment_id],';')))
          continue; /* MySQL 4.1 and above, the comment field is '15' */

        do 
        {
          /*       
            Found reference information in comment field from InnoDB type, 
            and parse the same to get the FK information .. 
          */
          key_seq= 1;

          if (!(token= my_next_token(NULL,&comment_token,NULL,'(')))
            break;
          fk_cols_start = token;

          if (!(token= my_next_token(token,&comment_token,ref_token,')')))
            continue;        
          fk_length= (uint)((token-1)-fk_cols_start);

          if (!(token= my_next_token(token+7,&comment_token,ref_token,'/')))
            continue;

          if (!(token= my_next_token(token,&comment_token,ref_token,'(')) ||
              myodbc_casecmp(PkTableName,ref_token,strlen(PkTableName)))
            continue;

          pk_cols_start = token;
        
          if (!(token= my_next_token(token,&comment_token,ref_token,')')))
            continue;
          pk_length= (uint)((token-1)-pk_cols_start);
             
          token = fkcomment = (char *)fk_cols_start; 
          pktoken = pkcomment = (char *)pk_cols_start;
          fkcomment[fk_length]= '\0';
          pkcomment[pk_length]= '\0';
    
          while (token= my_next_token(token,&fkcomment,ref_token,' '))
          {
            pktoken= my_next_token(pktoken,&pkcomment,ref_token,' ');
            row_count++;
          }                
          row_count++;

        } while (comment_token = strchr(comment_token,';'));/* multi table ref */
      }
	}

    stmt->result_array= (char**) my_malloc(sizeof(char*)*SQLFORE_KEYS_FIELDS*
             (ulong) row_count,
             MYF(MY_FAE | MY_ZEROFILL));

    pthread_mutex_lock(&stmt->dbc->lock);
    if (!(stmt->result= mysql_table_status(stmt,FkTableQualifier,FkTableName)))
    {    
      DBUG_PRINT("error",("%d:%s", mysql_errno(&stmt->dbc->mysql),
                           mysql_error(&stmt->dbc->mysql)));
      pthread_mutex_unlock(&stmt->dbc->lock);
      goto empty_set;
    }    
    pthread_mutex_unlock(&stmt->dbc->lock);
	row_count = 0;
    /* Convert mysql fields to data that odbc wants */
    alloc= &stmt->result->field_alloc;
    data= stmt->result_array;    

    while ((row= mysql_fetch_row(stmt->result)))
    {
      if ((row[1] && strcmp(row[1],"InnoDB")==0))
      {
        const char *token,*pktoken,*fk_cols_start,*pk_cols_start;
        char       *comment_token, ref_token[NAME_LEN+1];
        char       *pkcomment,*fkcomment;
        uint       key_seq,pk_length,fk_length;
        
        if (!(comment_token= strchr(row[comment_id],';')))
          continue; /* MySQL 4.1 and above, the comment field is '15' */

        do 
        {
          /*       
            Found reference information in comment field from InnoDB type, 
            and parse the same to get the FK information .. 
          */
          key_seq= 1;

          if (!(token= my_next_token(NULL,&comment_token,NULL,'(')))
            break;
          fk_cols_start = token;

          if (!(token= my_next_token(token,&comment_token,ref_token,')')))
            continue;        
          fk_length= (uint)((token-1)-fk_cols_start);

          if (!(token= my_next_token(token+7,&comment_token,ref_token,'/')))
            continue;
          data[0]= strdup_root(alloc,ref_token); /* PKTABLE_CAT */

          if (!(token= my_next_token(token,&comment_token,ref_token,'(')) ||
              myodbc_casecmp(PkTableName,ref_token,strlen(PkTableName)))
            continue;

          data[2]= strdup_root(alloc,ref_token); /* PKTABLE_TABLE */        
          pk_cols_start = token;
        
          if (!(token= my_next_token(token,&comment_token,ref_token,')')))
            continue;
          pk_length= (uint)((token-1)-pk_cols_start);
             
          data[1]= "";                           /* PKTABLE_SCHEM */
          data[4]= strdup_root(alloc,FkTableQualifier); /* FKTABLE_CAT */
          data[5]= "";                           /* FKTABLE_SCHEM */
          data[6]= row[0];                       /* FKTABLE_TABLE */   
 
          /* 
             TODO : FIX both UPDATE_RULE and DELETE_RULE after 
             3.23.52 is released, which supports this feature in 
             server by updating the 'comment' field as well as 
             from SHOW CREATE TABLE defination..

             right now return only SQL_CASCADE as the DELETE/UPDATE 
             rule
          */ 

          data[9]=  "1"; /*SQL_CASCADE*/        /* UPDATE_RULE */ 
          data[10]= "1"; /*SQL_CASCADE*/        /* DELETE_RULE */ 
          data[11]= "NULL";                     /* FK_NAME */
          data[12]= "NULL";                     /* PK_NAME */
          data[13]= "7"; /*SQL_NOT_DEFERRABLE*/ /* DEFERRABILITY */

          token = fkcomment = (char *)fk_cols_start; 
          pktoken = pkcomment = (char *)pk_cols_start;
          fkcomment[fk_length]= '\0';
          pkcomment[pk_length]= '\0';
    
          while (token= my_next_token(token,&fkcomment,ref_token,' '))
          {
            /* Multiple columns exists .. parse them to individual rows */
            char **prev_data= data;
            data[7]= strdup_root(alloc,ref_token);    /* FKTABLE_COLUMN */
            pktoken= my_next_token(pktoken,&pkcomment,ref_token,' ');
            data[3]= strdup_root(alloc,ref_token);    /* PKTABLE_COLUMN */
            sprintf(ref_token,"%d",key_seq++);
            data[8]= strdup_root(alloc,ref_token);    /* KEY_SEQ */
            data+= SQLFORE_KEYS_FIELDS;
            row_count++;
            for (fk_length= SQLFORE_KEYS_FIELDS; fk_length--;)
              data[fk_length]= prev_data[fk_length];
          }                
          data[7]= strdup_root(alloc,fkcomment);      /* FKTABLE_COLUMN */ 
          data[3]= strdup_root(alloc,pkcomment);      /* PKTABLE_COLUMN */
          sprintf(ref_token,"%d",key_seq);
          data[8]= strdup_root(alloc,ref_token);      /* KEY_SEQ */

          data+= SQLFORE_KEYS_FIELDS;
          row_count++;

        } while (comment_token = strchr(comment_token,';'));/* multi table ref */
      }
    } 
  }
  else /* NO FOREIGN KEY support from SERVER */
  {     
    stmt->result=(MYSQL_RES*) my_malloc(sizeof(MYSQL_RES),MYF(MY_ZEROFILL));
    stmt->result->eof=1;
  }  
  stmt->result->row_count= row_count;
  mysql_link_fields(stmt,SQLFORE_KEYS_fields,SQLFORE_KEYS_FIELDS);
  DBUG_PRINT("info ",("total keys count: %ld", row_count));
  DBUG_RETURN_STATUS(SQL_SUCCESS);
  
empty_set:
  DBUG_PRINT("info ",("Can't match anything; Returning empty set"));
  stmt->result= (MYSQL_RES*) my_malloc(sizeof(MYSQL_RES),MYF(MY_ZEROFILL));
  stmt->result->row_count= 0;
  stmt->result_array= (MYSQL_ROW) my_memdup((gptr) SQLFORE_KEYS_values,
                                            sizeof(SQLFORE_KEYS_values), 
                                            MYF(0)); 
  mysql_link_fields(stmt,SQLFORE_KEYS_fields,SQLFORE_KEYS_FIELDS);
  DBUG_RETURN_STATUS(SQL_SUCCESS);
}
[29 Jul 2004 1:22] Timothy Smith
I have verified this with MyODBC 3.51.8+ (bitkeeper source) and MySQL 4.0.20+, on FreeBSD 5.2 with unixODBC 2.2.9.

Please see the "Files" link for test case code.  My test breaks at 30 foreign keys, instead of the 19 reported by Jochen.

Interestingly, it fails in two stages.  At 31, I can fetch all the data, but then I get a core dump when calling SQLDisconnect(hdbc).  It does this from 31 through 35.

At 36, it segfaults on the first SQLFetch(hstmt) after calling SQLForeignKeys().
[29 Jul 2004 1:23] Timothy Smith
Run it like this:   $ perl fk.pl 36 | mysql test

Attachment: fk.pl (application/octet-stream, text), 639 bytes.

[29 Jul 2004 1:24] Timothy Smith
After running fk.pl to create the tables, just run $ ./foreignkeys

Attachment: foreignkeys.c (application/octet-stream, text), 9.49 KiB.

[8 Sep 2005 9:54] Bogdan Degtyariov
Thank you for your bug report. This issue has been committed to our
source repository of that product and will be incorporated into the
next release.

If necessary, you can access the source repository and build the latest
available version, including the bugfix, yourself. More information 
about accessing the source trees is available at
    http://www.mysql.com/doc/en/Installing_source_tree.html