From 96b9bf8f74af8abf1e30054f996708db7dc285be Mon Sep 17 00:00:00 2001
From: Bram Moolenaar <Bram@vim.org>
Date: Sat, 24 Sep 2022 17:24:12 +0100
Subject: patch 9.0.0577: buffer underflow with unexpected :finally

Problem:    Buffer underflow with unexpected :finally.
Solution:   Check CSF_TRY can be found.
---
 src/ex_eval.c                 | 523 +++++++++++++++++-----------------
 src/testdir/test_trycatch.vim |  22 ++
 2 files changed, 282 insertions(+), 263 deletions(-)

diff --git a/src/ex_eval.c b/src/ex_eval.c
index 5721b766e..77d6e8bb9 100644
--- a/src/ex_eval.c
+++ b/src/ex_eval.c
@@ -1935,128 +1935,127 @@ ex_finally(exarg_T *eap)
     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;
     }
 }
 
@@ -2076,185 +2075,183 @@ ex_endtry(exarg_T *eap)
     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);
 }
 
 /*
diff --git a/src/testdir/test_trycatch.vim b/src/testdir/test_trycatch.vim
index aa42205a1..28cd39f04 100644
--- a/src/testdir/test_trycatch.vim
+++ b/src/testdir/test_trycatch.vim
@@ -3,6 +3,7 @@
 
 source check.vim
 source shared.vim
+import './vim9.vim' as v9
 
 "-------------------------------------------------------------------------------
 " Test environment							    {{{1
@@ -2008,6 +2009,27 @@ func Test_try_catch_errors()
   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
-- 
2.23.0

