Commit: patch 9.2.1129: out of bounds read in the 'complete' F{func} callbacks

3 views
Skip to first unread message

Christian Brabandt

unread,
Sep 24, 2026, 3:30:15 PM (2 days ago) Sep 24
to vim...@googlegroups.com
patch 9.2.1129: out of bounds read in the 'complete' F{func} callbacks

Commit: https://github.com/vim/vim/commit/7854b1dbdb50b6264cb8b54cdd90e042d19f93b4
Author: Christian Brabandt <c...@256bit.org>
Date: Thu Sep 24 19:15:15 2026 +0000

patch 9.2.1129: out of bounds read in the 'complete' F{func} callbacks

Problem: set_cpt_callbacks() always uses the F{func} callback array from
the buffer-local value, causing heap-buffer-overflow
(Evaopo, after v9.1.1603)
Solution: Parse the option value that actually changed and check the
callback array index.

closes: #21358

Assisted-by: Claude
Signed-off-by: Christian Brabandt <c...@256bit.org>

diff --git a/src/insexpand.c b/src/insexpand.c
index 9b9738077..811a79c61 100644
--- a/src/insexpand.c
+++ b/src/insexpand.c
@@ -3445,17 +3445,17 @@ ins_compl_next_buf(buf_T *buf, int flag)
}

/*
- * Count the number of entries in the 'complete' option (curbuf->b_p_cpt).
+ * Count the number of entries in the 'complete' option value "cpt".
* Each non-empty, comma-separated segment is counted as one entry.
*/
static int
-get_cpt_sources_count(void)
+get_cpt_sources_count(char_u *cpt)
{
char_u dummy[LSIZE];
int count = 0;
char_u *p;

- for (p = curbuf->b_p_cpt; *p != NUL; )
+ for (p = cpt; *p != NUL; )
{
while (*p == ',' || *p == ' ')
p++; // Skip delimiters
@@ -3585,12 +3585,12 @@ clear_cpt_callbacks(callback_T **callbacks, int count)
static int
copy_cpt_callbacks(callback_T **dest, int *dest_cnt, callback_T *src, int cnt)
{
- if (cnt == 0)
- return OK;
-
clear_cpt_callbacks(dest, *dest_cnt);
*dest_cnt = 0;

+ if (cnt == 0)
+ return OK;
+
*dest = ALLOC_CLEAR_MULT(callback_T, cnt);
if (*dest == NULL)
return FAIL;
@@ -3612,7 +3612,7 @@ copy_cpt_callbacks(callback_T **dest, int *dest_cnt, callback_T *src, int cnt)
set_buflocal_cpt_callbacks(buf_T *buf UNUSED)
{
# ifdef FEAT_EVAL
- if (buf == NULL || cpt_cb_count == 0)
+ if (buf == NULL)
return;
(void)copy_cpt_callbacks(&buf->b_p_cpt_cb, &buf->b_p_cpt_count, cpt_cb,
cpt_cb_count);
@@ -3620,36 +3620,35 @@ set_buflocal_cpt_callbacks(buf_T *buf UNUSED)
}

/*
- * Parse 'complete' option and initialize F{func} callbacks.
- * Frees any existing callbacks and allocates new ones.
- * Only F{func} entries are processed; others are ignored.
+ * Parse the 'complete' option value "cpt" and store the callbacks for the
+ * F{func} entries in a newly allocated array in "*cbp", setting "*cnt" to the
+ * number of entries. The array has one entry for every item in "cpt", also
+ * for items that are not a function, so that it can be indexed with the item
+ * index. Any previous array in "*cbp" is cleared.
+ * Returns FAIL when out of memory.
*/
- int
-set_cpt_callbacks(optset_T *args)
+ static int
+parse_cpt_callbacks(char_u *cpt, callback_T **cbp, int *cnt)
{
char_u buf[LSIZE];
char_u *p;
int idx = 0;
int slen;
int count;
- int local = (args->os_flags & OPT_LOCAL) != 0;
-
- if (curbuf == NULL)
- return FAIL;

- clear_cpt_callbacks(&curbuf->b_p_cpt_cb, curbuf->b_p_cpt_count);
- curbuf->b_p_cpt_count = 0;
+ clear_cpt_callbacks(cbp, *cnt);
+ *cnt = 0;

- count = get_cpt_sources_count();
+ count = get_cpt_sources_count(cpt);
if (count == 0)
return OK;

- curbuf->b_p_cpt_cb = ALLOC_CLEAR_MULT(callback_T, count);
- if (curbuf->b_p_cpt_cb == NULL)
+ *cbp = ALLOC_CLEAR_MULT(callback_T, count);
+ if (*cbp == NULL)
return FAIL;
- curbuf->b_p_cpt_count = count;
+ *cnt = count;

- for (p = curbuf->b_p_cpt; *p != NUL; )
+ for (p = cpt; *p != NUL; )
{
while (*p == ',' || *p == ' ')
p++; // Skip delimiters
@@ -3659,28 +3658,52 @@ set_cpt_callbacks(optset_T *args)
slen = copy_option_part(&p, buf, LSIZE, ","); // Advance p
if (slen > 0 && buf[0] == 'F' && buf[1] != NUL)
{
- char_u *caret;
- caret = vim_strchr(buf, '^');
+ char_u *caret = vim_strchr(buf, '^');
+
if (caret != NULL)
*caret = NUL;

- if (option_set_callback_func(buf + 1, &curbuf->b_p_cpt_cb[idx])
- != OK)
- curbuf->b_p_cpt_cb[idx].cb_name = NULL;
+ if (option_set_callback_func(buf + 1, &(*cbp)[idx]) != OK)
+ (*cbp)[idx].cb_name = NULL;
}
idx++;
}
}

- if (!local) // ':set' used instead of ':setlocal'
- // Cache the callback array
- if (copy_cpt_callbacks(&cpt_cb, &cpt_cb_count, curbuf->b_p_cpt_cb,
- curbuf->b_p_cpt_count) != OK)
- return FAIL;
-
return OK;
}

+/*
+ * Parse 'complete' option and initialize F{func} callbacks.
+ * Frees any existing callbacks and allocates new ones.
+ */
+ int
+set_cpt_callbacks(optset_T *args)
+{
+ int opt_flags = args->os_flags;
+
+ if (curbuf == NULL)
+ return FAIL;
+
+ // ":setglobal" does not change the buffer-local option value
+ if (!(opt_flags & OPT_GLOBAL)
+ && parse_cpt_callbacks(curbuf->b_p_cpt, &curbuf->b_p_cpt_cb,
+ &curbuf->b_p_cpt_count) != OK)
+ return FAIL;
+
+ // ":setlocal" does not change the global option value
+ if (opt_flags & OPT_LOCAL)
+ return OK;
+
+ // Cache the callbacks for the global value
+ if (opt_flags & OPT_GLOBAL)
+ return parse_cpt_callbacks(p_cpt, &cpt_cb, &cpt_cb_count);
+
+ // set case
+ return copy_cpt_callbacks(&cpt_cb, &cpt_cb_count, curbuf->b_p_cpt_cb,
+ curbuf->b_p_cpt_count);
+}
+
/*
* Parse the 'thesaurusfunc' option value and set the callback function.
* Invoked when the 'thesaurusfunc' option is set. The option value can be a
@@ -5278,6 +5301,9 @@ get_callback_if_cpt_func(char_u *p, int idx)
if (*++p != ',' && *p != NUL)
{
// 'F{func}' case
+ if (curbuf->b_p_cpt_cb == NULL
+ || idx < 0 || idx >= curbuf->b_p_cpt_count)
+ return NULL;
return curbuf->b_p_cpt_cb[idx].cb_name != NULL
? &curbuf->b_p_cpt_cb[idx] : NULL;
}
@@ -7732,7 +7758,7 @@ setup_cpt_sources(void)

cpt_sources_clear();

- count = get_cpt_sources_count();
+ count = get_cpt_sources_count(curbuf->b_p_cpt);
if (count == 0)
return OK;

diff --git a/src/testdir/test_ins_complete.vim b/src/testdir/test_ins_complete.vim
index 2ae9b109f..165f7d236 100644
--- a/src/testdir/test_ins_complete.vim
+++ b/src/testdir/test_ins_complete.vim
@@ -6882,4 +6882,80 @@ func Test_complete_fuzzy_resort()
set completeopt&
endfunc

+" Test for 'complete' F{func} callbacks when using ":setglobal"
+func Test_complete_cpt_func_setglobal()
+ func! CptSetglobalOne(findstart, base)
+ if a:findstart
+ return col('.') - 1
+ endif
+ return ['one']
+ endfunc
+ func! CptSetglobalTwo(findstart, base)
+ if a:findstart
+ return col('.') - 1
+ endif
+ return ['two']
+ endfunc
+
+ new
+ setlocal complete=FCptSetglobalOne
+ " ":setglobal" does not change the buffer-local value
+ setglobal complete=t,FCptSetglobalTwo
+ exe "normal! i\<C-N>\<Esc>"
+ call assert_equal('one', getline(1))
+
+ " A new buffer uses the global value, the callbacks must match it
+ new
+ exe "normal! i\<C-N>\<Esc>"
+ call assert_equal('two', getline(1))
+ bwipe!
+ bwipe!
+
+ " When the global value has more entries than the buffer-local one, the
+ " callback array used to be indexed out of bounds
+ new
+ setlocal complete=.
+ setglobal complete=FCptSetglobalTwo,FCptSetglobalTwo,FCptSetglobalTwo,FCptSetglobalTwo,FCptSetglobalTwo,FCptSetglobalTwo,FCptSetglobalTwo,FCptSetglobalTwo
+ new
+ exe "normal! i\<C-N>\<Esc>"
+ call assert_equal('two', getline(1))
+ bwipe!
+ bwipe!
+
+ " ":setlocal" does not change the callbacks cached for the global value
+ new
+ setlocal complete=FCptSetglobalOne
+ new
+ exe "normal! i\<C-N>\<Esc>"
+ call assert_equal('two', getline(1))
+ bwipe!
+ bwipe!
+
+ set complete&
+ delfunc CptSetglobalOne
+ delfunc CptSetglobalTwo
+endfunc
+
+
+" change the completion value while being triggered
+func Test_complete_cpt_func_changes_complete()
+ func! CptChange(findstart, base)
+ if a:findstart
+ set complete=.
+ return col('.') - 1
+ endif
+ return ['changed']
+ endfunc
+
+ new
+ call setline(1, ['alpha', ''])
+ setlocal complete=FCptChange,FCptChange,FCptChange
+ exe "normal! Gi\<C-N>\<Esc>"
+ call assert_equal('alpha', getline(2))
+
+ set complete&
+ delfunc CptChange
+ bwipe!
+endfunc
+
" vim: shiftwidth=2 sts=2 expandtab nofoldenable
diff --git a/src/version.c b/src/version.c
index 8ed01bff9..0d0adbc35 100644
--- a/src/version.c
+++ b/src/version.c
@@ -763,6 +763,8 @@ static char *(features[]) =

static int included_patches[] =
{ /* Add new patch number below this line */
+/**/
+ 1129,
/**/
1128,
/**/
Reply all
Reply to author
Forward
0 new messages