Details
-
Bug
-
Status: Confirmed (View Workflow)
-
Critical
-
Resolution: Unresolved
-
10.11, 11.4, 11.8, 12.3, 13.0
-
None
Description
JSON_OVERLAPS() calls a wild function pointer and crashes the server when
one of its arguments is a JSON array nested deeper than JSON_DEPTH_LIMIT.
No table, no session setup and no special configuration are needed - a single
SELECT from any user who can run a query is enough.
The depth overflow itself is detected and reported correctly by every other
JSON function; only JSON_OVERLAPS continues to walk an engine that has
already failed, and the engine's stack pointer has by then been left one
element past the end of its stack array.
How to repeat
SELECT JSON_OVERLAPS(CONCAT(REPEAT('[',32),'1',REPEAT(']',32)), '{"a":1}'); |
The document is an array nested 32 deep, which is one past the effective
limit. At 31 the same statement answers normally:
-- fine, returns 0
|
SELECT JSON_OVERLAPS(CONCAT(REPEAT('[',31),'1',REPEAT(']',31)), '{"a":1}'); |
-- crashes
|
SELECT JSON_OVERLAPS(CONCAT(REPEAT('[',32),'1',REPEAT(']',32)), '{"a":1}'); |
Affected argument shapes
Each row below was run in its own server instance.
| argument 1 | argument 2 | result |
|---|---|---|
| array, depth 31 | '{"a":1}' | 0, no crash |
| array, depth 32 | '{"a":1}' | SIGSEGV |
| array, depth 32 | '"s"' | SIGSEGV |
| array, depth 32 | '[[1]]' | SIGSEGV |
| '{"a":1}' | array, depth 32 | SIGSEGV |
| object, depth 32 | '{"a":1}' | 0 + warning 4040, no crash |
So the trigger is an over-deep array, in either argument position; the type
of the other argument does not matter. An over-deep object is handled
correctly.
For contrast, the sibling functions handle the identical argument correctly:
-- both return NULL with warning 4040, no crash
|
SELECT JSON_CONTAINS(CONCAT(REPEAT('[',32),'1',REPEAT(']',32)), '{"a":1}'); |
SELECT JSON_EQUALS(CONCAT(REPEAT('[',32),'1',REPEAT(']',32)), '{"a":1}'); |
Stack trace
Server version: 10.11.19-MariaDB-debug source revision: 1dab253482dd562457c89deb68082fb647572959
|
|
|
Thread pointer: 0x7f1088000dd8
|
Attempting backtrace. Include this in the bug report.
|
mysys/stacktrace.c:215(my_print_stacktrace)
|
sql/signal_handler.cc:232(handle_fatal_signal)
|
/lib64/libc.so.6(+0x19fb0)
|
feedback.cc:0(feedback::mysql_sysvar_debug_interval)
|
|
|
Query (0x7f1088015880): SELECT JSON_OVERLAPS(CONCAT(REPEAT('[',32),'1',REPEAT(']',32)), '{"a":1}')
|
The feedback::mysql_sysvar_debug_interval entry is not a real frame: it is
simply the nearest symbol to the address that was jumped to. From the core
file the call chain is:
#4 0x... in feedback::mysql_sysvar_debug_interval () <-- wild jump target
|
#5 json_scan_next (j=0x...) strings/json_lib.c:982
|
#6 json_compare_arr_and_obj (js=0x..., value=0x...) sql/item_jsonfunc.cc:4762
|
#7 json_find_overlap_with_array (js=0x..., value=0x..., compare_whole=false)
|
sql/item_jsonfunc.cc:4852
|
#8 check_overlaps (js=0x..., value=0x..., compare_whole=false)
|
sql/item_jsonfunc.cc:5069
|
#9 Item_func_json_overlaps::val_bool (this=0x...) sql/item_jsonfunc.cc:5111
|
Root cause
All four steps below were confirmed under gdb on a live server with
watchpoints; none of it is inferred.
Step 1 - the depth check leaves stack_p out of bounds.
mark_array() increments stack_p before testing it, so when the test
fails the increment is not undone:
/* strings/json_lib.c:170-181 */
|
static int mark_array(json_engine_t *j) |
{
|
j->state= JST_ARRAY_START;
|
if (++j->stack_p < JSON_DEPTH_LIMIT) /* <-- incremented, then tested */ |
{
|
j->stack[j->stack_p]= JST_ARRAY_CONT;
|
j->value= j->value_begin;
|
return 0; |
}
|
j->s.error= JE_DEPTH; /* stack_p is left at 32 */ |
return 1; |
}
|
read_obj(), mark_object() and read_array() (:140-196) have the same
shape. Observed:
Hardware watchpoint: *(int*)&js->stack_p
|
Old value = 31
|
New value = 32
|
mark_array (j=0x7f8ee5835590) at strings/json_lib.c:173
|
#1 json_scan_next strings/json_lib.c:982
|
#2 json_skip_to_level (level=2) strings/json_lib.c:1284
|
#3 json_compare_arr_and_obj sql/item_jsonfunc.cc:4774
|
The stack has room for exactly JSON_DEPTH_LIMIT entries, so index 32 is
one past the end:
/* include/json_lib.h:232-233 */
|
int stack[JSON_DEPTH_LIMIT]; /* Keeps the stack of nested JSON structures. */ |
int stack_p; /* The 'stack' pointer. */ |
Note that stack_p is declared immediately after stack, so
stack[JSON_DEPTH_LIMIT] aliases stack_p itself. That is what decides
the value the crash ends up using.
Step 2 - JSON_OVERLAPS keeps scanning the failed engine.
/* sql/item_jsonfunc.cc:4759-4777 */
|
bool json_compare_arr_and_obj(json_engine_t *js, json_engine_t *value) |
{
|
st_json_engine_t loc_val= *value;
|
while (json_scan_next(js) == 0 && js->state == JST_VALUE) |
{
|
if (json_read_value(js)) |
return FALSE; |
...
|
if (js->value_type == JSON_VALUE_ARRAY) |
json_skip_level(js); /* :4774 - return value not checked */ |
}
|
return FALSE; |
}
|
The loop tests the return value of json_scan_next() but not that of
json_skip_level(), and it never looks at js->s.error. So after the
depth failure it goes round again and calls json_read_value(js) on an
engine whose s.error is already JE_DEPTH (-7).
Step 3 - the out-of-bounds read.
Several scan handlers restore the state from the stack. With stack_p at
32 this reads one element past the array, which is stack_p itself, so the
state becomes the literal value 32:
/* strings/json_lib.c:538, reached from read_num() :554 and json_read_value() :966 */
|
j->state= j->stack[j->stack_p];
|
Observed:
Hardware watchpoint: *(int*)&js->state
|
Old value = 0
|
New value = 32
|
skip_num_constant (j=0x7f8ee5835590) at strings/json_lib.c:539
|
#1 read_num strings/json_lib.c:554
|
#2 json_read_value strings/json_lib.c:966
|
#3 json_compare_arr_and_obj sql/item_jsonfunc.cc:4764
|
Step 4 - the wild call.
/* strings/json_lib.c:972-983 */
|
int json_scan_next(json_engine_t *j) |
{
|
int t_next; |
get_first_nonspace(&j->s, &t_next, &j->sav_c_len);
|
...
|
return json_actions[j->state][t_next](j); /* :982 */ |
}
|
json_actions is declared [NR_JSON_STATES][NR_C_CLASSES] with
NR_JSON_STATES = 10 (json_lib.c:756, states enumerated at :113-116), so
indexing it with state 32 reads far past the table and calls whatever the
resulting bytes happen to be. State captured at the faulting call:
Breakpoint at strings/json_lib.c:982 if j->state >= 10 || j->state < 0
|
|
|
STATE=32 STACK_P=32 ERROR=-7 (-7 = JE_DEPTH)
|
(gdb) p j->stack
|
$1 = {6, 8 <repeats 31 times>} (32 elements, indices 0..31)
|
(gdb) p j->stack_p
|
$2 = 32
|
Affected versions
JSON_OVERLAPS was added by MDEV-27677 (commit a653dde279a,
2022-03-30), first released in 10.9.1, and json_compare_arr_and_obj() has
carried this loop since then. Every release from 10.9.1 onward is therefore
expected to be affected.
Verified on 10.11 only (10.11.19-MariaDB-debug @ 1dab253482d). Other
branches were not tested.
Attachments
Issue Links
- split from
-
MDEV-40642 Project "JSON Phoenix" (placeholder)
-
- Open
-