Commit: patch 9.2.0236: stack-overflow with deeply nested data in json_encode/decode()

0 views
Skip to first unread message

Christian Brabandt

unread,
3:32 PM (8 hours ago) 3:32 PM
to vim...@googlegroups.com
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 <matt...@gmail.com>
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 <matt...@gmail.com>
Signed-off-by: Christian Brabandt <c...@256bit.org>

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,
/**/
Reply all
Reply to author
Forward
0 new messages