CVE-2022-2343: Heap-based Buffer Overflow (CWE-122)
value: Heap-based Buffer Overflow in GitHub repository vim/vim prior to 9.0.0044.
CWE description: A heap overflow condition is a buffer overflow, where the buffer that can be overwritten is allocated in the heap portion of memory, generally meaning that the buffer was allocated using a routine such as malloc().
CVSS v3 base score: 7.8
Published: 2022-07-08T18:15Z
Repository: https://github.com/vim/vim
Language: C
Files changed: src/insexpand.c, src/testdir/test_ins_complete.vim, src/version.c
Commit date: 2022-07-07 19:42:04 +0100
Commit message: patch 9.0.0045: reading past end of completion with a long line

Problem:    Reading past end of completion with a long line and 'infercase'
            set.
Solution:   Allocate the string if needed.

Diff: diff --git a/src/insexpand.c b/src/insexpand.c
index 734550ffd231..0ecb6560c5fc 100644
--- a/src/insexpand.c
+++ b/src/insexpand.c
@@ -522,33 +522,36 @@ ins_compl_accept_char(int c)
     return vim_iswordc(c);
 }
 
 /*
  * Get the completed text by inferring the case of the originally typed text.
+ * If the result is in allocated memory "tofree" is set to it.
  */
     static char_u *
 ins_compl_infercase_gettext(
 	char_u	*str,
-	int	actual_len,
-	int	actual_compl_length,
-	int	min_len)
+	int	char_len,
+	int	compl_char_len,
+	int	min_len,
+	char_u  **tofree)
 {
     int		*wca;			// Wide character array.
     char_u	*p;
     int		i, c;
     int		has_lower = FALSE;
     int		was_letter = FALSE;
+    garray_T	gap;
 
     IObuff[0] = NUL;
 
     // Allocate wide character array for the completion and fill it.
-    wca = ALLOC_MULT(int, actual_len);
+    wca = ALLOC_MULT(int, char_len);
     if (wca == NULL)
 	return IObuff;
 
     p = str;
-    for (i = 0; i < actual_len; ++i)
+    for (i = 0; i < char_len; ++i)
 	if (has_mbyte)
 	    wca[i] = mb_ptr2char_adv(&p);
 	else
 	    wca[i] = *(p++);
 
@@ -564,11 +567,11 @@ ins_compl_infercase_gettext(
 	{
 	    has_lower = TRUE;
 	    if (MB_ISUPPER(wca[i]))
 	    {
 		// Rule 1 is satisfied.
-		for (i = actual_compl_length; i < actual_len; ++i)
+		for (i = compl_char_len; i < char_len; ++i)
 		    wca[i] = MB_TOLOWER(wca[i]);
 		break;
 	    }
 	}
     }
@@ -585,11 +588,11 @@ ins_compl_infercase_gettext(
 	    else
 		c = *(p++);
 	    if (was_letter && MB_ISUPPER(c) && MB_ISLOWER(wca[i]))
 	    {
 		// Rule 2 is satisfied.
-		for (i = actual_compl_length; i < actual_len; ++i)
+		for (i = compl_char_len; i < char_len; ++i)
 		    wca[i] = MB_TOUPPER(wca[i]);
 		break;
 	    }
 	    was_letter = MB_ISLOWER(c) || MB_ISUPPER(c);
 	}
@@ -608,24 +611,56 @@ ins_compl_infercase_gettext(
 	else if (MB_ISUPPER(c))
 	    wca[i] = MB_TOUPPER(wca[i]);
     }
 
     // Generate encoding specific output from wide character array.
-    // Multi-byte characters can occupy up to five bytes more than
-    // ASCII characters, and we also need one byte for NUL, so stay
-    // six bytes away from the edge of IObuff.
     p = IObuff;
     i = 0;
-    while (i < actual_len && (p - IObuff + 6) < IOSIZE)
-	if (has_mbyte)
+    ga_init2(&gap, 1, 500);
+    while (i < char_len)
+    {
+	if (gap.ga_data != NULL)
+	{
+	    if (ga_grow(&gap, 10) == FAIL)
+	    {
+		ga_clear(&gap);
+		return (char_u *)"[failed]";
+	    }
+	    p = (char_u *)gap.ga_data + gap.ga_len;
+	    if (has_mbyte)
+		gap.ga_len += (*mb_char2bytes)(wca[i++], p);
+	    else
+	    {
+		*p = wca[i++];
+		++gap.ga_len;
+	    }
+	}
+	else if ((p - IObuff) + 6 >= IOSIZE)
+	{
+	    // Multi-byte characters can occupy up to five bytes more than
+	    // ASCII characters, and we also need one byte for NUL, so when
+	    // getting to six bytes from the edge of IObuff switch to using a
+	    // growarray.  Add the character in the next round.
+	    if (ga_grow(&gap, IOSIZE) == FAIL)
+		return (char_u *)"[failed]";
+	    STRCPY(gap.ga_data, IObuff);
+	    gap.ga_len = STRLEN(IObuff);
+	}
+	else if (has_mbyte)
 	    p += (*mb_char2bytes)(wca[i++], p);
 	else
 	    *(p++) = wca[i++];
-    *p = NUL;
-
+    }
     vim_free(wca);
 
+    if (gap.ga_data != NULL)
+    {
+	*tofree = gap.ga_data;
+	return gap.ga_data;
+    }
+
+    *p = NUL;
     return IObuff;
 }
 
 /*
  * This is like ins_compl_add(), but if 'ic' and 'inf' are set, then the
@@ -642,61 +677,64 @@ ins_compl_add_infercase(
     int		dir,
     int		cont_s_ipos)  // next ^X<> will set initial_pos
 {
     char_u	*str = str_arg;
     char_u	*p;
-    int		actual_len;		// Take multi-byte characters
-    int		actual_compl_length;	// into account.
+    int		char_len;		// count multi-byte characters
+    int		compl_char_len;
     int		min_len;
     int		flags = 0;
+    int		res;
+    char_u	*tofree = NULL;
 
     if (p_ic && curbuf->b_p_inf && len > 0)
     {
 	// Infer case of completed part.
 
 	// Find actual length of completion.
 	if (has_mbyte)
 	{
 	    p = str;
-	    actual_len = 0;
+	    char_len = 0;
 	    while (*p != NUL)
 	    {
 		MB_PTR_ADV(p);
-		++actual_len;
+		++char_len;
 	    }
 	}
 	else
-	    actual_len = len;
+	    char_len = len;
 
 	// Find actual length of original text.
 	if (has_mbyte)
 	{
 	    p = compl_orig_text;
-	    actual_compl_length = 0;
+	    compl_char_len = 0;
 	    while (*p != NUL)
 	    {
 		MB_PTR_ADV(p);
-		++actual_compl_length;
+		++compl_char_len;
 	    }
 	}
 	else
-	    actual_compl_length = compl_length;
+	    compl_char_len = compl_length;
 
-	// "actual_len" may be smaller than "actual_compl_length" when using
+	// "char_len" may be smaller than "compl_char_len" when using
 	// thesaurus, only use the minimum when comparing.
-	min_len = actual_len < actual_compl_length
-					   ? actual_len : actual_compl_length;
+	min_len = char_len < compl_char_len ? char_len : compl_char_len;
 
-	str = ins_compl_infercase_gettext(str, actual_len, actual_compl_length,
-								min_len);
+	str = ins_compl_infercase_gettext(str, char_len,
+					  compl_char_len, min_len, &tofree);
     }
     if (cont_s_ipos)
 	flags |= CP_CONT_S_IPOS;
     if (icase)
 	flags |= CP_ICASE;
 
-    return ins_compl_add(str, len, fname, NULL, NULL, dir, flags, FALSE);
+    res = ins_compl_add(str, len, fname, NULL, NULL, dir, flags, FALSE);
+    vim_free(tofree);
+    return res;
 }
 
 /*
  * Add a match to the list of matches. The arguments are:
  *     str       - text of the match to add
diff --git a/src/testdir/test_ins_complete.vim b/src/testdir/test_ins_complete.vim
index baf9ef1711fe..5e5b1bbece68 100644
--- a/src/testdir/test_ins_complete.vim
+++ b/src/testdir/test_ins_complete.vim
@@ -2095,6 +2095,22 @@ func Test_complete_overrun()
   new
   sil norm si0s0
   bwipe!
 endfunc
 
+func Test_infercase_very_long_line()
+  " this was truncating the line when inferring case
+  new
+  let longLine = "blah "->repeat(300)
+  let verylongLine = "blah "->repeat(400)
+  call setline(1, verylongLine)
+  call setline(2, longLine)
+  set ic infercase
+  exe "normal 2Go\<C-X>\<C-L>\<Esc>"
+  call assert_equal(longLine, getline(3))
+
+  bwipe!
+  set noic noinfercase
+endfunc
+
+
 " vim: shiftwidth=2 sts=2 expandtab
diff --git a/src/version.c b/src/version.c
index 31ece0e6b2d4..75de2fc7b751 100644
--- a/src/version.c
+++ b/src/version.c
@@ -733,10 +733,12 @@ static char *(features[]) =
 	NULL
 };
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    45,
 /**/
     44,
 /**/
     43,
 /**/

Vulnerable code: 	int	actual_len,
	int	actual_compl_length,
	int	min_len)
    wca = ALLOC_MULT(int, actual_len);
    for (i = 0; i < actual_len; ++i)
		for (i = actual_compl_length; i < actual_len; ++i)
		for (i = actual_compl_length; i < actual_len; ++i)
    // Multi-byte characters can occupy up to five bytes more than
    // ASCII characters, and we also need one byte for NUL, so stay
    // six bytes away from the edge of IObuff.
    while (i < actual_len && (p - IObuff + 6) < IOSIZE)
	if (has_mbyte)
    *p = NUL;

    int		actual_len;		// Take multi-byte characters
    int		actual_compl_length;	// into account.
	    actual_len = 0;
		++actual_len;
	    actual_len = len;
	    actual_compl_length = 0;
		++actual_compl_length;
	    actual_compl_length = compl_length;
	// "actual_len" may be smaller than "actual_compl_length" when using
	min_len = actual_len < actual_compl_length
					   ? actual_len : actual_compl_length;
	str = ins_compl_infercase_gettext(str, actual_len, actual_compl_length,
								min_len);
    return ins_compl_add(str, len, fname, NULL, NULL, dir, flags, FALSE);
Fixed code:  * If the result is in allocated memory "tofree" is set to it.
	int	char_len,
	int	compl_char_len,
	int	min_len,
	char_u  **tofree)
    garray_T	gap;
    wca = ALLOC_MULT(int, char_len);
    for (i = 0; i < char_len; ++i)
		for (i = compl_char_len; i < char_len; ++i)
		for (i = compl_char_len; i < char_len; ++i)
    ga_init2(&gap, 1, 500);
    while (i < char_len)
    {
	if (gap.ga_data != NULL)
	{
	    if (ga_grow(&gap, 10) == FAIL)
	    {
		ga_clear(&gap);
		return (char_u *)"[failed]";
	    }
	    p = (char_u *)gap.ga_data + gap.ga_len;
	    if (has_mbyte)
		gap.ga_len += (*mb_char2bytes)(wca[i++], p);
	    else
	    {
		*p = wca[i++];
		++gap.ga_len;
	    }
	}
	else if ((p - IObuff) + 6 >= IOSIZE)
	{
	    // Multi-byte characters can occupy up to five bytes more than
	    // ASCII characters, and we also need one byte for NUL, so when
	    // getting to six bytes from the edge of IObuff switch to using a
	    // growarray.  Add the character in the next round.
	    if (ga_grow(&gap, IOSIZE) == FAIL)
		return (char_u *)"[failed]";
	    STRCPY(gap.ga_data, IObuff);
	    gap.ga_len = STRLEN(IObuff);
	}
	else if (has_mbyte)
    }
    if (gap.ga_data != NULL)
    {
	*tofree = gap.ga_data;
	return gap.ga_data;
    }

    *p = NUL;
    int		char_len;		// count multi-byte characters
    int		compl_char_len;
    int		res;
    char_u	*tofree = NULL;
	    char_len = 0;
		++char_len;
	    char_len = len;
	    compl_char_len = 0;
		++compl_char_len;
	    compl_char_len = compl_length;
	// "char_len" may be smaller than "compl_char_len" when using
	min_len = char_len < compl_char_len ? char_len : compl_char_len;
	str = ins_compl_infercase_gettext(str, char_len,
					  compl_char_len, min_len, &tofree);
    res = ins_compl_add(str, len, fname, NULL, NULL, dir, flags, FALSE);
    vim_free(tofree);
    return res;
func Test_infercase_very_long_line()
  " this was truncating the line when inferring case
  new
  let longLine = "blah "->repeat(300)
  let verylongLine = "blah "->repeat(400)
  call setline(1, verylongLine)
  call setline(2, longLine)
  set ic infercase
  exe "normal 2Go\<C-X>\<C-L>\<Esc>"
  call assert_equal(longLine, getline(3))

  bwipe!
  set noic noinfercase
endfunc


/**/
    45,