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

Server crash in ExtractValue and UpdateXML on a deeply nested XPath predicate

    XMLWordPrintable

Details

    • Can result in hang or crash

    Description

      Problem and Use Case

      Any user who can log in and run a SELECT can crash the whole server
      with a single statement of well under 1 KB.

      The statement calls ExtractValue() or UpdateXML() with an XPath
      expression built from nested predicate brackets, such as
      a[a[a[ ... ]]]. The server process dies with SIGSEGV. Every other
      connected client loses its session at the same time, and the server has
      to be restarted.

      No table is required, no stored data is required, and no privilege
      beyond the ability to connect and run a SELECT is required. The XML
      document argument is never looked at, so a placeholder such as
      '<a/>' is enough.

      Both ExtractValue() and UpdateXML() reach the same defect, so
      denying one function name does not remove the exposure.

      Context

      The XPath expression handed to ExtractValue() and UpdateXML() is
      parsed in Item_xml_str_func::fix_fields() at
      sql/item_xmlfunc.cc:2798, which calls my_xpath_parse() at line
      2843. That is the only caller of the XPath parser in the tree, and
      Item_func_xml_extractvalue and Item_func_xml_update are the only
      two classes that inherit the call. Parsing happens during statement
      preparation, before any XML is processed.

      sql/item_xmlfunc.cc implements the XPath grammar by recursive
      descent. Predicate brackets are parsed by
      my_xpath_parse_AxisSpecifier_NodeTest_opt_Predicate_list() at line
      1815, which loops over [ tokens and calls
      my_xpath_parse_PredicateExpr() at line 1832 for the content of each
      bracket. That call re-enters the top of the expression grammar and
      closes a cycle of thirteen functions:

      my_xpath_parse_AxisSpecifier_NodeTest_opt_Predicate_list  1815, calls at 1832
        my_xpath_parse_PredicateExpr / my_xpath_parse_Expr      macros, 1661 and 1664
        my_xpath_parse_OrExpr                                   2214, calls at 2216
        my_xpath_parse_AndExpr                                  2246, calls at 2248
        my_xpath_parse_EqualityExpr                             2311, calls at 2314
        my_xpath_parse_RelationalExpr                           2372, calls at 2375
        my_xpath_parse_AdditiveExpr                             2415, calls at 2417
        my_xpath_parse_MultiplicativeExpr                       2467, calls at 2469
        my_xpath_parse_UnaryExpr                                2510, calls at 2513
        my_xpath_parse_UnionExpr                                2096, calls at 2098
        my_xpath_parse_PathExpr                                 2173, calls at 2175
        my_xpath_parse_LocationPath                             1701, calls at 1706
        my_xpath_parse_RelativeLocationPath                     1779, calls at 1781
        my_xpath_parse_Step                                     1867, calls at 1869
        back to my_xpath_parse_AxisSpecifier_NodeTest_opt_Predicate_list
      

      Nothing in the cycle counts depth. The recursion descends one full pass
      through those thirteen functions for every [ in the input, and the
      input sets the depth.

      sql/item_xmlfunc.cc contains exactly one check_stack_overrun()
      call, at line 2006 inside my_xpath_parse_lp_Expr_rp(). That guard
      covers the '(' Expr ')' rule. It runs its check before attempting to
      match, so every route through my_xpath_parse_PrimaryExpr() at line
      2023 is protected. The predicate cycle never reaches it.
      my_xpath_parse_PathExpr() at line 2173 tries
      my_xpath_parse_LocationPath() first and only falls back to the
      branch leading to PrimaryExpr when the location path fails. Input
      shaped like a location path always takes the first branch, so the
      guarded rule is never entered and the guard never runs.

      That guard was added on 2026-07-06 by commit 265bf3a9b10 for
      MDEV-39750, "ExtractValue does not control recursion depth", reported by
      Chris Anley at NCC Group. MDEV-39750 is public and its fix shipped in
      10.6.28, 10.11.19, 11.4.13, 11.8.9, 12.3.3, and 13.0.2. The two vectors
      that fix covers are REPEAT('(', 100000) and REPEAT('/n', 100000).
      The predicate vector reported here bypasses it. This is an incomplete
      fix rather than a regression.

      The recursion itself is original code. The predicate loop and its call
      back into the expression grammar have been present since commit
      5e4c3ce6822c of 2005-12-21, which introduced ExtractValue and
      UpdateXML, and no depth limit has ever guarded them.

      Affected versions. Every branch from 10.6 through main carries the
      unguarded predicate recursion:

      • 10.6, 10.11, 11.4, 11.8, 12.3, 13.0, and main: affected. Each carries
        the MDEV-39750 guard at line 2006, and none of them guards the predicate
        cycle.
      • 12.0, 12.1, and 12.2: affected, and additionally missing the
        MDEV-39750 guard. Their last commits date from 2025-07-31, 2025-11-13,
        and 2026-02-04, so they are no longer receiving merges.

      Thresholds. The default thread_stack is 299008 bytes
      (DEFAULT_THREAD_STACK in include/my_pthread.h:669, which is
      292*1024). The reporter measured the crash on a debug build at depth
      312, with depth 311 surviving. Dividing 299008 by 312 gives 958 bytes
      per predicate level, or roughly 74 bytes for each of the thirteen
      frames. That 958 is a lower bound rather than a measurement, because the
      connection thread has already consumed stack before my_xpath_parse()
      is entered, so the true per level cost is higher. Sanitizer builds raise
      DEFAULT_THREAD_STACK to 2 MiB or 11 MiB and enlarge the frames, so
      the crashing depth there is different and has not been measured.

      Reachability from stored objects. Item_xml_str_func and its two
      subclasses do not override check_vcol_func_processor(). The nearest
      override in the inheritance chain is
      Item_result_field::check_vcol_func_processor(), which returns false
      and therefore permits the function. On that reading ExtractValue()
      is accepted in a virtual column, a CHECK constraint, and a
      DEFAULT expression, all of which are fixed when the table is opened.
      Whether that yields a crash surviving restart is unverified, because the
      CREATE TABLE would itself have to survive fixing the same
      expression. This is worth settling before the severity is finalized.

      Reproduction

      The reporter's statement, which crashes a debug build at the default
      thread_stack. The XPath is 937 bytes.

      SELECT ExtractValue('<a/>', CONCAT(REPEAT('a[',312),'a',REPEAT(']',312)));
      SELECT UpdateXML('<a/>', CONCAT(REPEAT('a[',312),'a',REPEAT(']',312)), '<b/>');
      

      A proposed MTR test, mysql-test/main/xml_xpath_predicate_depth.test.
      The depth is derived from @@thread_stack rather than hardcoded, so
      the test stays past the threshold on builds whose frames differ from the
      reporter's.

      # Each predicate level costs one pass through the thirteen grammar
      # functions of the predicate cycle. On a debug build at the default
      # 292 KiB stack that pass measures at least 958 bytes, which exhausts
      # the stack near depth 312. Dividing the stack by 64 clears that by
      # more than a factor of ten, which keeps the depth past the threshold
      # on optimized builds, where the frames are smaller and more levels
      # fit before the stack runs out.
      --let $depth= `SELECT @@thread_stack DIV 64`
       
      --echo #
      --echo # Test Case 1: A predicate nested past the stack limit is
      --echo # rejected by ExtractValue rather than crashing the server.
      --echo #
      --replace_regex /overrun:  [0-9]* bytes used of a [0-9]* byte stack, and [0-9]* bytes needed/overrun:  'used bytes' used of a 'available' byte stack, and 'X' bytes needed/
      --error ER_STACK_OVERRUN_NEED_MORE
      --eval SELECT ExtractValue('<a/>', CONCAT(REPEAT('a[', $depth), 'a', REPEAT(']', $depth)))
       
      --echo #
      --echo # Test Case 2: The same expression is rejected by UpdateXML,
      --echo # which parses its XPath argument through the same code.
      --echo #
      --replace_regex /overrun:  [0-9]* bytes used of a [0-9]* byte stack, and [0-9]* bytes needed/overrun:  'used bytes' used of a 'available' byte stack, and 'X' bytes needed/
      --error ER_STACK_OVERRUN_NEED_MORE
      --eval SELECT UpdateXML('<a/>', CONCAT(REPEAT('a[', $depth), 'a', REPEAT(']', $depth)), '<b/>')
      

      The test mutates no global state and creates no tables, so it carries no
      cleanup block. Appending the two cases to mysql-test/main/xml.test
      next to the MDEV-39750 cases is the alternative, and matches how
      MDEV-39750 was tested.

      Against unfixed code the test is expected to crash the server rather
      than report a failed expectation, which is the impact this ticket
      claims. That has not been run and stays unverified until it is.

      Repoted by: daffainfo

      Attachments

        Issue Links

          Activity

            People

              holyfoot Alexey Botchkov
              bnestere Brandon Nesterenko
              Votes:
              0 Vote for this issue
              Watchers:
              4 Start watching this issue

              Dates

                Created:
                Updated:
                Resolved:

                Git Integration

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