Bug #59699 InnoDB - Hidden bug would cause a crash for unknown tablespace flags.
Submitted: 24 Jan 2011 16:03 Modified: 11 Feb 2011 0:25
Reporter: Kevin Lewis Email Updates:
Status: Closed Impact on me:
None 
Category:MySQL Server: InnoDB storage engine Severity:S3 (Non-critical)
Version:5.5 OS:Any
Assigned to: Kevin Lewis CPU Architecture:Any

[24 Jan 2011 16:03] Kevin Lewis
Description:
There is a bug in the current code which would cause a crash of InnoDB if a non-standard or unknown tablespace flags existed in a SYS_TABLES record. This is important because the next file version, Cheetah, will identify itself by expanding this field.  So unless this is fixed, an older engine that tries to open a table in a tablespace with a newer file version will crash instead of report an error and refuse to open the table, as it should do.

Details;
The file space flag can be found in page zero of each ibd file at offset 38 + 16 (FSP_HEADER_OFFSET + FSP_SPACE_FLAGS).  In engines that support row_format=barracuda, only the lowest order 6 bits are used or allowed to be non-zero.

The lowest order bit is for DICT_TF_COMPACT. 
The next 4 bits are for the zip size.
The 6th bit counting from 1, is the file format bit.

No other bits are ever looked at.  See this in 
 innobase\include\dict0mem.h(96): 
   #define DICT_TF_BITS	6	/*!< number of flag bits */

The following assert is used in several places to make sure no bit is set in the 32 bit flags other than the first 6 low order bits.
   ut_a(!(flags & (~0UL << DICT_TF_BITS)));

So if any engine that supports Barracuda file format tries to open a tablespace that uses any of those bits, it should fail to open the table and report an error before any of these asserts are hit.

The first place these flags are encountered when opening a file-per-table tablespace is when reading the SYS_TABLES record in the system tablespace.
The function is called dict_sys_tables_get_flags() in dict0load.c.

This function contains an 'if' statement that looks for any bits set other than the first 6.  If so, it returns ULINT_UNDEFINED to the caller.

The function is called at startup when the system tables are first read.  That code path works correctly by reporting the following errors;
  InnoDB: Error: table 'test/t4'
  InnoDB: in InnoDB data dictionary has unknown type 69.

This is that call stack that correctly skips any table that has an unknown flag;
  dict_sys_tables_get_flags(),
  dict_check_tablespaces_and_store_max_id()
  innobase_start_or_create_for_mysql()
  innobase_init()

But when the table is explicitly accessed, the following call stack crashes;
  dict_sys_tables_get_flags()
  dict_load_table_low()
  dict_load_table()
  dict_table_get_low()
  dict_table_open_on_name_low()
  dict_table_open_on_name()
  innobase::open()

It crashes in dict_load_table() line 1688;  
   if (table->space == 0) {
because table has not been assigned a value by dict_load_table_low(), which has a bug in its error management in that it reports this error;
  InnoDB: in InnoDB data dictionary has unknown type 69.
and then returns NULL.  
The caller, dict_load_table(), does not know it failed, and assumes that 'table' was assigned a value.

dict_load_table_low() usually returns an error string when there is a problem, but in this case, it returns NULL, and the caller crashes.

How to repeat:
set global innodb_file_per_table=ON;
set global innodb_file_format=Barracuda;
create table t1 (a int) row_format=compressed;

Set a breakpoint at dict0load.c, line 635;
	if (UNIV_UNLIKELY(flags & (~0 << DICT_TF_BITS))) {

select * from t1;

Change flags from 0x29 to 0x69.  (101001 to 1101001)
This sets the 7th bit which is unknown.

Step forward until the caller crashes.

Suggested fix:
=== modified file 'storage/innobase/dict/dict0load.c'
--- storage/innobase/dict/dict0load.c   2011-01-17 12:20:52 +0000
+++ storage/innobase/dict/dict0load.c   2011-01-22 05:11:28 +0000
@@ -1550,7 +1550,7 @@
                                "InnoDB: in InnoDB data dictionary"
                                " has unknown type %lx.\n",
                                (ulong) flags);
-                       return(NULL);
+                       return("incorrect flags in SYS_TABLES");
                }
        } else {
                flags = 0;
[25 Jan 2011 20:30] Marko Mäkelä
This bug was introduced in MySQL 5.5, when refactoring the access functions to data dictionary records.

dict_load_table_low() in 5.5+ is based on dict_load_table() in earlier versions, but the return type was changed from dict_table_t* (returning a table handle, or NULL on error) to const char* (returning an error message string, or NULL on success).

The patch looks correct. I reviewed all the refactored functions in dict0load.c and did not find other cases where they would return NULL on failure.
[4 Feb 2011 14:59] Bugs System
A patch for this bug has been committed. After review, it may
be pushed to the relevant source trees for release in the next
version. You can access the patch from:

  http://lists.mysql.com/commits/130427

3303 kevin.lewis@oracle.com	2011-02-04
      Bug#59699 - Hidden bug would cause a crash for unknown tablespace flags.
      
      The bug would cause a crash of InnoDB if a non-standard or unknown table
      flags existed in a SYS_TABLES record. This is important because the next
      file version, Cheetah, will identify itself by expanding this field.  So
      unless this is fixed, an older engine that tries to open a table in a
      tablespace with a newer file version will crash instead of report an error
      and refuse to open the table, as it should do.
      
      Reviewed at RB://583.    Approved by Marko.
[4 Feb 2011 16:28] Kevin Lewis
Pushed to mysql-5.5-innodb and mysql-trunk-innodb
[8 Feb 2011 17:37] Bugs System
Pushed into mysql-trunk 5.6.2 (revid:vasil.dimov@oracle.com-20110208173442-ocy58fdcuew3xvex) (version source revid:vasil.dimov@oracle.com-20110208173331-fu0j2s14jbg915zu) (merge vers: 5.6.2) (pib:24)
[8 Feb 2011 17:37] Bugs System
Pushed into mysql-5.5 5.5.10 (revid:vasil.dimov@oracle.com-20110208173046-qsmzbrw1gppahx5o) (version source revid:vasil.dimov@oracle.com-20110208172800-tls70r2ot1i0dub7) (merge vers: 5.5.10) (pib:24)