CVE-2022-3296: Stack-based Buffer Overflow (CWE-121)
value: Stack-based Buffer Overflow in GitHub repository vim/vim prior to 9.0.0577.
CWE description: A stack-based buffer overflow condition is a condition where the buffer being overwritten is allocated on the stack (i.e., is a local variable or, rarely, a parameter to a function).
CVSS v3 base score: 7.8
Published: 2022-09-25T17:15Z
Repository: https://github.com/vim/vim
Language: C
Files changed: src/ex_eval.c, src/testdir/test_trycatch.vim, src/version.c
Commit date: 2022-09-24 17:24:12 +0100
Commit message: patch 9.0.0577: buffer underflow with unexpected :finally

Problem:    Buffer underflow with unexpected :finally.
Solution:   Check CSF_TRY can be found.

Diff: diff --git a/src/ex_eval.c b/src/ex_eval.c
index 5721b766ee54..77d6e8bb9811 100644
--- a/src/ex_eval.c
+++ b/src/ex_eval.c
@@ -1933,132 +1933,131 @@ ex_finally(exarg_T *eap)
     cstack_T	*cstack = eap->cstack;
 
     if (cmdmod_error(FALSE))
 	return;
 
-    if (cstack->cs_trylevel <= 0 || cstack->cs_idx < 0)
+    for (idx = cstack->cs_idx; idx >= 0; --idx)
+	if (cstack->cs_flags[idx] & CSF_TRY)
+	    break;
+    if (cstack->cs_trylevel <= 0 || idx < 0)
+    {
 	eap->errmsg = _(e_finally_without_try);
-    else
+	return;
+    }
+
+    if (!(cstack->cs_flags[cstack->cs_idx] & CSF_TRY))
     {
-	if (!(cstack->cs_flags[cstack->cs_idx] & CSF_TRY))
-	{
-	    eap->errmsg = get_end_emsg(cstack);
-	    for (idx = cstack->cs_idx - 1; idx > 0; --idx)
-		if (cstack->cs_flags[idx] & CSF_TRY)
-		    break;
-	    // Make this error pending, so that the commands in the following
-	    // finally clause can be executed.  This overrules also a pending
-	    // ":continue", ":break", ":return", or ":finish".
-	    pending = CSTP_ERROR;
-	}
-	else
-	    idx = cstack->cs_idx;
+	eap->errmsg = get_end_emsg(cstack);
+	// Make this error pending, so that the commands in the following
+	// finally clause can be executed.  This overrules also a pending
+	// ":continue", ":break", ":return", or ":finish".
+	pending = CSTP_ERROR;
+    }
 
-	if (cstack->cs_flags[idx] & CSF_FINALLY)
+    if (cstack->cs_flags[idx] & CSF_FINALLY)
+    {
+	// Give up for a multiple ":finally" and ignore it.
+	eap->errmsg = _(e_multiple_finally);
+	return;
+    }
+    rewind_conditionals(cstack, idx, CSF_WHILE | CSF_FOR,
+						   &cstack->cs_looplevel);
+
+    /*
+     * Don't do something when the corresponding try block never got active
+     * (because of an inactive surrounding conditional or after an error or
+     * interrupt or throw) or for a ":finally" without ":try" or a multiple
+     * ":finally".  After every other error (did_emsg or the conditional
+     * errors detected above) or after an interrupt (got_int) or an
+     * exception (did_throw), the finally clause must be executed.
+     */
+    skip = !(cstack->cs_flags[cstack->cs_idx] & CSF_TRUE);
+
+    if (!skip)
+    {
+	// When debugging or a breakpoint was encountered, display the
+	// debug prompt (if not already done).  The user then knows that the
+	// finally clause is executed.
+	if (dbg_check_skipped(eap))
 	{
-	    // Give up for a multiple ":finally" and ignore it.
-	    eap->errmsg = _(e_multiple_finally);
-	    return;
+	    // Handle a ">quit" debug command as if an interrupt had
+	    // occurred before the ":finally".  That is, discard the
+	    // original exception and replace it by an interrupt
+	    // exception.
+	    (void)do_intthrow(cstack);
 	}
-	rewind_conditionals(cstack, idx, CSF_WHILE | CSF_FOR,
-						       &cstack->cs_looplevel);
 
 	/*
-	 * Don't do something when the corresponding try block never got active
-	 * (because of an inactive surrounding conditional or after an error or
-	 * interrupt or throw) or for a ":finally" without ":try" or a multiple
-	 * ":finally".  After every other error (did_emsg or the conditional
-	 * errors detected above) or after an interrupt (got_int) or an
-	 * exception (did_throw), the finally clause must be executed.
+	 * If there is a preceding catch clause and it caught the exception,
+	 * finish the exception now.  This happens also after errors except
+	 * when this is a multiple ":finally" or one not within a ":try".
+	 * After an error or interrupt, this also discards a pending
+	 * ":continue", ":break", ":finish", or ":return" from the preceding
+	 * try block or catch clause.
 	 */
-	skip = !(cstack->cs_flags[cstack->cs_idx] & CSF_TRUE);
+	cleanup_conditionals(cstack, CSF_TRY, FALSE);
 
-	if (!skip)
+	if (cstack->cs_idx >= 0
+			   && (cstack->cs_flags[cstack->cs_idx] & CSF_TRY))
 	{
-	    // When debugging or a breakpoint was encountered, display the
-	    // debug prompt (if not already done).  The user then knows that the
-	    // finally clause is executed.
-	    if (dbg_check_skipped(eap))
-	    {
-		// Handle a ">quit" debug command as if an interrupt had
-		// occurred before the ":finally".  That is, discard the
-		// original exception and replace it by an interrupt
-		// exception.
-		(void)do_intthrow(cstack);
-	    }
-
-	    /*
-	     * If there is a preceding catch clause and it caught the exception,
-	     * finish the exception now.  This happens also after errors except
-	     * when this is a multiple ":finally" or one not within a ":try".
-	     * After an error or interrupt, this also discards a pending
-	     * ":continue", ":break", ":finish", or ":return" from the preceding
-	     * try block or catch clause.
-	     */
-	    cleanup_conditionals(cstack, CSF_TRY, FALSE);
+	    // Variables declared in the previous block can no longer be
+	    // used.
+	    leave_block(cstack);
+	    enter_block(cstack);
+	}
 
-	    if (cstack->cs_idx >= 0
-			       && (cstack->cs_flags[cstack->cs_idx] & CSF_TRY))
+	/*
+	 * Make did_emsg, got_int, did_throw pending.  If set, they overrule
+	 * a pending ":continue", ":break", ":return", or ":finish".  Then
+	 * we have particularly to discard a pending return value (as done
+	 * by the call to cleanup_conditionals() above when did_emsg or
+	 * got_int is set).  The pending values are restored by the
+	 * ":endtry", except if there is a new error, interrupt, exception,
+	 * ":continue", ":break", ":return", or ":finish" in the following
+	 * finally clause.  A missing ":endwhile", ":endfor" or ":endif"
+	 * detected here is treated as if did_emsg and did_throw had
+	 * already been set, respectively in case that the error is not
+	 * converted to an exception, did_throw had already been unset.
+	 * We must not set did_emsg here since that would suppress the
+	 * error message.
+	 */
+	if (pending == CSTP_ERROR || did_emsg || got_int || did_throw)
+	{
+	    if (cstack->cs_pending[cstack->cs_idx] == CSTP_RETURN)
 	    {
-		// Variables declared in the previous block can no longer be
-		// used.
-		leave_block(cstack);
-		enter_block(cstack);
+		report_discard_pending(CSTP_RETURN,
+				       cstack->cs_rettv[cstack->cs_idx]);
+		discard_pending_return(cstack->cs_rettv[cstack->cs_idx]);
 	    }
-
-	    /*
-	     * Make did_emsg, got_int, did_throw pending.  If set, they overrule
-	     * a pending ":continue", ":break", ":return", or ":finish".  Then
-	     * we have particularly to discard a pending return value (as done
-	     * by the call to cleanup_conditionals() above when did_emsg or
-	     * got_int is set).  The pending values are restored by the
-	     * ":endtry", except if there is a new error, interrupt, exception,
-	     * ":continue", ":break", ":return", or ":finish" in the following
-	     * finally clause.  A missing ":endwhile", ":endfor" or ":endif"
-	     * detected here is treated as if did_emsg and did_throw had
-	     * already been set, respectively in case that the error is not
-	     * converted to an exception, did_throw had already been unset.
-	     * We must not set did_emsg here since that would suppress the
-	     * error message.
-	     */
-	    if (pending == CSTP_ERROR || did_emsg || got_int || did_throw)
-	    {
-		if (cstack->cs_pending[cstack->cs_idx] == CSTP_RETURN)
-		{
-		    report_discard_pending(CSTP_RETURN,
-					   cstack->cs_rettv[cstack->cs_idx]);
-		    discard_pending_return(cstack->cs_rettv[cstack->cs_idx]);
-		}
-		if (pending == CSTP_ERROR && !did_emsg)
-		    pending |= (THROW_ON_ERROR) ? CSTP_THROW : 0;
-		else
-		    pending |= did_throw ? CSTP_THROW : 0;
-		pending |= did_emsg  ? CSTP_ERROR     : 0;
-		pending |= got_int   ? CSTP_INTERRUPT : 0;
-		cstack->cs_pending[cstack->cs_idx] = pending;
-
-		// It's mandatory that the current exception is stored in the
-		// cstack so that it can be rethrown at the ":endtry" or be
-		// discarded if the finally clause is left by a ":continue",
-		// ":break", ":return", ":finish", error, interrupt, or another
-		// exception.  When emsg() is called for a missing ":endif" or
-		// a missing ":endwhile"/":endfor" detected here, the
-		// exception will be discarded.
-		if (did_throw && cstack->cs_exception[cstack->cs_idx]
-							 != current_exception)
-		    internal_error("ex_finally()");
-	    }
-
-	    /*
-	     * Set CSL_HAD_FINA, so do_cmdline() will reset did_emsg,
-	     * got_int, and did_throw and make the finally clause active.
-	     * This will happen after emsg() has been called for a missing
-	     * ":endif" or a missing ":endwhile"/":endfor" detected here, so
-	     * that the following finally clause will be executed even then.
-	     */
-	    cstack->cs_lflags |= CSL_HAD_FINA;
+	    if (pending == CSTP_ERROR && !did_emsg)
+		pending |= (THROW_ON_ERROR) ? CSTP_THROW : 0;
+	    else
+		pending |= did_throw ? CSTP_THROW : 0;
+	    pending |= did_emsg  ? CSTP_ERROR     : 0;
+	    pending |= got_int   ? CSTP_INTERRUPT : 0;
+	    cstack->cs_pending[cstack->cs_idx] = pending;
+
+	    // It's mandatory that the current exception is stored in the
+	    // cstack so that it can be rethrown at the ":endtry" or be
+	    // discarded if the finally clause is left by a ":continue",
+	    // ":break", ":return", ":finish", error, interrupt, or another
+	    // exception.  When emsg() is called for a missing ":endif" or
+	    // a missing ":endwhile"/":endfor" detected here, the
+	    // exception will be discarded.
+	    if (did_throw && cstack->cs_exception[cstack->cs_idx]
+						     != current_exception)
+		internal_error("ex_finally()");
 	}
+
+	/*
+	 * Set CSL_HAD_FINA, so do_cmdline() will reset did_emsg,
+	 * got_int, and did_throw and make the finally clause active.
+	 * This will happen after emsg() has been called for a missing
+	 * ":endif" or a missing ":endwhile"/":endfor" detected here, so
+	 * that the following finally clause will be executed even then.
+	 */
+	cstack->cs_lflags |= CSL_HAD_FINA;
     }
 }
 
 /*
  * ":endtry"
@@ -2074,189 +2073,187 @@ ex_endtry(exarg_T *eap)
     cstack_T	*cstack = eap->cstack;
 
     if (cmdmod_error(FALSE))
 	return;
 
-    if (cstack->cs_trylevel <= 0 || cstack->cs_idx < 0)
-	eap->errmsg = _(e_endtry_without_try);
-    else
+    for (idx = cstack->cs_idx; idx >= 0; --idx)
+	if (cstack->cs_flags[idx] & CSF_TRY)
+	    break;
+    if (cstack->cs_trylevel <= 0 || idx < 0)
     {
-	/*
-	 * Don't do something after an error, interrupt or throw in the try
-	 * block, catch clause, or finally clause preceding this ":endtry" or
-	 * when an error or interrupt occurred after a ":continue", ":break",
-	 * ":return", or ":finish" in a try block or catch clause preceding this
-	 * ":endtry" or when the try block never got active (because of an
-	 * inactive surrounding conditional or after an error or interrupt or
-	 * throw) or when there is a surrounding conditional and it has been
-	 * made inactive by a ":continue", ":break", ":return", or ":finish" in
-	 * the finally clause.  The latter case need not be tested since then
-	 * anything pending has already been discarded. */
-	skip = did_emsg || got_int || did_throw
+	eap->errmsg = _(e_endtry_without_try);
+	return;
+    }
+
+    /*
+     * Don't do something after an error, interrupt or throw in the try
+     * block, catch clause, or finally clause preceding this ":endtry" or
+     * when an error or interrupt occurred after a ":continue", ":break",
+     * ":return", or ":finish" in a try block or catch clause preceding this
+     * ":endtry" or when the try block never got active (because of an
+     * inactive surrounding conditional or after an error or interrupt or
+     * throw) or when there is a surrounding conditional and it has been
+     * made inactive by a ":continue", ":break", ":return", or ":finish" in
+     * the finally clause.  The latter case need not be tested since then
+     * anything pending has already been discarded. */
+    skip = did_emsg || got_int || did_throw
 			     || !(cstack->cs_flags[cstack->cs_idx] & CSF_TRUE);
 
-	if (!(cstack->cs_flags[cstack->cs_idx] & CSF_TRY))
-	{
-	    eap->errmsg = get_end_emsg(cstack);
+    if (!(cstack->cs_flags[cstack->cs_idx] & CSF_TRY))
+    {
+	eap->errmsg = get_end_emsg(cstack);
 
-	    // Find the matching ":try" and report what's missing.
-	    idx = cstack->cs_idx;
-	    do
-		--idx;
-	    while (idx > 0 && !(cstack->cs_flags[idx] & CSF_TRY));
-	    rewind_conditionals(cstack, idx, CSF_WHILE | CSF_FOR,
-						       &cstack->cs_looplevel);
-	    skip = TRUE;
+	// Find the matching ":try" and report what's missing.
+	rewind_conditionals(cstack, idx, CSF_WHILE | CSF_FOR,
+							&cstack->cs_looplevel);
+	skip = TRUE;
 
-	    /*
-	     * If an exception is being thrown, discard it to prevent it from
-	     * being rethrown at the end of this function.  It would be
-	     * discarded by the error message, anyway.  Resets did_throw.
-	     * This does not affect the script termination due to the error
-	     * since "trylevel" is decremented after emsg() has been called.
-	     */
-	    if (did_throw)
-		discard_current_exception();
+	/*
+	 * If an exception is being thrown, discard it to prevent it from
+	 * being rethrown at the end of this function.  It would be
+	 * discarded by the error message, anyway.  Resets did_throw.
+	 * This does not affect the script termination due to the error
+	 * since "trylevel" is decremented after emsg() has been called.
+	 */
+	if (did_throw)
+	    discard_current_exception();
 
-	    // report eap->errmsg, also when there already was an error
-	    did_emsg = FALSE;
-	}
-	else
-	{
-	    idx = cstack->cs_idx;
+	// report eap->errmsg, also when there already was an error
+	did_emsg = FALSE;
+    }
+    else
+    {
+	idx = cstack->cs_idx;
 
-	    // Check the flags only when not in a skipped block.
-	    if (!skip && in_vim9script()
+	// Check the flags only when not in a skipped block.
+	if (!skip && in_vim9script()
 		     && (cstack->cs_flags[idx] & (CSF_CATCH|CSF_FINALLY)) == 0)
-	    {
-		// try/endtry without any catch or finally: give an error and
-		// continue.
-		eap->errmsg = _(e_missing_catch_or_finally);
-	    }
-
-	    /*
-	     * If we stopped with the exception currently being thrown at this
-	     * try conditional since we didn't know that it doesn't have
-	     * a finally clause, we need to rethrow it after closing the try
-	     * conditional.
-	     */
-	    if (did_throw && (cstack->cs_flags[idx] & CSF_TRUE)
-		    && !(cstack->cs_flags[idx] & CSF_FINALLY))
-		rethrow = TRUE;
-	}
-
-	// If there was no finally clause, show the user when debugging or
-	// a breakpoint was encountered that the end of the try conditional has
-	// been reached: display the debug prompt (if not already done).  Do
-	// this on normal control flow or when an exception was thrown, but not
-	// on an interrupt or error not converted to an exception or when
-	// a ":break", ":continue", ":return", or ":finish" is pending.  These
-	// actions are carried out immediately.
-	if ((rethrow || (!skip
-			&& !(cstack->cs_flags[idx] & CSF_FINALLY)
-			&& !cstack->cs_pending[idx]))
-		&& dbg_check_skipped(eap))
 	{
-	    // Handle a ">quit" debug command as if an interrupt had occurred
-	    // before the ":endtry".  That is, throw an interrupt exception and
-	    // set "skip" and "rethrow".
-	    if (got_int)
-	    {
-		skip = TRUE;
-		(void)do_intthrow(cstack);
-		// The do_intthrow() call may have reset did_throw or
-		// cstack->cs_pending[idx].
-		rethrow = FALSE;
-		if (did_throw && !(cstack->cs_flags[idx] & CSF_FINALLY))
-		    rethrow = TRUE;
-	    }
+	    // try/endtry without any catch or finally: give an error and
+	    // continue.
+	    eap->errmsg = _(e_missing_catch_or_finally);
 	}
 
 	/*
-	 * If a ":return" is pending, we need to resume it after closing the
-	 * try conditional; remember the return value.  If there was a finally
-	 * clause making an exception pending, we need to rethrow it.  Make it
-	 * the exception currently being thrown.
+	 * If we stopped with the exception currently being thrown at this
+	 * try conditional since we didn't know that it doesn't have
+	 * a finally clause, we need to rethrow it after closing the try
+	 * conditional.
 	 */
-	if (!skip)
+	if (did_throw && (cstack->cs_flags[idx] & CSF_TRUE)
+		&& !(cstack->cs_flags[idx] & CSF_FINALLY))
+	    rethrow = TRUE;
+    }
+
+    // If there was no finally clause, show the user when debugging or
+    // a breakpoint was encountered that the end of the try conditional has
+    // been reached: display the debug prompt (if not already done).  Do
+    // this on normal control flow or when an exception was thrown, but not
+    // on an interrupt or error not converted to an exception or when
+    // a ":break", ":continue", ":return", or ":finish" is pending.  These
+    // actions are carried out immediately.
+    if ((rethrow || (!skip && !(cstack->cs_flags[idx] & CSF_FINALLY)
+		    && !cstack->cs_pending[idx]))
+	    && dbg_check_skipped(eap))
+    {
+	// Handle a ">quit" debug command as if an interrupt had occurred
+	// before the ":endtry".  That is, throw an interrupt exception and
+	// set "skip" and "rethrow".
+	if (got_int)
 	{
-	    pending = cstack->cs_pending[idx];
-	    cstack->cs_pending[idx] = CSTP_NONE;
-	    if (pending == CSTP_RETURN)
-		rettv = cstack->cs_rettv[idx];
-	    else if (pending & CSTP_THROW)
-		current_exception = cstack->cs_exception[idx];
+	    skip = TRUE;
+	    (void)do_intthrow(cstack);
+	    // The do_intthrow() call may have reset did_throw or
+	    // cstack->cs_pending[idx].
+	    rethrow = FALSE;
+	    if (did_throw && !(cstack->cs_flags[idx] & CSF_FINALLY))
+		rethrow = TRUE;
 	}
+    }
 
-	/*
-	 * Discard anything pending on an error, interrupt, or throw in the
-	 * finally clause.  If there was no ":finally", discard a pending
-	 * ":continue", ":break", ":return", or ":finish" if an error or
-	 * interrupt occurred afterwards, but before the ":endtry" was reached.
-	 * If an exception was caught by the last of the catch clauses and there
-	 * was no finally clause, finish the exception now.  This happens also
-	 * after errors except when this ":endtry" is not within a ":try".
-	 * Restore "emsg_silent" if it has been reset by this try conditional.
-	 */
-	(void)cleanup_conditionals(cstack, CSF_TRY | CSF_SILENT, TRUE);
+    /*
+     * If a ":return" is pending, we need to resume it after closing the
+     * try conditional; remember the return value.  If there was a finally
+     * clause making an exception pending, we need to rethrow it.  Make it
+     * the exception currently being thrown.
+     */
+    if (!skip)
+    {
+	pending = cstack->cs_pending[idx];
+	cstack->cs_pending[idx] = CSTP_NONE;
+	if (pending == CSTP_RETURN)
+	    rettv = cstack->cs_rettv[idx];
+	else if (pending & CSTP_THROW)
+	    current_exception = cstack->cs_exception[idx];
+    }
 
-	if (cstack->cs_idx >= 0
-			       && (cstack->cs_flags[cstack->cs_idx] & CSF_TRY))
-	    leave_block(cstack);
-	--cstack->cs_trylevel;
+    /*
+     * Discard anything pending on an error, interrupt, or throw in the
+     * finally clause.  If there was no ":finally", discard a pending
+     * ":continue", ":break", ":return", or ":finish" if an error or
+     * interrupt occurred afterwards, but before the ":endtry" was reached.
+     * If an exception was caught by the last of the catch clauses and there
+     * was no finally clause, finish the exception now.  This happens also
+     * after errors except when this ":endtry" is not within a ":try".
+     * Restore "emsg_silent" if it has been reset by this try conditional.
+     */
+    (void)cleanup_conditionals(cstack, CSF_TRY | CSF_SILENT, TRUE);
 
-	if (!skip)
-	{
-	    report_resume_pending(pending,
+    if (cstack->cs_idx >= 0 && (cstack->cs_flags[cstack->cs_idx] & CSF_TRY))
+	leave_block(cstack);
+    --cstack->cs_trylevel;
+
+    if (!skip)
+    {
+	report_resume_pending(pending,
 		    (pending == CSTP_RETURN) ? rettv :
 		    (pending & CSTP_THROW) ? (void *)current_exception : NULL);
-	    switch (pending)
-	    {
-		case CSTP_NONE:
-		    break;
+	switch (pending)
+	{
+	    case CSTP_NONE:
+		break;
 
-		// Reactivate a pending ":continue", ":break", ":return",
-		// ":finish" from the try block or a catch clause of this try
-		// conditional.  This is skipped, if there was an error in an
-		// (unskipped) conditional command or an interrupt afterwards
-		// or if the finally clause is present and executed a new error,
-		// interrupt, throw, ":continue", ":break", ":return", or
-		// ":finish".
-		case CSTP_CONTINUE:
-		    ex_continue(eap);
-		    break;
-		case CSTP_BREAK:
-		    ex_break(eap);
-		    break;
-		case CSTP_RETURN:
-		    do_return(eap, FALSE, FALSE, rettv);
-		    break;
-		case CSTP_FINISH:
-		    do_finish(eap, FALSE);
-		    break;
+	    // Reactivate a pending ":continue", ":break", ":return",
+	    // ":finish" from the try block or a catch clause of this try
+	    // conditional.  This is skipped, if there was an error in an
+	    // (unskipped) conditional command or an interrupt afterwards
+	    // or if the finally clause is present and executed a new error,
+	    // interrupt, throw, ":continue", ":break", ":return", or
+	    // ":finish".
+	    case CSTP_CONTINUE:
+		ex_continue(eap);
+		break;
+	    case CSTP_BREAK:
+		ex_break(eap);
+		break;
+	    case CSTP_RETURN:
+		do_return(eap, FALSE, FALSE, rettv);
+		break;
+	    case CSTP_FINISH:
+		do_finish(eap, FALSE);
+		break;
 
-		// When the finally clause was entered due to an error,
-		// interrupt or throw (as opposed to a ":continue", ":break",
-		// ":return", or ":finish"), restore the pending values of
-		// did_emsg, got_int, and did_throw.  This is skipped, if there
-		// was a new error, interrupt, throw, ":continue", ":break",
-		// ":return", or ":finish".  in the finally clause.
-		default:
-		    if (pending & CSTP_ERROR)
-			did_emsg = TRUE;
-		    if (pending & CSTP_INTERRUPT)
-			got_int = TRUE;
-		    if (pending & CSTP_THROW)
-			rethrow = TRUE;
-		    break;
-	    }
+	    // When the finally clause was entered due to an error,
+	    // interrupt or throw (as opposed to a ":continue", ":break",
+	    // ":return", or ":finish"), restore the pending values of
+	    // did_emsg, got_int, and did_throw.  This is skipped, if there
+	    // was a new error, interrupt, throw, ":continue", ":break",
+	    // ":return", or ":finish".  in the finally clause.
+	    default:
+		if (pending & CSTP_ERROR)
+		    did_emsg = TRUE;
+		if (pending & CSTP_INTERRUPT)
+		    got_int = TRUE;
+		if (pending & CSTP_THROW)
+		    rethrow = TRUE;
+		break;
 	}
-
-	if (rethrow)
-	    // Rethrow the current exception (within this cstack).
-	    do_throw(cstack);
     }
+
+    if (rethrow)
+	// Rethrow the current exception (within this cstack).
+	do_throw(cstack);
 }
 
 /*
  * enter_cleanup() and leave_cleanup()
  *
diff --git a/src/testdir/test_trycatch.vim b/src/testdir/test_trycatch.vim
index aa42205a1b0b..28cd39f04824 100644
--- a/src/testdir/test_trycatch.vim
+++ b/src/testdir/test_trycatch.vim
@@ -1,10 +1,11 @@
 " Test try-catch-finally exception handling
 " Most of this was formerly in test49.
 
 source check.vim
 source shared.vim
+import './vim9.vim' as v9
 
 "-------------------------------------------------------------------------------
 " Test environment							    {{{1
 "-------------------------------------------------------------------------------
 
@@ -2006,10 +2007,31 @@ func Test_try_catch_errors()
   call assert_fails('finally', 'E606:')
   call assert_fails('try | finally | finally | endtry', 'E607:')
   call assert_fails('try | for i in range(5) | endif | endtry', 'E580:')
   call assert_fails('try | while v:true | endtry', 'E170:')
   call assert_fails('try | if v:true | endtry', 'E171:')
+
+  " this was using a negative index in cstack[]
+  let lines =<< trim END
+      try
+      for
+      if
+      endwhile
+      if
+      finally
+  END
+  call v9.CheckScriptFailure(lines, 'E690:')
+
+  let lines =<< trim END
+      try
+      for
+      if
+      endwhile
+      if
+      endtry
+  END
+  call v9.CheckScriptFailure(lines, 'E690:')
 endfunc
 
 " Test for verbose messages with :try :catch, and :finally                 {{{1
 func Test_try_catch_verbose()
   " This test works only when the language is English
diff --git a/src/version.c b/src/version.c
index b3efbbe063c1..942a783ba0ee 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 */
+/**/
+    577,
 /**/
     576,
 /**/
     575,
 /**/

Vulnerable code:     if (cstack->cs_trylevel <= 0 || cstack->cs_idx < 0)
    else
	if (!(cstack->cs_flags[cstack->cs_idx] & CSF_TRY))
	{
	    eap->errmsg = get_end_emsg(cstack);
	    for (idx = cstack->cs_idx - 1; idx > 0; --idx)
		if (cstack->cs_flags[idx] & CSF_TRY)
		    break;
	    // Make this error pending, so that the commands in the following
	    // finally clause can be executed.  This overrules also a pending
	    // ":continue", ":break", ":return", or ":finish".
	    pending = CSTP_ERROR;
	}
	else
	    idx = cstack->cs_idx;
	if (cstack->cs_flags[idx] & CSF_FINALLY)
	    // Give up for a multiple ":finally" and ignore it.
	    eap->errmsg = _(e_multiple_finally);
	    return;
	rewind_conditionals(cstack, idx, CSF_WHILE | CSF_FOR,
						       &cstack->cs_looplevel);
	 * Don't do something when the corresponding try block never got active
	 * (because of an inactive surrounding conditional or after an error or
	 * interrupt or throw) or for a ":finally" without ":try" or a multiple
	 * ":finally".  After every other error (did_emsg or the conditional
	 * errors detected above) or after an interrupt (got_int) or an
	 * exception (did_throw), the finally clause must be executed.
	skip = !(cstack->cs_flags[cstack->cs_idx] & CSF_TRUE);
	if (!skip)
	    // When debugging or a breakpoint was encountered, display the
	    // debug prompt (if not already done).  The user then knows that the
	    // finally clause is executed.
	    if (dbg_check_skipped(eap))
	    {
		// Handle a ">quit" debug command as if an interrupt had
		// occurred before the ":finally".  That is, discard the
		// original exception and replace it by an interrupt
		// exception.
		(void)do_intthrow(cstack);
	    }

	    /*
	     * If there is a preceding catch clause and it caught the exception,
	     * finish the exception now.  This happens also after errors except
	     * when this is a multiple ":finally" or one not within a ":try".
	     * After an error or interrupt, this also discards a pending
	     * ":continue", ":break", ":finish", or ":return" from the preceding
	     * try block or catch clause.
	     */
	    cleanup_conditionals(cstack, CSF_TRY, FALSE);
	    if (cstack->cs_idx >= 0
			       && (cstack->cs_flags[cstack->cs_idx] & CSF_TRY))
		// Variables declared in the previous block can no longer be
		// used.
		leave_block(cstack);
		enter_block(cstack);

	    /*
	     * Make did_emsg, got_int, did_throw pending.  If set, they overrule
	     * a pending ":continue", ":break", ":return", or ":finish".  Then
	     * we have particularly to discard a pending return value (as done
	     * by the call to cleanup_conditionals() above when did_emsg or
	     * got_int is set).  The pending values are restored by the
	     * ":endtry", except if there is a new error, interrupt, exception,
	     * ":continue", ":break", ":return", or ":finish" in the following
	     * finally clause.  A missing ":endwhile", ":endfor" or ":endif"
	     * detected here is treated as if did_emsg and did_throw had
	     * already been set, respectively in case that the error is not
	     * converted to an exception, did_throw had already been unset.
	     * We must not set did_emsg here since that would suppress the
	     * error message.
	     */
	    if (pending == CSTP_ERROR || did_emsg || got_int || did_throw)
	    {
		if (cstack->cs_pending[cstack->cs_idx] == CSTP_RETURN)
		{
		    report_discard_pending(CSTP_RETURN,
					   cstack->cs_rettv[cstack->cs_idx]);
		    discard_pending_return(cstack->cs_rettv[cstack->cs_idx]);
		}
		if (pending == CSTP_ERROR && !did_emsg)
		    pending |= (THROW_ON_ERROR) ? CSTP_THROW : 0;
		else
		    pending |= did_throw ? CSTP_THROW : 0;
		pending |= did_emsg  ? CSTP_ERROR     : 0;
		pending |= got_int   ? CSTP_INTERRUPT : 0;
		cstack->cs_pending[cstack->cs_idx] = pending;

		// It's mandatory that the current exception is stored in the
		// cstack so that it can be rethrown at the ":endtry" or be
		// discarded if the finally clause is left by a ":continue",
		// ":break", ":return", ":finish", error, interrupt, or another
		// exception.  When emsg() is called for a missing ":endif" or
		// a missing ":endwhile"/":endfor" detected here, the
		// exception will be discarded.
		if (did_throw && cstack->cs_exception[cstack->cs_idx]
							 != current_exception)
		    internal_error("ex_finally()");
	    }

	    /*
	     * Set CSL_HAD_FINA, so do_cmdline() will reset did_emsg,
	     * got_int, and did_throw and make the finally clause active.
	     * This will happen after emsg() has been called for a missing
	     * ":endif" or a missing ":endwhile"/":endfor" detected here, so
	     * that the following finally clause will be executed even then.
	     */
	    cstack->cs_lflags |= CSL_HAD_FINA;
    if (cstack->cs_trylevel <= 0 || cstack->cs_idx < 0)
	eap->errmsg = _(e_endtry_without_try);
    else
	/*
	 * Don't do something after an error, interrupt or throw in the try
	 * block, catch clause, or finally clause preceding this ":endtry" or
	 * when an error or interrupt occurred after a ":continue", ":break",
	 * ":return", or ":finish" in a try block or catch clause preceding this
	 * ":endtry" or when the try block never got active (because of an
	 * inactive surrounding conditional or after an error or interrupt or
	 * throw) or when there is a surrounding conditional and it has been
	 * made inactive by a ":continue", ":break", ":return", or ":finish" in
	 * the finally clause.  The latter case need not be tested since then
	 * anything pending has already been discarded. */
	skip = did_emsg || got_int || did_throw
	if (!(cstack->cs_flags[cstack->cs_idx] & CSF_TRY))
	{
	    eap->errmsg = get_end_emsg(cstack);
	    // Find the matching ":try" and report what's missing.
	    idx = cstack->cs_idx;
	    do
		--idx;
	    while (idx > 0 && !(cstack->cs_flags[idx] & CSF_TRY));
	    rewind_conditionals(cstack, idx, CSF_WHILE | CSF_FOR,
						       &cstack->cs_looplevel);
	    skip = TRUE;
	    /*
	     * If an exception is being thrown, discard it to prevent it from
	     * being rethrown at the end of this function.  It would be
	     * discarded by the error message, anyway.  Resets did_throw.
	     * This does not affect the script termination due to the error
	     * since "trylevel" is decremented after emsg() has been called.
	     */
	    if (did_throw)
		discard_current_exception();
	    // report eap->errmsg, also when there already was an error
	    did_emsg = FALSE;
	}
	else
	{
	    idx = cstack->cs_idx;
	    // Check the flags only when not in a skipped block.
	    if (!skip && in_vim9script()
	    {
		// try/endtry without any catch or finally: give an error and
		// continue.
		eap->errmsg = _(e_missing_catch_or_finally);
	    }

	    /*
	     * If we stopped with the exception currently being thrown at this
	     * try conditional since we didn't know that it doesn't have
	     * a finally clause, we need to rethrow it after closing the try
	     * conditional.
	     */
	    if (did_throw && (cstack->cs_flags[idx] & CSF_TRUE)
		    && !(cstack->cs_flags[idx] & CSF_FINALLY))
		rethrow = TRUE;
	}

	// If there was no finally clause, show the user when debugging or
	// a breakpoint was encountered that the end of the try conditional has
	// been reached: display the debug prompt (if not already done).  Do
	// this on normal control flow or when an exception was thrown, but not
	// on an interrupt or error not converted to an exception or when
	// a ":break", ":continue", ":return", or ":finish" is pending.  These
	// actions are carried out immediately.
	if ((rethrow || (!skip
			&& !(cstack->cs_flags[idx] & CSF_FINALLY)
			&& !cstack->cs_pending[idx]))
		&& dbg_check_skipped(eap))
	    // Handle a ">quit" debug command as if an interrupt had occurred
	    // before the ":endtry".  That is, throw an interrupt exception and
	    // set "skip" and "rethrow".
	    if (got_int)
	    {
		skip = TRUE;
		(void)do_intthrow(cstack);
		// The do_intthrow() call may have reset did_throw or
		// cstack->cs_pending[idx].
		rethrow = FALSE;
		if (did_throw && !(cstack->cs_flags[idx] & CSF_FINALLY))
		    rethrow = TRUE;
	    }
	 * If a ":return" is pending, we need to resume it after closing the
	 * try conditional; remember the return value.  If there was a finally
	 * clause making an exception pending, we need to rethrow it.  Make it
	 * the exception currently being thrown.
	if (!skip)
	    pending = cstack->cs_pending[idx];
	    cstack->cs_pending[idx] = CSTP_NONE;
	    if (pending == CSTP_RETURN)
		rettv = cstack->cs_rettv[idx];
	    else if (pending & CSTP_THROW)
		current_exception = cstack->cs_exception[idx];
	/*
	 * Discard anything pending on an error, interrupt, or throw in the
	 * finally clause.  If there was no ":finally", discard a pending
	 * ":continue", ":break", ":return", or ":finish" if an error or
	 * interrupt occurred afterwards, but before the ":endtry" was reached.
	 * If an exception was caught by the last of the catch clauses and there
	 * was no finally clause, finish the exception now.  This happens also
	 * after errors except when this ":endtry" is not within a ":try".
	 * Restore "emsg_silent" if it has been reset by this try conditional.
	 */
	(void)cleanup_conditionals(cstack, CSF_TRY | CSF_SILENT, TRUE);
	if (cstack->cs_idx >= 0
			       && (cstack->cs_flags[cstack->cs_idx] & CSF_TRY))
	    leave_block(cstack);
	--cstack->cs_trylevel;
	if (!skip)
	{
	    report_resume_pending(pending,
	    switch (pending)
	    {
		case CSTP_NONE:
		    break;
		// Reactivate a pending ":continue", ":break", ":return",
		// ":finish" from the try block or a catch clause of this try
		// conditional.  This is skipped, if there was an error in an
		// (unskipped) conditional command or an interrupt afterwards
		// or if the finally clause is present and executed a new error,
		// interrupt, throw, ":continue", ":break", ":return", or
		// ":finish".
		case CSTP_CONTINUE:
		    ex_continue(eap);
		    break;
		case CSTP_BREAK:
		    ex_break(eap);
		    break;
		case CSTP_RETURN:
		    do_return(eap, FALSE, FALSE, rettv);
		    break;
		case CSTP_FINISH:
		    do_finish(eap, FALSE);
		    break;
		// When the finally clause was entered due to an error,
		// interrupt or throw (as opposed to a ":continue", ":break",
		// ":return", or ":finish"), restore the pending values of
		// did_emsg, got_int, and did_throw.  This is skipped, if there
		// was a new error, interrupt, throw, ":continue", ":break",
		// ":return", or ":finish".  in the finally clause.
		default:
		    if (pending & CSTP_ERROR)
			did_emsg = TRUE;
		    if (pending & CSTP_INTERRUPT)
			got_int = TRUE;
		    if (pending & CSTP_THROW)
			rethrow = TRUE;
		    break;
	    }

	if (rethrow)
	    // Rethrow the current exception (within this cstack).
	    do_throw(cstack);
Fixed code:     for (idx = cstack->cs_idx; idx >= 0; --idx)
	if (cstack->cs_flags[idx] & CSF_TRY)
	    break;
    if (cstack->cs_trylevel <= 0 || idx < 0)
    {
	return;
    }

    if (!(cstack->cs_flags[cstack->cs_idx] & CSF_TRY))
	eap->errmsg = get_end_emsg(cstack);
	// Make this error pending, so that the commands in the following
	// finally clause can be executed.  This overrules also a pending
	// ":continue", ":break", ":return", or ":finish".
	pending = CSTP_ERROR;
    }
    if (cstack->cs_flags[idx] & CSF_FINALLY)
    {
	// Give up for a multiple ":finally" and ignore it.
	eap->errmsg = _(e_multiple_finally);
	return;
    }
    rewind_conditionals(cstack, idx, CSF_WHILE | CSF_FOR,
						   &cstack->cs_looplevel);

    /*
     * Don't do something when the corresponding try block never got active
     * (because of an inactive surrounding conditional or after an error or
     * interrupt or throw) or for a ":finally" without ":try" or a multiple
     * ":finally".  After every other error (did_emsg or the conditional
     * errors detected above) or after an interrupt (got_int) or an
     * exception (did_throw), the finally clause must be executed.
     */
    skip = !(cstack->cs_flags[cstack->cs_idx] & CSF_TRUE);

    if (!skip)
    {
	// When debugging or a breakpoint was encountered, display the
	// debug prompt (if not already done).  The user then knows that the
	// finally clause is executed.
	if (dbg_check_skipped(eap))
	    // Handle a ">quit" debug command as if an interrupt had
	    // occurred before the ":finally".  That is, discard the
	    // original exception and replace it by an interrupt
	    // exception.
	    (void)do_intthrow(cstack);
	 * If there is a preceding catch clause and it caught the exception,
	 * finish the exception now.  This happens also after errors except
	 * when this is a multiple ":finally" or one not within a ":try".
	 * After an error or interrupt, this also discards a pending
	 * ":continue", ":break", ":finish", or ":return" from the preceding
	 * try block or catch clause.
	cleanup_conditionals(cstack, CSF_TRY, FALSE);
	if (cstack->cs_idx >= 0
			   && (cstack->cs_flags[cstack->cs_idx] & CSF_TRY))
	    // Variables declared in the previous block can no longer be
	    // used.
	    leave_block(cstack);
	    enter_block(cstack);
	}
	/*
	 * Make did_emsg, got_int, did_throw pending.  If set, they overrule
	 * a pending ":continue", ":break", ":return", or ":finish".  Then
	 * we have particularly to discard a pending return value (as done
	 * by the call to cleanup_conditionals() above when did_emsg or
	 * got_int is set).  The pending values are restored by the
	 * ":endtry", except if there is a new error, interrupt, exception,
	 * ":continue", ":break", ":return", or ":finish" in the following
	 * finally clause.  A missing ":endwhile", ":endfor" or ":endif"
	 * detected here is treated as if did_emsg and did_throw had
	 * already been set, respectively in case that the error is not
	 * converted to an exception, did_throw had already been unset.
	 * We must not set did_emsg here since that would suppress the
	 * error message.
	 */
	if (pending == CSTP_ERROR || did_emsg || got_int || did_throw)
	{
	    if (cstack->cs_pending[cstack->cs_idx] == CSTP_RETURN)
		report_discard_pending(CSTP_RETURN,
				       cstack->cs_rettv[cstack->cs_idx]);
		discard_pending_return(cstack->cs_rettv[cstack->cs_idx]);
	    if (pending == CSTP_ERROR && !did_emsg)
		pending |= (THROW_ON_ERROR) ? CSTP_THROW : 0;
	    else
		pending |= did_throw ? CSTP_THROW : 0;
	    pending |= did_emsg  ? CSTP_ERROR     : 0;
	    pending |= got_int   ? CSTP_INTERRUPT : 0;
	    cstack->cs_pending[cstack->cs_idx] = pending;

	    // It's mandatory that the current exception is stored in the
	    // cstack so that it can be rethrown at the ":endtry" or be
	    // discarded if the finally clause is left by a ":continue",
	    // ":break", ":return", ":finish", error, interrupt, or another
	    // exception.  When emsg() is called for a missing ":endif" or
	    // a missing ":endwhile"/":endfor" detected here, the
	    // exception will be discarded.
	    if (did_throw && cstack->cs_exception[cstack->cs_idx]
						     != current_exception)
		internal_error("ex_finally()");

	/*
	 * Set CSL_HAD_FINA, so do_cmdline() will reset did_emsg,
	 * got_int, and did_throw and make the finally clause active.
	 * This will happen after emsg() has been called for a missing
	 * ":endif" or a missing ":endwhile"/":endfor" detected here, so
	 * that the following finally clause will be executed even then.
	 */
	cstack->cs_lflags |= CSL_HAD_FINA;
    for (idx = cstack->cs_idx; idx >= 0; --idx)
	if (cstack->cs_flags[idx] & CSF_TRY)
	    break;
    if (cstack->cs_trylevel <= 0 || idx < 0)
	eap->errmsg = _(e_endtry_without_try);
	return;
    }

    /*
     * Don't do something after an error, interrupt or throw in the try
     * block, catch clause, or finally clause preceding this ":endtry" or
     * when an error or interrupt occurred after a ":continue", ":break",
     * ":return", or ":finish" in a try block or catch clause preceding this
     * ":endtry" or when the try block never got active (because of an
     * inactive surrounding conditional or after an error or interrupt or
     * throw) or when there is a surrounding conditional and it has been
     * made inactive by a ":continue", ":break", ":return", or ":finish" in
     * the finally clause.  The latter case need not be tested since then
     * anything pending has already been discarded. */
    skip = did_emsg || got_int || did_throw
    if (!(cstack->cs_flags[cstack->cs_idx] & CSF_TRY))
    {
	eap->errmsg = get_end_emsg(cstack);
	// Find the matching ":try" and report what's missing.
	rewind_conditionals(cstack, idx, CSF_WHILE | CSF_FOR,
							&cstack->cs_looplevel);
	skip = TRUE;
	/*
	 * If an exception is being thrown, discard it to prevent it from
	 * being rethrown at the end of this function.  It would be
	 * discarded by the error message, anyway.  Resets did_throw.
	 * This does not affect the script termination due to the error
	 * since "trylevel" is decremented after emsg() has been called.
	 */
	if (did_throw)
	    discard_current_exception();
	// report eap->errmsg, also when there already was an error
	did_emsg = FALSE;
    }
    else
    {
	idx = cstack->cs_idx;
	// Check the flags only when not in a skipped block.
	if (!skip && in_vim9script()
	    // try/endtry without any catch or finally: give an error and
	    // continue.
	    eap->errmsg = _(e_missing_catch_or_finally);
	 * If we stopped with the exception currently being thrown at this
	 * try conditional since we didn't know that it doesn't have
	 * a finally clause, we need to rethrow it after closing the try
	 * conditional.
	if (did_throw && (cstack->cs_flags[idx] & CSF_TRUE)
		&& !(cstack->cs_flags[idx] & CSF_FINALLY))
	    rethrow = TRUE;
    }

    // If there was no finally clause, show the user when debugging or
    // a breakpoint was encountered that the end of the try conditional has
    // been reached: display the debug prompt (if not already done).  Do
    // this on normal control flow or when an exception was thrown, but not
    // on an interrupt or error not converted to an exception or when
    // a ":break", ":continue", ":return", or ":finish" is pending.  These
    // actions are carried out immediately.
    if ((rethrow || (!skip && !(cstack->cs_flags[idx] & CSF_FINALLY)
		    && !cstack->cs_pending[idx]))
	    && dbg_check_skipped(eap))
    {
	// Handle a ">quit" debug command as if an interrupt had occurred
	// before the ":endtry".  That is, throw an interrupt exception and
	// set "skip" and "rethrow".
	if (got_int)
	    skip = TRUE;
	    (void)do_intthrow(cstack);
	    // The do_intthrow() call may have reset did_throw or
	    // cstack->cs_pending[idx].
	    rethrow = FALSE;
	    if (did_throw && !(cstack->cs_flags[idx] & CSF_FINALLY))
		rethrow = TRUE;
    }
    /*
     * If a ":return" is pending, we need to resume it after closing the
     * try conditional; remember the return value.  If there was a finally
     * clause making an exception pending, we need to rethrow it.  Make it
     * the exception currently being thrown.
     */
    if (!skip)
    {
	pending = cstack->cs_pending[idx];
	cstack->cs_pending[idx] = CSTP_NONE;
	if (pending == CSTP_RETURN)
	    rettv = cstack->cs_rettv[idx];
	else if (pending & CSTP_THROW)
	    current_exception = cstack->cs_exception[idx];
    }
    /*
     * Discard anything pending on an error, interrupt, or throw in the
     * finally clause.  If there was no ":finally", discard a pending
     * ":continue", ":break", ":return", or ":finish" if an error or
     * interrupt occurred afterwards, but before the ":endtry" was reached.
     * If an exception was caught by the last of the catch clauses and there
     * was no finally clause, finish the exception now.  This happens also
     * after errors except when this ":endtry" is not within a ":try".
     * Restore "emsg_silent" if it has been reset by this try conditional.
     */
    (void)cleanup_conditionals(cstack, CSF_TRY | CSF_SILENT, TRUE);
    if (cstack->cs_idx >= 0 && (cstack->cs_flags[cstack->cs_idx] & CSF_TRY))
	leave_block(cstack);
    --cstack->cs_trylevel;

    if (!skip)
    {
	report_resume_pending(pending,
	switch (pending)
	{
	    case CSTP_NONE:
		break;
	    // Reactivate a pending ":continue", ":break", ":return",
	    // ":finish" from the try block or a catch clause of this try
	    // conditional.  This is skipped, if there was an error in an
	    // (unskipped) conditional command or an interrupt afterwards
	    // or if the finally clause is present and executed a new error,
	    // interrupt, throw, ":continue", ":break", ":return", or
	    // ":finish".
	    case CSTP_CONTINUE:
		ex_continue(eap);
		break;
	    case CSTP_BREAK:
		ex_break(eap);
		break;
	    case CSTP_RETURN:
		do_return(eap, FALSE, FALSE, rettv);
		break;
	    case CSTP_FINISH:
		do_finish(eap, FALSE);
		break;
	    // When the finally clause was entered due to an error,
	    // interrupt or throw (as opposed to a ":continue", ":break",
	    // ":return", or ":finish"), restore the pending values of
	    // did_emsg, got_int, and did_throw.  This is skipped, if there
	    // was a new error, interrupt, throw, ":continue", ":break",
	    // ":return", or ":finish".  in the finally clause.
	    default:
		if (pending & CSTP_ERROR)
		    did_emsg = TRUE;
		if (pending & CSTP_INTERRUPT)
		    got_int = TRUE;
		if (pending & CSTP_THROW)
		    rethrow = TRUE;
		break;

    if (rethrow)
	// Rethrow the current exception (within this cstack).
	do_throw(cstack);
import './vim9.vim' as v9

  " this was using a negative index in cstack[]
  let lines =<< trim END
      try
      for
      if
      endwhile
      if
      finally
  END
  call v9.CheckScriptFailure(lines, 'E690:')

  let lines =<< trim END
      try
      for
      if
      endwhile
      if
      endtry
  END
  call v9.CheckScriptFailure(lines, 'E690:')
/**/
    577,