Uploaded image for project: 'MariaDB Server'
  1. MariaDB Server
  2. MDEV-40590

JSON_OVERLAPS crashes the server on an array nested deeper than the JSON depth limit

    XMLWordPrintable

Details

    • Bug
    • Status: Confirmed (View Workflow)
    • Critical
    • Resolution: Unresolved
    • 10.11, 11.4, 11.8, 12.3, 13.0
    • 10.11, 11.4, 11.8
    • JSON
    • 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

          Activity

            People

              rucha174 Rucha Deodhar
              arcivanov Arcadiy Ivanov
              Votes:
              0 Vote for this issue
              Watchers:
              3 Start watching this issue

              Dates

                Created:
                Updated:

                Time Tracking

                  Estimated:
                  Original Estimate - 0d
                  0d
                  Remaining:
                  Remaining Estimate - 2.5d
                  2.5d
                  Logged:
                  Time Spent - Not Specified
                  Not Specified

                  Git Integration

                    Error rendering 'com.xiplink.jira.git.jira_git_plugin:git-issue-webpanel'. Please contact your Jira administrators.