To: vim_dev@googlegroups.com Subject: Patch 9.0.0577 Fcc: outbox From: Bram Moolenaar Mime-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ------------ Patch 9.0.0577 Problem: Buffer underflow with unexpected :finally. Solution: Check CSF_TRY can be found. Files: src/ex_eval.c, src/testdir/test_trycatch.vim *** ../vim-9.0.0576/src/ex_eval.c 2022-09-21 18:59:10.671074961 +0100 --- src/ex_eval.c 2022-09-24 17:21:20.299859969 +0100 *************** *** 1935,2062 **** if (cmdmod_error(FALSE)) return; ! if (cstack->cs_trylevel <= 0 || cstack->cs_idx < 0) eap->errmsg = _(e_finally_without_try); ! 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; } } } --- 1935,2061 ---- if (cmdmod_error(FALSE)) return; ! 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); ! 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; } } *************** *** 2076,2260 **** if (cmdmod_error(FALSE)) return; ! 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 || !(cstack->cs_flags[cstack->cs_idx] & CSF_TRUE); ! 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() && (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; ! } } /* ! * 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, (pending == CSTP_RETURN) ? rettv : (pending & CSTP_THROW) ? (void *)current_exception : NULL); ! 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); } } /* --- 2075,2257 ---- if (cmdmod_error(FALSE)) return; ! 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 || !(cstack->cs_flags[cstack->cs_idx] & CSF_TRUE); ! 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() && (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; } + } ! /* ! * 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, (pending == CSTP_RETURN) ? rettv : (pending & CSTP_THROW) ? (void *)current_exception : NULL); ! 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); } /* *** ../vim-9.0.0576/src/testdir/test_trycatch.vim 2022-05-27 13:47:02.000000000 +0100 --- src/testdir/test_trycatch.vim 2022-09-24 17:22:06.711776571 +0100 *************** *** 3,8 **** --- 3,9 ---- source check.vim source shared.vim + import './vim9.vim' as v9 "------------------------------------------------------------------------------- " Test environment {{{1 *************** *** 2008,2013 **** --- 2009,2035 ---- 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 *** ../vim-9.0.0576/src/version.c 2022-09-24 15:55:22.385626232 +0100 --- src/version.c 2022-09-24 17:03:39.749548167 +0100 *************** *** 701,702 **** --- 701,704 ---- { /* Add new patch number below this line */ + /**/ + 577, /**/ -- Microsoft says that MS-Windows is much better for you than Linux. That's like the Pope saying that catholicism is much better for you than protestantism. /// Bram Moolenaar -- Bram@Moolenaar.net -- http://www.Moolenaar.net \\\ /// \\\ \\\ sponsor Vim, vote for features -- http://www.Vim.org/sponsor/ /// \\\ help me help AIDS victims -- http://ICCF-Holland.org ///