Details
-
Bug
-
Status: Open (View Workflow)
-
Critical
-
Resolution: Unresolved
-
10.6, 10.11, 11.4, 11.8, 12.3, 13.0
-
None
-
None
Description
The JSON functions that take a document take a pointer into their first
argument's buffer, then evaluate their remaining arguments, then read the
document off that pointer. Evaluating an argument can run arbitrary user
code, and if that code reassigns the same stored-routine variable the
document came from, the variable's buffer is reallocated and the earlier
pointer is left dangling. The document is then read out of freed heap.
This is not particular to the functions that edit a document, nor to
value arguments. Every argument is worked out just as late as every
other, so a path reaches it (JSON_REMOVE, JSON_EXTRACT, JSON_VALUE and
others have no value argument at all), so does a string to search for
(JSON_SEARCH), and so does the number of spaces to indent by
(JSON_DETAILED). Where the function scans the whole document rather
than splicing round one place - JSON_CONTAINS, JSON_OVERLAPS,
JSON_EQUALS - the entire read comes out of the freed buffer rather
than only the tail.
The read is masked today rather than harmless. The functions that splice a
document back together re-parse what they built, and the bytes that come
back out of freed heap usually fail that re-parse, so the statement returns
NULL with a "Broken JSON string" warning instead of crashing. The ones that
only read - JSON_LENGTH, JSON_EXISTS, JSON_CONTAINS_PATH and the like - have
no such re-parse and simply answer from whatever the freed buffer now
holds. Either way nothing tells the user that freed memory was read.
How to repeat
An Oracle-mode package body variable is the vehicle, being visible to both the
statement and the function it calls.
SET sql_mode='ORACLE'; |
DELIMITER $$
|
CREATE PACKAGE pkg AS |
PROCEDURE p; |
END; |
$$
|
CREATE PACKAGE BODY pkg AS |
v TEXT := '{"a":1}'; |
|
|
FUNCTION grow RETURN INT AS |
BEGIN |
v := '{"a":1,"pad":"' || LPAD('x', 20000, 'x') || '"}'; |
RETURN 7; |
END; |
|
|
PROCEDURE p AS |
BEGIN |
SELECT JSON_INSERT(v, '$.b', grow()) AS r; |
END; |
END; |
$$
|
DELIMITER ;
|
CALL pkg.p();
|
On a normal build:
r
|
NULL
|
Warning 4035 Broken JSON string in argument 1 to function 'json_insert' at position 13
|
The variable is 7 bytes before the call and 20016 after it, so the store
cannot have been satisfied in place.
ASAN
Built from 10.11 at 1dab253482d with -DWITH_ASAN=ON, clang 21.
==3238202==ERROR: AddressSanitizer: heap-use-after-free on address 0x7bd1aea4c2ce
|
READ of size 1 at 0x7bd1aea4c2ce thread T6
|
#0 __asan_memcpy
|
#1 Binary_string::q_append(char const*, unsigned long) sql/sql_string.h:435
|
#2 append_simple(String*, char const*, unsigned long) sql/item_jsonfunc.cc:82
|
#3 Item_func_json_insert::val_str(String*) sql/item_jsonfunc.cc:3603
|
#4 Type_handler::Item_send_str(Item*, Protocol*, st_value*) const sql/sql_type.cc:7713
|
#5 Protocol::send_result_set_row(List<Item>*) sql/protocol.cc:1333
|
#14 sp_instr_stmt::exec_core(THD*, unsigned int*) sql/sp_head.cc:3950
|
#18 sp_head::execute_procedure(THD*, List<Item>*) sql/sp_head.cc:2483
|
|
|
0x7bd1aea4c2ce is located 30 bytes inside of 32-byte region
|
[0x7bd1aea4c2b0,0x7bd1aea4c2d0)
|
|
|
freed by thread T6 here:
|
#0 free
|
#1 my_free mysys/my_malloc.c:217
|
#2 Binary_string::free_buffer() sql/sql_string.h:308
|
#3 Binary_string::real_alloc(unsigned long) sql/sql_string.cc:44
|
#4 Binary_string::alloc(unsigned long) sql/sql_string.h:774
|
#5 Field_blob::store(char const*, unsigned long, charset_info_st const*) sql/field.cc:8878
|
#6 Item::save_str_in_field(Field*, bool) sql/item.cc:7192
|
#8 Field::sp_prepare_and_store_item(THD*, Item**) sql/field.cc:1517
|
#9 THD::sp_eval_expr(Field*, Item**) sql/sp_head.cc:453
|
#10 sp_rcontext::set_variable(THD*, unsigned int, Item**) sql/sp_rcontext.cc:640
|
#11 sp_instr_set::exec_core(THD*, unsigned int*) sql/sp_head.cc:3983
|
#15 sp_head::execute_function(...) sql/sp_head.cc:2228
|
#16 Item_sp::execute_impl(THD*, Item**, unsigned int) sql/item.cc:3003
|
#19 Item_func_sp::val_str(String*) sql/item_func.h:4154
|
#20 append_json_value(String*, Item*, String*) sql/item_jsonfunc.cc:1967
|
#21 Item_func_json_insert::val_str(String*) sql/item_jsonfunc.cc:3602
|
|
|
previously allocated by thread T6 here:
|
#0 malloc
|
#1 my_malloc mysys/my_malloc.c:92
|
#2 Binary_string::real_alloc(unsigned long) sql/sql_string.cc:45
|
#4 Field_blob::store(char const*, unsigned long, charset_info_st const*) sql/field.cc:8878
|
#5 Item::save_str_value_in_field(Field*, String*) sql/item.cc:413
|
#6 Item_string::save_in_field(Field*, bool) sql/item.cc:7281
|
#7 Field::sp_prepare_and_store_item(THD*, Item**) sql/field.cc:1517
|
#9 sp_rcontext::set_variable(THD*, unsigned int, Item**) sql/sp_rcontext.cc:640
|
|
|
SUMMARY: AddressSanitizer: heap-use-after-free in __asan_memcpy
|
The free and the read are in the same Item_func_json_insert::val_str
frame, one line apart: the buffer is freed underneath :3602 and read at
:3603.
Where the pointer is taken
Item_func_json_insert::val_str opens with
String *js= args[0]->val_json(&tmp_js);
|
and Item_sp_variable::val_str ends with
str_value.set(res->ptr(), res->length(), res->charset());
|
which aliases rather than copies. That is deliberate - the comment above it
explains that it marks the value const so that functions such as CONCAT cannot
scribble on the variable through it. What is not accounted for is a value
argument writing to the same variable while the alias is still being held.
Affected functions
Every row below was reproduced separately under ASAN, one server per
case, all on 10.11 at 1dab253482d. Grouped by which argument is the
one whose evaluation frees the document.
Splicing a value into the document:
| function | implementation | read | freed under |
|---|---|---|---|
| JSON_INSERT | Item_func_json_insert | item_jsonfunc.cc:3603 | :3602 |
| JSON_SET | Item_func_json_insert | item_jsonfunc.cc:3627 | :3626 |
| JSON_REPLACE | Item_func_json_insert | item_jsonfunc.cc:3627 | :3626 |
| JSON_ARRAY_APPEND | Item_func_json_array_append | item_jsonfunc.cc:2233 | :2228 |
| JSON_ARRAY_INSERT | Item_func_json_array_insert | item_jsonfunc.cc:2428 | :2412 |
| JSON_CONTAINS | Item_func_json_contains | item_jsonfunc.cc:1632 | :1596 |
Working out a path, with no value argument in the statement at all:
| function | implementation | read | freed under |
|---|---|---|---|
| JSON_EXISTS | Item_func_json_exists | item_jsonfunc.cc:734 | :714 |
| JSON_VALUE, JSON_QUERY | Json_path_extractor::extract | item_jsonfunc.cc:818 | :781 |
| JSON_EXTRACT, -> | Item_func_json_extract::read_json | item_jsonfunc.cc:1212 | :1176 |
| JSON_CONTAINS_PATH | Item_func_json_contains_path | item_jsonfunc.cc:1873 | :1845 |
| JSON_LENGTH | Item_func_json_length | item_jsonfunc.cc:3204 | :3187 |
| JSON_REMOVE | Item_func_json_remove | item_jsonfunc.cc:3742 | :3701 |
| JSON_KEYS | Item_func_json_keys | item_jsonfunc.cc:3955 | :3936 |
Working out a second document:
| function | implementation | read | freed under |
|---|---|---|---|
| JSON_EQUALS | Item_func_json_equals | item_jsonfunc.cc:665 | :642 |
| JSON_MERGE, JSON_MERGE_PRESERVE | Item_func_json_merge | item_jsonfunc.cc:2779 | :2767 |
| JSON_MERGE_PATCH | Item_func_json_merge_patch | item_jsonfunc.cc:3108 | :3077 |
| JSON_OVERLAPS | Item_func_json_overlaps | item_jsonfunc.cc:5108 | :5090 |
Working out something that is neither:
| function | implementation | read | freed under | the argument |
|---|---|---|---|---|
| JSON_SEARCH | Item_func_json_search | item_jsonfunc.cc:4187 | :4144 | string to search for |
| JSON_DETAILED | Item_func_json_format | item_jsonfunc.cc:4326 | :4309 | number of spaces to indent by |
*Twenty-one functions across seventeen implementations.* The one
implementation NOT affected among those that take a document is
Item_func_json_contains_path::val_int, and only because it sits
inside #ifdef DUMMY and is never compiled; the live val_bool
entry point is in the table above.
Functions with a single argument - JSON_VALID, JSON_TYPE, JSON_DEPTH,
JSON_QUOTE, JSON_UNQUOTE, JSON_COMPACT, JSON_LOOSE, JSON_NORMALIZE -
have nothing to work out after the document and are not affected.
JSON_DETAILED is affected only in its two-argument form.
What does NOT reproduce
A session variable with a plain stored function, in the default sql_mode, is
clean under ASAN and answers correctly:
DELIMITER $$
|
CREATE FUNCTION grow2() RETURNS INT |
BEGIN
|
SET @v = CONCAT('{"a":1,"pad":"', REPEAT('x', 20000), '"}'); |
RETURN 7; |
END
|
$$
|
DELIMITER ;
|
SET @v = '{"a":1}'; |
SELECT JSON_INSERT(@v, '$.b', grow2()) AS r; |
r
|
{"a": 1, "b": 7}
|
Affected versions
Reproduced on 10.11 at 1dab253482d, which is the tip of that branch. The
same alias-then-evaluate shape is present in sql/item_jsonfunc.cc on 10.5
through main - nine occurrences of the pattern on each branch - but only 10.11
was built and run.
Attachments
Issue Links
- split from
-
MDEV-40642 Project "JSON Phoenix" (placeholder)
-
- Open
-