Uploaded image for project: 'MariaDB Server'
  1. MariaDB Server
  2. MDEV-40634

Const MEMORY table with a BLOB returns another connection's data or freed memory

    XMLWordPrintable

Details

    Description

      Summary

      A single-row table is read once during optimization and its row kept in
      record[0] for the rest of the statement, on the understanding that a const
      table's value is a constant. For a MEMORY table with a BLOB column that row is
      not self-contained: hp_read_blobs() answers the read by pointing
      record[0] at the blob data inside HP_SHARE rather than copying it.

      make_join_statistics() then releases the lock on every const table. From
      that moment the blob data the statement is still reading belongs to no one, and
      another connection is free to overwrite or free it. The statement goes on
      reading it for as long as it runs.

      The result is a const table whose value changes in the middle of the statement
      that is using it, and a read of freed memory.

      How to repeat

      CREATE TABLE t1 (a INT, b BLOB) ENGINE=MEMORY;
      INSERT INTO t1 VALUES (1, REPEAT('x', 4000));
       
      CREATE TABLE t2 (a INT) ENGINE=MEMORY;
      INSERT INTO t2 VALUES (1),(2),(3),(4);
       
      connect (writer,localhost,root,,test);
       
      connection default;
      --send SELECT LEFT(t1.b, 16) AS head, LENGTH(t1.b) AS len FROM t1, t2 WHERE SLEEP(0.5) = 0
       
      connection writer;
      --sleep 1
      UPDATE t1 SET b = REPEAT('y', 4000) WHERE a = 1;
      INSERT INTO t1 VALUES (2, REPEAT('z', 4000)), (3, REPEAT('w', 4000));
       
      connection default;
      --reap
      

      t1 is a const table: one row, read during optimization, unlocked
      immediately after. t2 supplies the four rows of the join, and the
      SLEEP() keeps the statement running long enough for the writer to reach it.

      Result

      head                    len
      xxxxxxxxxxxxxxxx        4000
      xxxxxxxxxxxxxxxx        4000
      zzzzzzzzzzzzzzzz        4000
      zzzzzzzzzzzzzzzz        4000
      

      Four rows of the same const table, and the last two return the blob of a row
      that a different connection inserted after this statement began. The
      UPDATE freed the chain the const row still pointed at, and the INSERT
      of (2, REPEAT('z', 4000)) was handed those exact records.

      LENGTH() stays 4000 throughout, because the length is stored in
      record[0] and only the data moved. There is no error and no warning.

      Result, second variant

      Replacing the writer's statements with

      DELETE FROM t1;
      INSERT INTO t1 VALUES (2, REPEAT('z', 9000));
      

      frees the blocks outright rather than recycling them, and the const row reads
      back the debug build's freed-memory fill pattern (shown as ? below; the
      bytes are non-printable):

      head                    len
      xxxxxxxxxxxxxxxx        4000
      xxxxxxxxxxxxxxxx        4000
      ????????????????        4000
      ????????????????        4000
      

      This is a use-after-free read. No crash was observed in either variant, but
      that is a property of where the allocator happened to put the freed block, not
      of the code: the same free through munmap faults, and ASAN reports
      heap-use-after-free unconditionally.

      CHECK TABLE t1 reports OK in both variants. The table is undamaged – the
      corruption exists only in the reading connection's record[0].

      Analysis

      hp_read_blobs() classifies each blob's storage into three cases:

      • Case A (HP_ROW_SINGLE_REC), single-record run: the record already holds
        the right pointer, and it points into HP_BLOCK
      • Case B (HP_ROW_CONT_ZEROCOPY), single run over several records: the
        pointer is adjusted past the run header, and still points into HP_BLOCK
      • Case C (HP_ROW_MULTIPLE_REC), multi-run chain: the blob is reassembled
        into info->blob_buff, which belongs to the handler

      Only cases A and B are exposed. A case C blob lives in blob_buff until
      heap_reset() frees it at the end of the statement, so it survives the
      unlock; a 4MB blob in the test above returns correct data for that reason. The
      exposed window is therefore small blobs, which is the common case.

      The unlock is reached through mysql_unlock_some_tables() ->
      mysql_unlock_tables() -> unlock_external() -> thr_multi_unlock().
      The comment on the call justifies it with

      It's safe to ignore result code as all tables where opened for read only.

      which holds for a handler that answers a read with a copy of the row. MEMORY
      with a BLOB does not.

      Sequences are already excluded from this unlock, via
      GET_LOCK_SKIP_SEQUENCES, because accessing them may still require table
      updates. Non-transactional TEMPORARY tables never reach it at all –
      get_lock_data() leaves them out of the lock set entirely – so a temporary
      MEMORY table can be neither early-unlocked nor raced.

      Scope

      BLOB support in the MEMORY engine was added by MDEV-38975 and is not present in
      any released version, so this does not affect released servers. It reproduces
      on the current HEAP blob tree.

      Every other early-unlock path in the server was checked and none retains a row
      across the unlock: JOIN::join_free() closes all table cursors before
      calling mysql_unlock_read_tables(), and every mysql_lock_remove() call
      site closes the handler immediately afterwards.

      Attachments

        Issue Links

          Activity

            People

              monty Michael Widenius
              arcivanov Arcadiy Ivanov
              Votes:
              0 Vote for this issue
              Watchers:
              2 Start watching this issue

              Dates

                Created:
                Updated:

                Git Integration

                  Error rendering 'com.xiplink.jira.git.jira_git_plugin:git-issue-webpanel'. Please contact your Jira administrators.