CVE-2022-3297: Use After Free (CWE-416)
value: Use After Free in GitHub repository vim/vim prior to 9.0.0579.
CWE description: The product reuses or references memory after it has been freed. At some point afterward, the memory may be allocated again and saved in another pointer, while the original pointer references a location somewhere within the new allocation. Any operations using the original pointer are no longer valid because the memory "belongs" to the code that operates on the new pointer.
CVSS v3 base score: 7.8
Published: 2022-09-25T19:15Z
Repository: https://github.com/vim/vim
Language: C
Files changed: src/insexpand.c, src/move.c, src/testdir/test_ins_complete.vim, src/version.c
Commit date: 2022-09-24 19:20:30 +0100
Commit message: patch 9.0.0579: using freed memory when 'tagfunc' wipes out buffer

Problem:    Using freed memory when 'tagfunc' wipes out buffer that holds
            'complete'.
Solution:   Make a copy of the option.  Make sure cursor position is valid.

Diff: diff --git a/src/insexpand.c b/src/insexpand.c
index 37162a48ce5a..d3a1f881ea1f 100644
--- a/src/insexpand.c
+++ b/src/insexpand.c
@@ -2488,11 +2488,12 @@ ins_compl_next_buf(buf_T *buf, int flag)
 {
     static win_T *wp = NULL;
 
     if (flag == 'w')		// just windows
     {
-	if (buf == curbuf || wp == NULL)  // first call for this flag/expansion
+	if (buf == curbuf || !win_valid(wp))
+	    // first call for this flag/expansion or window was closed
 	    wp = curwin;
 	while ((wp = (wp->w_next != NULL ? wp->w_next : firstwin)) != curwin
 		&& wp->w_buffer->b_scanned)
 	    ;
 	buf = wp->w_buffer;
@@ -3186,13 +3187,14 @@ enum
  * state information used for getting the next set of insert completion
  * matches.
  */
 typedef struct
 {
-    char_u	*e_cpt;			// current entry in 'complete'
+    char_u	*e_cpt_copy;		// copy of 'complete'
+    char_u	*e_cpt;			// current entry in "e_cpt_copy"
     buf_T	*ins_buf;		// buffer being scanned
-    pos_T	*cur_match_pos;			// current match position
+    pos_T	*cur_match_pos;		// current match position
     pos_T	prev_match_pos;		// previous match position
     int		set_match_pos;		// save first_match_pos/last_match_pos
     pos_T	first_match_pos;	// first match position
     pos_T	last_match_pos;		// last match position
     int		found_all;		// found all matches of a certain type.
@@ -3255,11 +3257,12 @@ process_next_cpt_value(
 	// Remember the first match so that the loop stops when we
 	// wrap and come back there a second time.
 	st->set_match_pos = TRUE;
     }
     else if (vim_strchr((char_u *)"buwU", *st->e_cpt) != NULL
-	    && (st->ins_buf = ins_compl_next_buf(st->ins_buf, *st->e_cpt)) != curbuf)
+	    && (st->ins_buf = ins_compl_next_buf(
+					   st->ins_buf, *st->e_cpt)) != curbuf)
     {
 	// Scan a buffer, but not the current one.
 	if (st->ins_buf->b_ml.ml_mfp != NULL)   // loaded buffer
 	{
 	    compl_started = TRUE;
@@ -3754,23 +3757,34 @@ get_next_completion_match(int type, ins_compl_next_state_T *st, pos_T *ini)
  * Return the total number of matches or -1 if still unknown -- Acevedo
  */
     static int
 ins_compl_get_exp(pos_T *ini)
 {
-    static ins_compl_next_state_T st;
+    static ins_compl_next_state_T   st;
+    static int			    st_cleared = FALSE;
     int		i;
     int		found_new_match;
     int		type = ctrl_x_mode;
 
     if (!compl_started)
     {
-	FOR_ALL_BUFFERS(st.ins_buf)
-	    st.ins_buf->b_scanned = 0;
+	buf_T *buf;
+
+	FOR_ALL_BUFFERS(buf)
+	    buf->b_scanned = 0;
+	if (!st_cleared)
+	{
+	    CLEAR_FIELD(st);
+	    st_cleared = TRUE;
+	}
 	st.found_all = FALSE;
 	st.ins_buf = curbuf;
-	st.e_cpt = (compl_cont_status & CONT_LOCAL)
-					    ? (char_u *)"." : curbuf->b_p_cpt;
+	vim_free(st.e_cpt_copy);
+	// Make a copy of 'complete', if case the buffer is wiped out.
+	st.e_cpt_copy = vim_strsave((compl_cont_status & CONT_LOCAL)
+					    ? (char_u *)"." : curbuf->b_p_cpt);
+	st.e_cpt = st.e_cpt_copy == NULL ? (char_u *)"" : st.e_cpt_copy;
 	st.last_match_pos = st.first_match_pos = *ini;
     }
     else if (st.ins_buf != curbuf && !buf_valid(st.ins_buf))
 	st.ins_buf = curbuf;  // In case the buffer was wiped out.
 
@@ -4110,10 +4124,11 @@ ins_compl_next(
 {
     int	    num_matches = -1;
     int	    todo = count;
     int	    advance;
     int	    started = compl_started;
+    buf_T   *orig_curbuf = curbuf;
 
     // When user complete function return -1 for findstart which is next
     // time of 'always', compl_shown_match become NULL.
     if (compl_shown_match == NULL)
 	return -1;
@@ -4142,10 +4157,17 @@ ins_compl_next(
     // around.
     if (find_next_completion_match(allow_get_expansion, todo, advance,
 							&num_matches) == -1)
 	return -1;
 
+    if (curbuf != orig_curbuf)
+    {
+	// In case some completion function switched buffer, don't want to
+	// insert the completion elsewhere.
+	return -1;
+    }
+
     // Insert the text of the new completion, or the compl_leader.
     if (compl_no_insert && !started)
     {
 	ins_bytes(compl_orig_text + get_compl_len());
 	compl_used_match = FALSE;
diff --git a/src/move.c b/src/move.c
index a51d2d128e86..967dd77fb971 100644
--- a/src/move.c
+++ b/src/move.c
@@ -681,10 +681,11 @@ cursor_valid(void)
  * w_topline must be valid, you may need to call update_topline() first!
  */
     void
 validate_cursor(void)
 {
+    check_cursor();
     check_cursor_moved(curwin);
     if ((curwin->w_valid & (VALID_WCOL|VALID_WROW)) != (VALID_WCOL|VALID_WROW))
 	curs_columns(TRUE);
 }
 
diff --git a/src/testdir/test_ins_complete.vim b/src/testdir/test_ins_complete.vim
index 379db7fe7965..3fc55ec1cdc9 100644
--- a/src/testdir/test_ins_complete.vim
+++ b/src/testdir/test_ins_complete.vim
@@ -545,13 +545,12 @@ func Test_pum_with_preview_win()
       set completeopt+=longest
   END
 
   call writefile(lines, 'Xpreviewscript')
   let buf = RunVimInTerminal('-S Xpreviewscript', #{rows: 12})
-  call TermWait(buf, 50)
   call term_sendkeys(buf, "Gi\<C-X>\<C-O>")
-  call TermWait(buf, 100)
+  call TermWait(buf, 200)
   call term_sendkeys(buf, "\<C-N>")
   call VerifyScreenDump(buf, 'Test_pum_with_preview_win', {})
 
   call term_sendkeys(buf, "\<Esc>")
   call StopVimInTerminal(buf)
@@ -2170,6 +2169,23 @@ func Test_ins_complete_end_of_line()
   sil! norm o
 
   bwipe!
 endfunc
 
+func s:Tagfunc(t,f,o)
+  bwipe!
+  return []
+endfunc
+
+" This was using freed memory, since 'complete' was in a wiped out buffer.
+" Also using a window that was closed.
+func Test_tagfunc_wipes_out_buffer()
+  new
+  set complete=.,t,w,b,u,i
+  se tagfunc=s:Tagfunc
+  sil norm i
+
+  bwipe!
+endfunc
+
+
 " vim: shiftwidth=2 sts=2 expandtab
diff --git a/src/version.c b/src/version.c
index ff8264415d60..1061e4c67fa6 100644
--- a/src/version.c
+++ b/src/version.c
@@ -697,10 +697,12 @@ static char *(features[]) =
 	NULL
 };
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    579,
 /**/
     578,
 /**/
     577,
 /**/

Vulnerable code: 	if (buf == curbuf || wp == NULL)  // first call for this flag/expansion
    char_u	*e_cpt;			// current entry in 'complete'
    pos_T	*cur_match_pos;			// current match position
	    && (st->ins_buf = ins_compl_next_buf(st->ins_buf, *st->e_cpt)) != curbuf)
    static ins_compl_next_state_T st;
	FOR_ALL_BUFFERS(st.ins_buf)
	    st.ins_buf->b_scanned = 0;
	st.e_cpt = (compl_cont_status & CONT_LOCAL)
					    ? (char_u *)"." : curbuf->b_p_cpt;
  call TermWait(buf, 50)
  call TermWait(buf, 100)
Fixed code: 	if (buf == curbuf || !win_valid(wp))
	    // first call for this flag/expansion or window was closed
    char_u	*e_cpt_copy;		// copy of 'complete'
    char_u	*e_cpt;			// current entry in "e_cpt_copy"
    pos_T	*cur_match_pos;		// current match position
	    && (st->ins_buf = ins_compl_next_buf(
					   st->ins_buf, *st->e_cpt)) != curbuf)
    static ins_compl_next_state_T   st;
    static int			    st_cleared = FALSE;
	buf_T *buf;

	FOR_ALL_BUFFERS(buf)
	    buf->b_scanned = 0;
	if (!st_cleared)
	{
	    CLEAR_FIELD(st);
	    st_cleared = TRUE;
	}
	vim_free(st.e_cpt_copy);
	// Make a copy of 'complete', if case the buffer is wiped out.
	st.e_cpt_copy = vim_strsave((compl_cont_status & CONT_LOCAL)
					    ? (char_u *)"." : curbuf->b_p_cpt);
	st.e_cpt = st.e_cpt_copy == NULL ? (char_u *)"" : st.e_cpt_copy;
    buf_T   *orig_curbuf = curbuf;
    if (curbuf != orig_curbuf)
    {
	// In case some completion function switched buffer, don't want to
	// insert the completion elsewhere.
	return -1;
    }

    check_cursor();
  call TermWait(buf, 200)
func s:Tagfunc(t,f,o)
  bwipe!
  return []
endfunc

" This was using freed memory, since 'complete' was in a wiped out buffer.
" Also using a window that was closed.
func Test_tagfunc_wipes_out_buffer()
  new
  set complete=.,t,w,b,u,i
  se tagfunc=s:Tagfunc
  sil norm i

  bwipe!
endfunc


/**/
    579,