# HG changeset patch # User Bram Moolenaar # Date 1664037004 -7200 # Node ID 4bbc920bdef7855782103ce4f82698b2261ae5aa # Parent dc376deb7e21caa11dbdfb90ead6d7f8adc3aa7f patch 9.0.0577: buffer underflow with unexpected :finally Commit: https://github.com/vim/vim/commit/96b9bf8f74af8abf1e30054f996708db7dc285be Author: Bram Moolenaar Date: Sat Sep 24 17:24:12 2022 +0100 patch 9.0.0577: buffer underflow with unexpected :finally Problem: Buffer underflow with unexpected :finally. Solution: Check CSF_TRY can be found. diff --git a/src/ex_eval.c b/src/ex_eval.c --- 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)) + { + 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[cstack->cs_idx] & CSF_TRY)) + // 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)) { - 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; + // 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); } - 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. + * 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); - - 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); - } + // 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) + /* + * 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) { - 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; + 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()"); - } + // 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; - } + /* + * 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 --- 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 diff --git a/src/version.c b/src/version.c --- a/src/version.c +++ b/src/version.c @@ -700,6 +700,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ /**/ + 577, +/**/ 576, /**/ 575,