Commit: patch 9.0.2050: Vim9: crash with deferred function call and exception

4 views
Skip to first unread message

Christian Brabandt

unread,
Oct 19, 2023, 5:00:13 AM10/19/23
to vim...@googlegroups.com
patch 9.0.2050: Vim9: crash with deferred function call and exception

Commit: https://github.com/vim/vim/commit/c59c1e0d88651a71ece7366e418f1253abbe2a28
Author: Yegappan Lakshmanan <yega...@yahoo.com>
Date: Thu Oct 19 10:52:34 2023 +0200

patch 9.0.2050: Vim9: crash with deferred function call and exception

Problem: Vim9: crash with deferred function call and exception
Solution: Save and restore exception state

Crash when a deferred function is called after an exception and another
exception is thrown

closes: #13376
closes: #13377

Signed-off-by: Christian Brabandt <c...@256bit.org>
Co-authored-by: Yegappan Lakshmanan <yega...@yahoo.com>

diff --git a/src/ex_eval.c b/src/ex_eval.c
index 68dc6d78c..e319dee0f 100644
--- a/src/ex_eval.c
+++ b/src/ex_eval.c
@@ -747,6 +747,43 @@ finish_exception(except_T *excp)
discard_exception(excp, TRUE);
}

+/*
+ * Save the current exception state in "estate"
+ */
+ void
+exception_state_save(exception_state_T *estate)
+{
+ estate->estate_current_exception = current_exception;
+ estate->estate_did_throw = did_throw;
+ estate->estate_need_rethrow = need_rethrow;
+ estate->estate_trylevel = trylevel;
+}
+
+/*
+ * Restore the current exception state from "estate"
+ */
+ void
+exception_state_restore(exception_state_T *estate)
+{
+ if (current_exception == NULL)
+ current_exception = estate->estate_current_exception;
+ did_throw |= estate->estate_did_throw;
+ need_rethrow |= estate->estate_need_rethrow;
+ trylevel |= estate->estate_trylevel;
+}
+
+/*
+ * Clear the current exception state
+ */
+ void
+exception_state_clear(void)
+{
+ current_exception = NULL;
+ did_throw = FALSE;
+ need_rethrow = FALSE;
+ trylevel = 0;
+}
+
/*
* Flags specifying the message displayed by report_pending.
*/
diff --git a/src/proto/ex_eval.pro b/src/proto/ex_eval.pro
index a3be429b1..979d6fb8f 100644
--- a/src/proto/ex_eval.pro
+++ b/src/proto/ex_eval.pro
@@ -11,6 +11,9 @@ char *get_exception_string(void *value, except_type_T type, char_u *cmdname, int
int throw_exception(void *value, except_type_T type, char_u *cmdname);
void discard_current_exception(void);
void catch_exception(except_T *excp);
+void exception_state_save(exception_state_T *estate);
+void exception_state_restore(exception_state_T *estate);
+void exception_state_clear(void);
void report_make_pending(int pending, void *value);
int cmd_is_name_only(char_u *arg);
void ex_eval(exarg_T *eap);
diff --git a/src/structs.h b/src/structs.h
index f7f3b2ec5..a1a94b0eb 100644
--- a/src/structs.h
+++ b/src/structs.h
@@ -1088,6 +1088,19 @@ struct cleanup_stuff
except_T *exception; // exception value
};

+/*
+ * Exception state that is saved and restored when calling timer callback
+ * functions and deferred functions.
+ */
+typedef struct exception_state_S exception_state_T;
+struct exception_state_S
+{
+ except_T *estate_current_exception;
+ int estate_did_throw;
+ int estate_need_rethrow;
+ int estate_trylevel;
+};
+
#ifdef FEAT_SYN_HL
// struct passed to in_id_list()
struct sp_syn
diff --git a/src/testdir/test_user_func.vim b/src/testdir/test_user_func.vim
index 8fc82c4e3..8c3f33dd6 100644
--- a/src/testdir/test_user_func.vim
+++ b/src/testdir/test_user_func.vim
@@ -873,11 +873,21 @@ endfunc
" Test for calling a deferred function after an exception
func Test_defer_after_exception()
let g:callTrace = []
+ func Bar()
+ let g:callTrace += [1]
+ throw 'InnerException'
+ endfunc
+
func Defer()
- let g:callTrace += ['a']
- let g:callTrace += ['b']
- let g:callTrace += ['c']
- let g:callTrace += ['d']
+ let g:callTrace += [2]
+ let g:callTrace += [3]
+ try
+ call Bar()
+ catch /InnerException/
+ let g:callTrace += [4]
+ endtry
+ let g:callTrace += [5]
+ let g:callTrace += [6]
endfunc

func Foo()
@@ -888,9 +898,9 @@ func Test_defer_after_exception()
try
call Foo()
catch /TestException/
- let g:callTrace += ['e']
+ let g:callTrace += [7]
endtry
- call assert_equal(['a', 'b', 'c', 'd', 'e'], g:callTrace)
+ call assert_equal([2, 3, 1, 4, 5, 6, 7], g:callTrace)

delfunc Defer
delfunc Foo
diff --git a/src/testdir/test_vim9_script.vim b/src/testdir/test_vim9_script.vim
index f8280c6d2..75a358e85 100644
--- a/src/testdir/test_vim9_script.vim
+++ b/src/testdir/test_vim9_script.vim
@@ -4691,12 +4691,22 @@ def Test_defer_after_exception()
var lines =<< trim END
vim9script

- var callTrace: list<string> = []
+ var callTrace: list<number> = []
+ def Bar()
+ callTrace += [1]
+ throw 'InnerException'
+ enddef
+
def Defer()
- callTrace += ['a']
- callTrace += ['b']
- callTrace += ['c']
- callTrace += ['d']
+ callTrace += [2]
+ callTrace += [3]
+ try
+ Bar()
+ catch /InnerException/
+ callTrace += [4]
+ endtry
+ callTrace += [5]
+ callTrace += [6]
enddef

def Foo()
@@ -4707,10 +4717,10 @@ def Test_defer_after_exception()
try
Foo()
catch /TestException/
- callTrace += ['e']
+ callTrace += [7]
endtry

- assert_equal(['a', 'b', 'c', 'd', 'e'], callTrace)
+ assert_equal([2, 3, 1, 4, 5, 6, 7], callTrace)
END
v9.CheckScriptSuccess(lines)
enddef
diff --git a/src/time.c b/src/time.c
index 62b38b4bf..8725a8852 100644
--- a/src/time.c
+++ b/src/time.c
@@ -561,13 +561,12 @@ check_due_timer(void)
int prev_uncaught_emsg = uncaught_emsg;
int save_called_emsg = called_emsg;
int save_must_redraw = must_redraw;
- int save_trylevel = trylevel;
- int save_did_throw = did_throw;
- int save_need_rethrow = need_rethrow;
int save_ex_pressedreturn = get_pressedreturn();
int save_may_garbage_collect = may_garbage_collect;
- except_T *save_current_exception = current_exception;
- vimvars_save_T vvsave;
+ vimvars_save_T vvsave;
+ exception_state_T estate;
+
+ exception_state_save(&estate);

// Create a scope for running the timer callback, ignoring most of
// the current scope, such as being inside a try/catch.
@@ -576,11 +575,8 @@ check_due_timer(void)
called_emsg = 0;
did_emsg = FALSE;
must_redraw = 0;
- trylevel = 0;
- did_throw = FALSE;
- need_rethrow = FALSE;
- current_exception = NULL;
may_garbage_collect = FALSE;
+ exception_state_clear();
save_vimvars(&vvsave);

// Invoke the callback.
@@ -597,10 +593,7 @@ check_due_timer(void)
++timer->tr_emsg_count;
did_emsg = save_did_emsg;
called_emsg = save_called_emsg;
- trylevel = save_trylevel;
- did_throw = save_did_throw;
- need_rethrow = save_need_rethrow;
- current_exception = save_current_exception;
+ exception_state_restore(&estate);
restore_vimvars(&vvsave);
if (must_redraw != 0)
need_update_screen = TRUE;
diff --git a/src/userfunc.c b/src/userfunc.c
index 092b3927b..5ef0f7d9c 100644
--- a/src/userfunc.c
+++ b/src/userfunc.c
@@ -6252,21 +6252,16 @@ handle_defer_one(funccall_T *funccal)
dr->dr_name = NULL;

// If the deferred function is called after an exception, then only the
- // first statement in the function will be executed. Save and restore
- // the try/catch/throw exception state.
- int save_trylevel = trylevel;
- int save_did_throw = did_throw;
- int save_need_rethrow = need_rethrow;
-
- trylevel = 0;
- did_throw = FALSE;
- need_rethrow = FALSE;
+ // first statement in the function will be executed (because of the
+ // exception). So save and restore the try/catch/throw exception
+ // state.
+ exception_state_T estate;
+ exception_state_save(&estate);
+ exception_state_clear();

call_func(name, -1, &rettv, dr->dr_argcount, dr->dr_argvars, &funcexe);

- trylevel = save_trylevel;
- did_throw = save_did_throw;
- need_rethrow = save_need_rethrow;
+ exception_state_restore(&estate);

clear_tv(&rettv);
vim_free(name);
diff --git a/src/vim9execute.c b/src/vim9execute.c
index dd3d263b5..a6bf4715a 100644
--- a/src/vim9execute.c
+++ b/src/vim9execute.c
@@ -1141,21 +1141,16 @@ invoke_defer_funcs(ectx_T *ectx)
functv->vval.v_string = NULL;

// If the deferred function is called after an exception, then only the
- // first statement in the function will be executed. Save and restore
- // the try/catch/throw exception state.
- int save_trylevel = trylevel;
- int save_did_throw = did_throw;
- int save_need_rethrow = need_rethrow;
-
- trylevel = 0;
- did_throw = FALSE;
- need_rethrow = FALSE;
+ // first statement in the function will be executed (because of the
+ // exception). So save and restore the try/catch/throw exception
+ // state.
+ exception_state_T estate;
+ exception_state_save(&estate);
+ exception_state_clear();

(void)call_func(name, -1, &rettv, argcount, argvars, &funcexe);

- trylevel = save_trylevel;
- did_throw = save_did_throw;
- need_rethrow = save_need_rethrow;
+ exception_state_restore(&estate);

clear_tv(&rettv);
vim_free(name);

Tony Mechelynck

unread,
Oct 19, 2023, 7:34:35 AM10/19/23
to Christian Brabandt, vim...@googlegroups.com
On Thu, Oct 19, 2023 at 11:00 AM Christian Brabandt <cbl...@256bit.org> wrote:
>
> patch 9.0.2050: Vim9: crash with deferred function call and exception
[...]

This patch lacks a change to src/version.c so when running the
executable, :intro and :version both say it is patchlevel 2049. The
repository tags are OK AFAICT.

Best regards,
Tony.

Christian Brabandt

unread,
Oct 19, 2023, 11:03:20 AM10/19/23
to vim...@googlegroups.com
Oh shoot, I forgot to add version.c to the git commit. Will fix it in a
second.

Best,
Christian
Reply all
Reply to author
Forward
0 new messages