CVE-2022-2231: NULL Pointer Dereference (CWE-476)
value: NULL Pointer Dereference in GitHub repository vim/vim prior to 8.2.
CWE description: The product dereferences a pointer that it expects to be valid but is NULL.
CVSS v3 base score: 5.5
Published: 2022-06-28T20:15Z
Repository: https://github.com/vim/vim
Language: C
Files changed: src/eval.c, src/testdir/test_vimscript.vim, src/version.c
Commit date: 2022-06-27 20:15:10 +0100
Commit message: patch 8.2.5169: nested :source may use NULL pointer

Problem:    Nested :source may use NULL pointer.
Solution:   Do not use the NULL pointer.

Diff: diff --git a/src/eval.c b/src/eval.c
index dce78f7f041..42b883e9b00 100644
--- a/src/eval.c
+++ b/src/eval.c
@@ -2385,31 +2385,36 @@ eval0_retarg(
     int		check_for_end = retarg == NULL;
     int		end_error = FALSE;
 
     p = skipwhite(arg);
     ret = eval1(&p, rettv, evalarg);
-    expr_end = p;
-    p = skipwhite(p);
 
-    // In Vim9 script a command block is not split at NL characters for
-    // commands using an expression argument.  Skip over a '#' comment to check
-    // for a following NL.  Require white space before the '#'.
-    if (in_vim9script() && p > expr_end && retarg == NULL)
-	while (*p == '#')
-	{
-	    char_u *nl = vim_strchr(p, NL);
+    if (ret != FAIL)
+    {
+	expr_end = p;
+	p = skipwhite(p);
 
-	    if (nl == NULL)
-		break;
-	    p = skipwhite(nl + 1);
-	    if (eap != NULL && *p != NUL)
-		eap->nextcmd = p;
-	    check_for_end = FALSE;
-	}
+	// In Vim9 script a command block is not split at NL characters for
+	// commands using an expression argument.  Skip over a '#' comment to
+	// check for a following NL.  Require white space before the '#'.
+	if (in_vim9script() && p > expr_end && retarg == NULL)
+	    while (*p == '#')
+	    {
+		char_u *nl = vim_strchr(p, NL);
+
+		if (nl == NULL)
+		    break;
+		p = skipwhite(nl + 1);
+		if (eap != NULL && *p != NUL)
+		    eap->nextcmd = p;
+		check_for_end = FALSE;
+	    }
+
+	if (check_for_end)
+	    end_error = !ends_excmd2(arg, p);
+    }
 
-    if (ret != FAIL && check_for_end)
-	end_error = !ends_excmd2(arg, p);
     if (ret == FAIL || end_error)
     {
 	if (ret != FAIL)
 	    clear_tv(rettv);
 	/*
@@ -2431,11 +2436,12 @@ eval0_retarg(
 	}
 
 	// Some of the expression may not have been consumed.  Do not check for
 	// a next command to avoid more errors, unless "|" is following, which
 	// could only be a command separator.
-	if (eap != NULL && skipwhite(p)[0] == '|' && skipwhite(p)[1] != '|')
+	if (eap != NULL && p != NULL
+			  &&  skipwhite(p)[0] == '|' && skipwhite(p)[1] != '|')
 	    eap->nextcmd = check_nextcmd(p);
 	return FAIL;
     }
 
     if (retarg != NULL)
diff --git a/src/testdir/test_vimscript.vim b/src/testdir/test_vimscript.vim
index c9f58c23976..07dc59d15a1 100644
--- a/src/testdir/test_vimscript.vim
+++ b/src/testdir/test_vimscript.vim
@@ -7526,9 +7526,28 @@ func Test_for_over_string()
     let res ..= c .. '-'
   endfor
   call assert_equal('', res)
 endfunc
 
+" Test for deeply nested :source command  {{{1
+func Test_deeply_nested_source()
+  let lines =<< trim END
+
+      so
+      sil 0scr
+      delete
+      so
+      0
+  END
+  call writefile(["vim9 silent! @0 \n/"] + lines, 'Xnested.vim')
+
+  " this must not crash
+  let cmd = GetVimCommand() .. " -e -s -S Xnested.vim -c qa!"
+  call system(cmd)
+
+  call delete('Xnested.vim')
+endfunc
+
 "-------------------------------------------------------------------------------
 " Modelines								    {{{1
 " vim: ts=8 sw=2 sts=2 expandtab tw=80 fdm=marker
 "-------------------------------------------------------------------------------
diff --git a/src/version.c b/src/version.c
index 5ac1cb93d0a..4a7e83f9c45 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 */
+/**/
+    5169,
 /**/
     5168,
 /**/
     5167,
 /**/

Vulnerable code:     expr_end = p;
    p = skipwhite(p);
    // In Vim9 script a command block is not split at NL characters for
    // commands using an expression argument.  Skip over a '#' comment to check
    // for a following NL.  Require white space before the '#'.
    if (in_vim9script() && p > expr_end && retarg == NULL)
	while (*p == '#')
	{
	    char_u *nl = vim_strchr(p, NL);
	    if (nl == NULL)
		break;
	    p = skipwhite(nl + 1);
	    if (eap != NULL && *p != NUL)
		eap->nextcmd = p;
	    check_for_end = FALSE;
	}
    if (ret != FAIL && check_for_end)
	end_error = !ends_excmd2(arg, p);
	if (eap != NULL && skipwhite(p)[0] == '|' && skipwhite(p)[1] != '|')
Fixed code:     if (ret != FAIL)
    {
	expr_end = p;
	p = skipwhite(p);
	// In Vim9 script a command block is not split at NL characters for
	// commands using an expression argument.  Skip over a '#' comment to
	// check for a following NL.  Require white space before the '#'.
	if (in_vim9script() && p > expr_end && retarg == NULL)
	    while (*p == '#')
	    {
		char_u *nl = vim_strchr(p, NL);

		if (nl == NULL)
		    break;
		p = skipwhite(nl + 1);
		if (eap != NULL && *p != NUL)
		    eap->nextcmd = p;
		check_for_end = FALSE;
	    }

	if (check_for_end)
	    end_error = !ends_excmd2(arg, p);
    }
	if (eap != NULL && p != NULL
			  &&  skipwhite(p)[0] == '|' && skipwhite(p)[1] != '|')
" Test for deeply nested :source command  {{{1
func Test_deeply_nested_source()
  let lines =<< trim END

      so
      sil 0scr
      delete
      so
      0
  END
  call writefile(["vim9 silent! @0 \n/"] + lines, 'Xnested.vim')

  " this must not crash
  let cmd = GetVimCommand() .. " -e -s -S Xnested.vim -c qa!"
  call system(cmd)

  call delete('Xnested.vim')
endfunc

/**/
    5169,