changeset 33613:31fb1a760ad6 v9.0.2050

patch 9.0.2050: Vim9: crash with deferred function call and exception Commit: https://github.com/vim/vim/commit/c59c1e0d88651a71ece7366e418f1253abbe2a28 Author: Yegappan Lakshmanan <yegappan@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 <cb@256bit.org> Co-authored-by: Yegappan Lakshmanan <yegappan@yahoo.com>
author Christian Brabandt <cb@256bit.org>
date Thu, 19 Oct 2023 11:00:07 +0200
parents 0dd1e3a17f68
children 948658f96878
files src/ex_eval.c src/proto/ex_eval.pro src/structs.h src/testdir/test_user_func.vim src/testdir/test_vim9_script.vim src/time.c src/userfunc.c src/vim9execute.c
diffstat 8 files changed, 106 insertions(+), 50 deletions(-) [+]
line wrap: on
line diff
--- a/src/ex_eval.c
+++ b/src/ex_eval.c
@@ -748,6 +748,43 @@ finish_exception(except_T *excp)
 }
 
 /*
+ * 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.
  */
 #define RP_MAKE		0
--- a/src/proto/ex_eval.pro
+++ b/src/proto/ex_eval.pro
@@ -11,6 +11,9 @@ char *get_exception_string(void *value, 
 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);
--- 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
--- 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
--- 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
--- 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;
--- 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);
--- 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);