CVE-2023-4750: Use After Free (CWE-416)
value: Use After Free in GitHub repository vim/vim prior to 9.0.1857.
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: 2023-09-04T14:15Z
Repository: https://github.com/vim/vim
Language: C
Files changed: src/main.c, src/quickfix.c, src/testdir/crash/bt_quickfix_poc, src/testdir/test_crash.vim, src/version.c
Commit date: 2023-09-03 20:20:52 +0200
Commit message: patch 9.0.1857: [security] heap-use-after-free in is_qf_win()

Problem:  heap-use-after-free in is_qf_win()
Solution: Check buffer is valid before accessing it

Signed-off-by: Christian Brabandt <cb@256bit.org>

Diff: diff --git a/src/main.c b/src/main.c
index cca53fe256d5..a40c3a66606e 100644
--- a/src/main.c
+++ b/src/main.c
@@ -1644,11 +1644,11 @@ getout(int exitval)
 	for (tp = first_tabpage; tp != NULL; tp = next_tp)
 	{
 	    next_tp = tp->tp_next;
 	    FOR_ALL_WINDOWS_IN_TAB(tp, wp)
 	    {
-		if (wp->w_buffer == NULL)
+		if (wp->w_buffer == NULL || !buf_valid(wp->w_buffer))
 		    // Autocmd must have close the buffer already, skip.
 		    continue;
 		buf = wp->w_buffer;
 		if (CHANGEDTICK(buf) != -1)
 		{
diff --git a/src/quickfix.c b/src/quickfix.c
index aa431ea10578..a3d3e8fb7914 100644
--- a/src/quickfix.c
+++ b/src/quickfix.c
@@ -4490,11 +4490,11 @@ is_qf_win(win_T *win, qf_info_T *qi)
 {
     // A window displaying the quickfix buffer will have the w_llist_ref field
     // set to NULL.
     // A window displaying a location list buffer will have the w_llist_ref
     // pointing to the location list.
-    if (bt_quickfix(win->w_buffer))
+    if (buf_valid(win->w_buffer) && bt_quickfix(win->w_buffer))
 	if ((IS_QF_STACK(qi) && win->w_llist_ref == NULL)
 		|| (IS_LL_STACK(qi) && win->w_llist_ref == qi))
 	    return TRUE;
 
     return FALSE;
diff --git a/src/testdir/crash/bt_quickfix_poc b/src/testdir/crash/bt_quickfix_poc
new file mode 100644
index 000000000000..bf02b4dcb817
--- /dev/null
+++ b/src/testdir/crash/bt_quickfix_poc
@@ -0,0 +1,9 @@
+comman!-narg=* Xexpr <mods>lex<args>
+auto BufReadPre * exe"sn" ..expand("<abuf>") 
+fu Xautocmd_changelist()
+cal writefile(['Xtestfile2:4:4'],'Xerr')
+  sil! edi Xerr
+Xexpr 'Xtestfile:4:4'
+endf
+call Xautocmd_changelist()
+call Xautocmd_changelist()
\ No newline at end of file
diff --git a/src/testdir/test_crash.vim b/src/testdir/test_crash.vim
index eb3c0a37fbc6..516d99193962 100644
--- a/src/testdir/test_crash.vim
+++ b/src/testdir/test_crash.vim
@@ -3,42 +3,62 @@ source check.vim
 source screendump.vim
 
 CheckScreendump
 
 func Test_crash1()
+  if !executable('sh')
+    throw 'Skipped: sh not executable!'
+  endif
   " The following used to crash Vim
-  " let opts = #{wait_for_ruler: 0, rows: 20, cmd: 'sh'}
   let opts = #{cmd: 'sh'}
-  let args = 'bash'
   let vim  = GetVimProg()
 
-  let buf = RunVimInTerminal(args, opts)
+  let buf = RunVimInTerminal('sh', opts)
 
   let file = 'crash/poc_huaf1'
   let cmn_args = "%s -u NONE -i NONE -n -e -s -S %s -c ':qa!'"
   let args = printf(cmn_args, vim, file)
   call term_sendkeys(buf, args ..
-    \ '  && echo "crash 1: [OK]" >> X_crash1_result.txt' .. "\<cr>")
+    \ '  && echo "crash 1: [OK]" > X_crash1_result.txt' .. "\<cr>")
+  call TermWait(buf, 50)
 
   let file = 'crash/poc_huaf2'
   let args = printf(cmn_args, vim, file)
   call term_sendkeys(buf, args ..
     \ '  && echo "crash 2: [OK]" >> X_crash1_result.txt' .. "\<cr>")
+  call TermWait(buf, 50)
 
   let file = 'crash/poc_huaf3'
   let args = printf(cmn_args, vim, file)
   call term_sendkeys(buf, args ..
     \ '  && echo "crash 3: [OK]" >> X_crash1_result.txt' .. "\<cr>")
+  call TermWait(buf, 100)
 
-  call TermWait(buf, 50)
+  let file = 'crash/bt_quickfix_poc'
+  let args = printf(cmn_args, vim, file)
+  call term_sendkeys(buf, args ..
+    \ '  && echo "crash 4: [OK]" >> X_crash1_result.txt' .. "\<cr>")
+  " clean up
+  call delete('Xerr')
+
+  " This test takes a bit longer
+  call TermWait(buf, 200)
 
   " clean up
+  call delete('Xerr')
   exe buf .. "bw!"
 
   sp X_crash1_result.txt
-  call assert_equal(['crash 1: [OK]', 'crash 2: [OK]', 'crash 3: [OK]'],
-    \ getline(1, '$'))
+
+  let expected = [
+      \ 'crash 1: [OK]',
+      \ 'crash 2: [OK]',
+      \ 'crash 3: [OK]',
+      \ 'crash 4: [OK]',
+      \ ]
+
+  call assert_equal(expected, getline(1, '$'))
   bw!
 
   call delete('X_crash1_result.txt')
 endfunc
 
diff --git a/src/version.c b/src/version.c
index 98964fbbd179..13d5d695a69f 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 */
+/**/
+    1857,
 /**/
     1856,
 /**/
     1855,
 /**/

Vulnerable code: 		if (wp->w_buffer == NULL)
    if (bt_quickfix(win->w_buffer))
  " let opts = #{wait_for_ruler: 0, rows: 20, cmd: 'sh'}
  let args = 'bash'
  let buf = RunVimInTerminal(args, opts)
    \ '  && echo "crash 1: [OK]" >> X_crash1_result.txt' .. "\<cr>")
  call TermWait(buf, 50)
  call assert_equal(['crash 1: [OK]', 'crash 2: [OK]', 'crash 3: [OK]'],
    \ getline(1, '$'))
Fixed code: 		if (wp->w_buffer == NULL || !buf_valid(wp->w_buffer))
    if (buf_valid(win->w_buffer) && bt_quickfix(win->w_buffer))
comman!-narg=* Xexpr <mods>lex<args>
auto BufReadPre * exe"sn" ..expand("<abuf>") 
fu Xautocmd_changelist()
cal writefile(['Xtestfile2:4:4'],'Xerr')
  sil! edi Xerr
Xexpr 'Xtestfile:4:4'
endf
call Xautocmd_changelist()
call Xautocmd_changelist()
  if !executable('sh')
    throw 'Skipped: sh not executable!'
  endif
  let buf = RunVimInTerminal('sh', opts)
    \ '  && echo "crash 1: [OK]" > X_crash1_result.txt' .. "\<cr>")
  call TermWait(buf, 50)
  call TermWait(buf, 50)
  call TermWait(buf, 100)
  let file = 'crash/bt_quickfix_poc'
  let args = printf(cmn_args, vim, file)
  call term_sendkeys(buf, args ..
    \ '  && echo "crash 4: [OK]" >> X_crash1_result.txt' .. "\<cr>")
  " clean up
  call delete('Xerr')

  " This test takes a bit longer
  call TermWait(buf, 200)
  call delete('Xerr')

  let expected = [
      \ 'crash 1: [OK]',
      \ 'crash 2: [OK]',
      \ 'crash 3: [OK]',
      \ 'crash 4: [OK]',
      \ ]

  call assert_equal(expected, getline(1, '$'))
/**/
    1857,