patch 9.2.0236: stack-overflow with deeply nested data in json_encode/decode()
Commit: https://github.com/vim/vim/commit/abd2d7d4532c5687736b88718863163618f13c13 Author: Yasuhiro Matsumoto <[email protected]> Date: Mon Mar 23 21:42:04 2026 +0000 patch 9.2.0236: stack-overflow with deeply nested data in json_encode/decode() Problem: stack-overflow with deeply nested data in json_encode/decode() (ZyX-I) Solution: Add depth limit check using 'maxfuncdepth' to json_encode_item() and json_decode_item() to avoid crash when encoding/decoding deeply nested lists, dicts, or JSON arrays/objects, fix typo in error name, add tests (Yasuhiro Matsumoto). fixes: #588 closes: #19808 Signed-off-by: Yasuhiro Matsumoto <[email protected]> Signed-off-by: Christian Brabandt <[email protected]> diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index d28646778..5f30fb715 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -1,4 +1,4 @@ -*options.txt* For Vim version 9.2. Last change: 2026 Mar 22 +*options.txt* For Vim version 9.2. Last change: 2026 Mar 23 VIM REFERENCE MANUAL by Bram Moolenaar @@ -6026,7 +6026,8 @@ A jump table for the options with a short description can be found at |Q_op|. Increasing this limit above 200 also changes the maximum for Ex command recursion, see |E169|. See also |:function|. - Also used for maximum depth of callback functions. + Also used for maximum depth of callback functions and encoding and + decoding of deeply nested json data. *'maxmapdepth'* *'mmd'* *E223* 'maxmapdepth' 'mmd' number (default 1000) diff --git a/src/errors.h b/src/errors.h index d780fb420..016b917bd 100644 --- a/src/errors.h +++ b/src/errors.h @@ -312,7 +312,7 @@ EXTERN char e_function_name_required[] // E130 unused EXTERN char e_cannot_delete_function_str_it_is_in_use[] INIT(= N_("E131: Cannot delete function %s: It is in use")); -EXTERN char e_function_call_depth_is_higher_than_macfuncdepth[] +EXTERN char e_function_call_depth_is_higher_than_maxfuncdepth[] INIT(= N_("E132: Function call depth is higher than 'maxfuncdepth'")); EXTERN char e_return_not_inside_function[] INIT(= N_("E133: :return not inside a function")); diff --git a/src/json.c b/src/json.c index 8b0b050b8..7c4874e5a 100644 --- a/src/json.c +++ b/src/json.c @@ -18,7 +18,7 @@ #if defined(FEAT_EVAL) -static int json_encode_item(garray_T *gap, typval_T *val, int copyID, int options); +static int json_encode_item(garray_T *gap, typval_T *val, int copyID, int options, int depth); /* * Encode "val" into a JSON format string. @@ -28,7 +28,7 @@ static int json_encode_item(garray_T *gap, typval_T *val, int copyID, int option static int json_encode_gap(garray_T *gap, typval_T *val, int options) { - if (json_encode_item(gap, val, get_copyID(), options) == FAIL) + if (json_encode_item(gap, val, get_copyID(), options, 0) == FAIL) { ga_clear(gap); gap->ga_data = vim_strsave((char_u *)""); @@ -268,7 +268,7 @@ is_simple_key(char_u *key) * Return FAIL or OK. */ static int -json_encode_item(garray_T *gap, typval_T *val, int copyID, int options) +json_encode_item(garray_T *gap, typval_T *val, int copyID, int options, int depth) { char_u numbuf[NUMBUFLEN]; char_u *res; @@ -278,6 +278,12 @@ json_encode_item(garray_T *gap, typval_T *val, int copyID, int options) dict_T *d; int i; + if (depth > p_mfd) + { + emsg(_(e_function_call_depth_is_higher_than_maxfuncdepth)); + return FAIL; + } + switch (val->v_type) { case VAR_BOOL: @@ -365,7 +371,8 @@ json_encode_item(garray_T *gap, typval_T *val, int copyID, int options) for (li = l->lv_first; li != NULL && !got_int; ) { if (json_encode_item(gap, &li->li_tv, copyID, - options & JSON_JS) == FAIL) + options & JSON_JS, + depth + 1) == FAIL) return FAIL; if ((options & JSON_JS) && li->li_next == NULL @@ -401,7 +408,8 @@ json_encode_item(garray_T *gap, typval_T *val, int copyID, int options) { typval_T *t_item = TUPLE_ITEM(tuple, i); if (json_encode_item(gap, t_item, copyID, - options & JSON_JS) == FAIL) + options & JSON_JS, + depth + 1) == FAIL) return FAIL; if ((options & JSON_JS) @@ -452,7 +460,8 @@ json_encode_item(garray_T *gap, typval_T *val, int copyID, int options) write_string(gap, hi->hi_key); ga_append(gap, ':'); if (json_encode_item(gap, &dict_lookup(hi)->di_tv, - copyID, options | JSON_NO_NONE) == FAIL) + copyID, options | JSON_NO_NONE, + depth + 1) == FAIL) return FAIL; } ga_append(gap, '}'); @@ -807,6 +816,12 @@ json_decode_item(js_read_T *reader, typval_T *res, int options) retval = FAIL; break; } + if (stack.ga_len >= p_mfd) + { + emsg(_(e_function_call_depth_is_higher_than_maxfuncdepth)); + retval = FAIL; + break; + } if (ga_grow(&stack, 1) == FAIL) { retval = FAIL; @@ -838,6 +853,12 @@ json_decode_item(js_read_T *reader, typval_T *res, int options) retval = FAIL; break; } + if (stack.ga_len >= p_mfd) + { + emsg(_(e_function_call_depth_is_higher_than_maxfuncdepth)); + retval = FAIL; + break; + } if (ga_grow(&stack, 1) == FAIL) { retval = FAIL; diff --git a/src/json_test.c b/src/json_test.c index 5fb772ee0..a35fd75a4 100644 --- a/src/json_test.c +++ b/src/json_test.c @@ -195,6 +195,7 @@ test_fill_called_on_string(void) main(void) { #if defined(FEAT_EVAL) + p_mfd = 100; test_decode_find_end(); test_fill_called_on_find_end(); test_fill_called_on_string(); diff --git a/src/testdir/test_json.vim b/src/testdir/test_json.vim index 96eddc23d..515ce9b38 100644 --- a/src/testdir/test_json.vim +++ b/src/testdir/test_json.vim @@ -326,4 +326,34 @@ func Test_json_encode_long() call assert_equal(4000, len(json)) endfunc +func Test_json_encode_depth() + let save_mfd = &maxfuncdepth + set maxfuncdepth=10 + + " Create a deeply nested list that exceeds maxfuncdepth. + let l = [] + let d = {} + for i in range(20) + let l = [l] + let d = {1: d} + endfor + call assert_fails('call json_encode(l)', 'E132:') + call assert_fails('call json_encode(d)', 'E132:') + + let &maxfuncdepth = save_mfd +endfunc + +func Test_json_decode_depth() + let save_mfd = &maxfuncdepth + set maxfuncdepth=10 + + let deep_json = repeat('[', 20) .. '1' .. repeat(']', 20) + call assert_fails('call json_decode(deep_json)', 'E132:') + + let deep_json = repeat('{"a":', 20) .. '1' .. repeat('}', 20) + call assert_fails('call json_decode(deep_json)', 'E132:') + + let &maxfuncdepth = save_mfd +endfunc + " vim: shiftwidth=2 sts=2 expandtab diff --git a/src/userfunc.c b/src/userfunc.c index 0d7cf23a0..9de6bdbaf 100644 --- a/src/userfunc.c +++ b/src/userfunc.c @@ -2910,7 +2910,7 @@ funcdepth_increment(void) { if (funcdepth >= p_mfd) { - emsg(_(e_function_call_depth_is_higher_than_macfuncdepth)); + emsg(_(e_function_call_depth_is_higher_than_maxfuncdepth)); return FAIL; } ++funcdepth; diff --git a/src/version.c b/src/version.c index 1fdb6f68f..80680b426 100644 --- a/src/version.c +++ b/src/version.c @@ -734,6 +734,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 236, /**/ 235, /**/ -- -- You received this message from the "vim_dev" maillist. Do not top-post! Type your reply below the text you are replying to. For more information, visit http://www.vim.org/maillist.php --- You received this message because you are subscribed to the Google Groups "vim_dev" group. To unsubscribe from this group and stop receiving emails from it, send an email to [email protected]. To view this discussion visit https://groups.google.com/d/msgid/vim_dev/E1w57S1-0047Eq-Af%40256bit.org.
