Details
-
Bug
-
Status: Open (View Workflow)
-
Critical
-
Resolution: Unresolved
-
N/A
-
None
-
Q3/2026 Server Maintenance
Description
DENY JSON db-name parse has no error branch on unescape failure, leaving entry.db NULL and crashing the server (NULL deref) on grant load (sql/sql_acl.cc:1286).
# DENY JSON db-parse: when the db name cannot be unescaped (here 300 chars, |
# longer than the NAME_LEN+1 buffer), sql/sql_acl.cc:1286 has no error branch |
# (unlike the table branch at 1314 and column branch at 1336), so entry.db |
# stays NULL and the entry is wrongly accepted. On grant load the NULL db is |
# dereferenced in acl_update_db / acl_insert_db (sql/sql_acl.cc:4467 / 4511), |
# crashing the server. A fixed build rejects the entry with a Malformed-DENY |
# warning and survives the flush. |
|
|
CREATE USER u@localhost; |
|
|
--eval CALL mtr.add_suppression('.*Malformed DENY entry in mysql\\.global_priv at array position 0 for ''u''@''localhost'' ignored')
|
|
|
UPDATE mysql.global_priv SET Priv= json_set(Priv, '$.denies', json_array(json_object('type','db','db', REPEAT('a',300), 'bits',1))) WHERE User='u' AND Host='localhost'; |
|
|
FLUSH PRIVILEGES; |
|
|
DROP USER u@localhost; |
|
|
--let SEARCH_FILE= `SELECT @@log_error`
|
--let SEARCH_PATTERN= Malformed DENY entry in mysql.global_priv at array position 0 for 'u'@'localhost' ignored
|
--source include/search_pattern_in_file.inc |
Leads to:
|
MDEV-14443 CS 13.1.0 b53c2617de05d2a2445addbb90de87485645cfa3 (Optimized, Clang 22.1.6-20260529) Build 11/06/2026 |
Core was generated by `/test/MDEV-14443_MD110626-mariadb-13.1.0-linux-x86_64-opt/bin/mariadbd --defaul'.
|
Program terminated with signal SIGSEGV, Segmentation fault.
|
#0 __strlen_evex () at ../sysdeps/x86_64/multiarch/strlen-evex-base.S:81
|
|
|
[Current thread is 1 (LWP 1734750)]
|
(gdb) bt
|
#0 __strlen_evex () at ../sysdeps/x86_64/multiarch/strlen-evex-base.S:81
|
#1 0x0000597776e96768 in strdup_root (root=0x5977773759b0 <acl_memroot>, str=0x0) at /test/MDEV-14443_opt/mysys/my_alloc.c:672
|
#2 0x00005977767d575a in acl_insert_db (user=<optimized out>, host=<optimized out>, db=<optimized out>, privileges={m_allow_bits = NO_ACL, m_deny_bits = SELECT_ACL, m_deny_subtree = NO_ACL}) at /test/MDEV-14443_opt/sql/sql_acl.cc:4511
|
#3 0x00005977767d4f7e in apply_deny_db (combo=@0x7bf6b822c628: {<AUTHID> = {user = {str = 0x7bf67c255ef0 "u", length = 1}, host = {str = 0x7bf67c143f70 "localhost", length = 9}}, auth = 0x597778fc0e28}, acc=@0x7bf6b822c610: {m_allow_bits = NO_ACL, m_deny_bits = SELECT_ACL, m_deny_subtree = NO_ACL}, db=0x0) at /test/MDEV-14443_opt/sql/sql_acl.cc:6959
|
#4 apply_deny_to_caches (user=@0x7bf6b822c628: {<AUTHID> = {user = {str = 0x7bf67c255ef0 "u", length = 1}, host = {str = 0x7bf67c143f70 "localhost", length = 9}}, auth = 0x597778fc0e28}, acc=@0x7bf6b822c610: {m_allow_bits = NO_ACL, m_deny_bits = SELECT_ACL, m_deny_subtree = NO_ACL}, type=<optimized out>, db=0x0, table=<optimized out>, column=<optimized out>) at /test/MDEV-14443_opt/sql/sql_acl.cc:7159
|
#5 0x00005977767c2bfe in apply_all_denies_to_caches (user_table=@0x7bf6b822c230: {<Grant_table_base> = {_vptr$Grant_table_base = 0x5977771bdea8 <vtable for User_table_json+16>, min_columns = 3, start_priv_columns = 0, end_priv_columns = 3, pk_parts = 2, m_table = 0x597779305cf0, static _vtable$ = <optimized out>}, static _vtable$ = <optimized out>}, combo=@0x7bf6b822c628: {<AUTHID> = {user = {str = 0x7bf67c255ef0 "u", length = 1}, host = {str = 0x7bf67c143f70 "localhost", length = 9}}, auth = 0x597778fc0e28}) at /test/MDEV-14443_opt/sql/sql_acl.cc:6911
|
#6 grant_load (thd=0x7bf67c000c70, user_table=@0x7bf6b822c230: {<Grant_table_base> = {_vptr$Grant_table_base = 0x5977771bdea8 <vtable for User_table_json+16>, min_columns = 3, start_priv_columns = 0, end_priv_columns = 3, pk_parts = 2, m_table = 0x597779305cf0, static _vtable$ = <optimized out>}, static _vtable$ = <optimized out>}, tables_priv=@0x7bf6b822c290: {<Grant_table_base> = {_vptr$Grant_table_base = 0x5977771be1e8 <vtable for Tables_priv_table+16>, min_columns = 8, start_priv_columns = 0, end_priv_columns = 8, pk_parts = 4, m_table = 0x59777934dbc0, static _vtable$ = <optimized out>}, static _vtable$ = <optimized out>}, columns_priv=@0x7bf6b822c2b0: {<Grant_table_base> = {_vptr$Grant_table_base = 0x5977771be220 <vtable for Columns_priv_table+16>, min_columns = 7, start_priv_columns = 0, end_priv_columns = 7, pk_parts = 5, m_table = 0x5977792cf8b0, static _vtable$ = <optimized out>}, static _vtable$ = <optimized out>}, procs_priv=@0x7bf6b822c2f0: {<Grant_table_base> = {_vptr$Grant_table_base = 0x5977771be290 <vtable for Procs_priv_table+16>, min_columns = 8, start_priv_columns = 0, end_priv_columns = 8, pk_parts = 5, m_table = 0x59777934ce20, static _vtable$ = <optimized out>}, static _vtable$ = <optimized out>})at /test/MDEV-14443_opt/sql/sql_acl.cc:9729
|
#7 grant_reload (thd=thd@entry=0x7bf67c000c70)at /test/MDEV-14443_opt/sql/sql_acl.cc:9814
|
#8 0x0000597776a477fc in reload_acl_and_cache (thd=thd@entry=0x7bf67c000c70, options=1, tables=tables@entry=0x0, write_to_binlog=write_to_binlog@entry=0x7bf6b822d150)at /test/MDEV-14443_opt/sql/sql_reload.cc:100
|
#9 0x00005977768ba5c4 in mysql_execute_command (thd=thd@entry=0x7bf67c000c70, is_called_from_prepared_stmt=false)at /test/MDEV-14443_opt/sql/sql_parse.cc:5397
|
#10 0x00005977768b352d in mysql_parse (thd=thd@entry=0x7bf67c000c70, rawbuf=<optimized out>, length=<optimized out>, parser_state=parser_state@entry=0x7bf6b822d4e0)at /test/MDEV-14443_opt/sql/sql_parse.cc:7959
|
#11 0x00005977768b1d05 in dispatch_command (command=command@entry=COM_QUERY, thd=thd@entry=0x7bf67c000c70, packet=packet@entry=0x7bf67c008dd1 "FLUSH PRIVILEGES", packet_length=packet_length@entry=16, blocking=true)at /test/MDEV-14443_opt/sql/sql_parse.cc:1903
|
#12 0x00005977768b39b0 in do_command (thd=thd@entry=0x7bf67c000c70, blocking=true) at /test/MDEV-14443_opt/sql/sql_parse.cc:1437
|
#13 0x0000597776a0c4cd in do_handle_one_connection (connect=<optimized out>, connect@entry=0x597778c8a300, put_in_cache=true)at /test/MDEV-14443_opt/sql/sql_connect.cc:1503
|
#14 0x0000597776a0c302 in handle_one_connection (arg=arg@entry=0x597778c8a300)at /test/MDEV-14443_opt/sql/sql_connect.cc:1415
|
#15 0x0000597776bcff13 in pfs_spawn_thread (arg=0x597779346590)at /test/MDEV-14443_opt/storage/perfschema/pfs.cc:2198
|
#16 0x00007bf6bde9ca94 in start_thread (arg=<optimized out>)at ./nptl/pthread_create.c:447
|
#17 0x00007bf6bdf29c3c in clone3 ()at ../sysdeps/unix/sysv/linux/x86_64/clone3.S:78
|
|
MDEV-14443 CS 13.1.0 b53c2617de05d2a2445addbb90de87485645cfa3 (Debug, Clang 22.1.6-20260529) Build 11/06/2026 |
Core was generated by `/test/MDEV-14443_MD110626-mariadb-13.1.0-linux-x86_64-dbg/bin/mariadbd --defaul'.
|
Program terminated with signal SIGSEGV, Segmentation fault.
|
#0 __strlen_evex () at ../sysdeps/x86_64/multiarch/strlen-evex-base.S:81
|
|
|
[Current thread is 1 (LWP 1321924)]
|
(gdb) bt
|
#0 __strlen_evex () at ../sysdeps/x86_64/multiarch/strlen-evex-base.S:81
|
#1 0x00005a11edfbdc49 in strdup_root (root=0x5a11ee6bb158 <acl_memroot>, str=0x0) at /test/MDEV-14443_dbg/mysys/my_alloc.c:672
|
#2 0x00005a11ed42815b in acl_insert_db (user=0x75189025b650 "u", host=0x7518900b01d0 "localhost", db=0x0, privileges={m_allow_bits = NO_ACL, m_deny_bits = SELECT_ACL, m_deny_subtree = NO_ACL}) at /test/MDEV-14443_dbg/sql/sql_acl.cc:4511
|
#3 0x00005a11ed4273c3 in apply_deny_db (combo=@0x7518cf136790: {<AUTHID> = {user = {str = 0x75189025b650 "u", length = 1}, host = {str = 0x7518900b01d0 "localhost", length = 9}}, auth = 0x7518cf1367d0}, acc=@0x7518cf135df8: {m_allow_bits = NO_ACL, m_deny_bits = SELECT_ACL, m_deny_subtree = NO_ACL}, db=0x0) at /test/MDEV-14443_dbg/sql/sql_acl.cc:6959
|
#4 0x00005a11ed4271c9 in apply_deny_to_caches (user=@0x7518cf136790: {<AUTHID> = {user = {str = 0x75189025b650 "u", length = 1}, host = {str = 0x7518900b01d0 "localhost", length = 9}}, auth = 0x7518cf1367d0}, acc=@0x7518cf135df8: {m_allow_bits = NO_ACL, m_deny_bits = SELECT_ACL, m_deny_subtree = NO_ACL}, type=PRIV_TYPE_DB, db=0x0, table=0x0, column=0x0)at /test/MDEV-14443_dbg/sql/sql_acl.cc:7159
|
#5 0x00005a11ed427026 in apply_all_denies_to_caches (user_table=@0x7518cf136c68: {<Grant_table_base> = {_vptr$Grant_table_base = 0x5a11ee3b0520 <vtable for User_table_json+16>, min_columns = 3, start_priv_columns = 0, end_priv_columns = 3, pk_parts = 2, m_table = 0x5a11efd5e770, static _vtable$ = <optimized out>}, static _vtable$ = <optimized out>}, combo=@0x7518cf136790: {<AUTHID> = {user = {str = 0x75189025b650 "u", length = 1}, host = {str = 0x7518900b01d0 "localhost", length = 9}}, auth = 0x7518cf1367d0}) at /test/MDEV-14443_dbg/sql/sql_acl.cc:6911
|
#6 0x00005a11ed40fe73 in grant_load (thd=0x751890000d60, user_table=@0x7518cf136c68: {<Grant_table_base> = {_vptr$Grant_table_base = 0x5a11ee3b0520 <vtable for User_table_json+16>, min_columns = 3, start_priv_columns = 0, end_priv_columns = 3, pk_parts = 2, m_table = 0x5a11efd5e770, static _vtable$ = <optimized out>}, static _vtable$ = <optimized out>}, tables_priv=@0x7518cf136cc8: {<Grant_table_base> = {_vptr$Grant_table_base = 0x5a11ee3b09d8 <vtable for Tables_priv_table+16>, min_columns = 8, start_priv_columns = 0, end_priv_columns = 8, pk_parts = 4, m_table = 0x5a11efdd7c70, static _vtable$ = <optimized out>}, static _vtable$ = <optimized out>}, columns_priv=@0x7518cf136ce8: {<Grant_table_base> = {_vptr$Grant_table_base = 0x5a11ee3b0a10 <vtable for Columns_priv_table+16>, min_columns = 7, start_priv_columns = 0, end_priv_columns = 7, pk_parts = 5, m_table = 0x5a11efdb76c0, static _vtable$ = <optimized out>}, static _vtable$ = <optimized out>}, procs_priv=@0x7518cf136d28: {<Grant_table_base> = {_vptr$Grant_table_base = 0x5a11ee3b0a80 <vtable for Procs_priv_table+16>, min_columns = 8, start_priv_columns = 0, end_priv_columns = 8, pk_parts = 5, m_table = 0x5a11efda3f90, static _vtable$ = <optimized out>}, static _vtable$ = <optimized out>})at /test/MDEV-14443_dbg/sql/sql_acl.cc:9729
|
#7 0x00005a11ed40f166 in grant_reload (thd=0x751890000d60)at /test/MDEV-14443_dbg/sql/sql_acl.cc:9814
|
#8 0x00005a11ed807a6e in reload_acl_and_cache (thd=0x751890000d60, options=1, tables=0x0, write_to_binlog=0x7518cf137af4)at /test/MDEV-14443_dbg/sql/sql_reload.cc:100
|
#9 0x00005a11ed5bab6e in mysql_execute_command (thd=0x751890000d60, is_called_from_prepared_stmt=false)at /test/MDEV-14443_dbg/sql/sql_parse.cc:5397
|
#10 0x00005a11ed5adcc8 in mysql_parse (thd=0x751890000d60, rawbuf=0x751890016f90 "FLUSH PRIVILEGES", length=16, parser_state=0x7518cf1389c0) at /test/MDEV-14443_dbg/sql/sql_parse.cc:7959
|
#11 0x00005a11ed5ab3ee in dispatch_command (command=COM_QUERY, thd=0x751890000d60, packet=0x7518901cb731 "FLUSH PRIVILEGES", packet_length=16, blocking=true)at /test/MDEV-14443_dbg/sql/sql_parse.cc:1903
|
#12 0x00005a11ed5ae74a in do_command (thd=0x751890000d60, blocking=true)at /test/MDEV-14443_dbg/sql/sql_parse.cc:1437
|
#13 0x00005a11ed7b391e in do_handle_one_connection (connect=0x5a11efdbc0a0, put_in_cache=true) at /test/MDEV-14443_dbg/sql/sql_connect.cc:1503
|
#14 0x00005a11ed7b3701 in handle_one_connection (arg=0x5a11efd45ad0)at /test/MDEV-14443_dbg/sql/sql_connect.cc:1415
|
#15 0x00007518d089ca94 in start_thread (arg=<optimized out>)at ./nptl/pthread_create.c:447
|
#16 0x00007518d0929c3c in clone3 ()at ../sysdeps/unix/sysv/linux/x86_64/clone3.S:78
|
Summary: When a database-level DENY entry is read from mysql.global_priv and its db value cannot be unescaped (for example a name longer than the NAME_LEN+1 target buffer, which makes json_unescape return a negative length), the db-parse branch silently leaves entry.db as nullptr and reports the entry as valid. The table and column branches reject such failures; the db branch does not. On the next grant load - FLUSH PRIVILEGES, RELOAD, or server restart - the NULL db is dereferenced in acl_update_db / acl_insert_db, crashing the server. The crash is persistent across restarts while the row remains in the table.
Source
sql/sql_acl.cc:1283-1296 - deny_entry_from_json, db branch:
if (json_get_object_key(element, element + element_len, "db", |
&field_val, &field_len) == JSV_STRING)
|
{
|
int len= json_unescape(json_charset, ..., |
(uchar*)db_buf, (uchar*)db_buf + db_buf_len);
|
if (len >= 0) |
{
|
db_buf[len]= '\0'; |
entry.db= db_buf;
|
}
|
/* <-- no else: on json_unescape failure entry.db stays nullptr */ |
}
|
else
|
return 1; // db field is mandatory for non-global entries |
|
|
if (entry.type == PRIV_TYPE_DB) |
return 0; // returns success with entry.db == nullptr |
The table branch (sql/sql_acl.cc:1314-1315) and column branch (sql/sql_acl.cc:1336-1337) both have else return 1; on the same json_unescape failure. The db branch omits it. entry.db is initialised to nullptr at sql/sql_acl.cc:1245. json_unescape returns a negative value (JSON_ERROR_OUT_OF_SPACE) when the unescaped name exceeds db_buf_len (NAME_LEN+1, with NAME_LEN = 64*3 = 192).
The iterator accepts the entry. deny_iter_t::advance treats a 0 return as a valid entry and emits no "Malformed DENY entry" warning, so the NULL-db entry is loaded silently.
The NULL flows into the cache appliers. On grant load apply_all_denies_to_caches -> apply_deny_to_caches -> apply_deny_db (sql/sql_acl.cc:6948) calls acl_update_db(username, hostname, db=NULL, ...), which dereferences db at sql/sql_acl.cc:4467 (!db[0]); the insert fallback acl_insert_db (sql/sql_acl.cc:4511) calls strdup_root(&acl_memroot, NULL) -> strlen(NULL). No NULL guard exists on this path.
The shipped test mysql-test/main/deny_json_invalid.test exercises the warning path for a malformed column entry, but does not cover a db value that fails to unescape - the exact case the db branch mishandles.
Possible Fix
Add the missing else return 1; to the db branch so an unescape failure rejects the entry, identically to the table and column branches. The iterator then emits the standard "Malformed DENY entry ... ignored" warning and no NULL db ever reaches the cache appliers. See fix.diff.
Completeness notes
- Related hardening in the same function: all three name branches write the terminator as buf[len]= '\0' after if (len >= 0), where json_unescape is given buf + sizeof(buf) as its end and may return len == sizeof(buf) when the unescaped name fills the buffer exactly. buf[len] is then a one-byte write past db_buf / table_buf / column_buf (sql/sql_acl.cc:1366-1368). Because these are adjacent members of deny_iter_t (followed by table_buf, column_buf, then deny_data_t cur), the stray byte lands in the next member or its alignment padding - an intra-object write that is benign in practice and not flagged by ASAN. It is still a defect; the same patch should bound the write (reject len >= sizeof(buf), or pass sizeof(buf) - 1 as the unescape limit) in all three branches.
MTR test
- The test injects a db-level DENY whose db value (300 chars) is too long to unescape into the NAME_LEN+1 buffer, then FLUSH PRIVILEGES. Inverted gate: a fixed build rejects the entry with the "Malformed DENY entry ... ignored" warning and survives the flush (warning FOUND, test PASSES); the vulnerable build accepts the NULL-db entry and crashes on the flush (server SIGSEGV, test FAILS).
AI Proposed Fix
Proposed fix summary: Add else return 1 to the db branch so an unescape failure rejects the entry identically to the table/column branches.
--- a/sql/sql_acl.cc
|
+++ b/sql/sql_acl.cc
|
@@ -1283,6 +1283,8 @@ static int deny_entry_from_json(const char *element, int element_len, |
if (len >= 0) |
{
|
db_buf[len]= '\0'; |
entry.db= db_buf;
|
}
|
+ else |
+ return 1; // Error unescaping db name |
}
|
else |
return 1; // db field is mandatory for non-global entries |
Attachments
Issue Links
- is caused by
-
MDEV-14443 DENY clause for access control a.k.a. "negative grants"
-
- In Testing
-