From 70958dae75efc797fe85b29df11fb2ea2ebf9401 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Mon, 24 Aug 2026 09:27:46 +0800 Subject: [PATCH] vim-patch:9.2.0996: debugger: crash when evaluating a variable in a :def function frame (#41460) Problem: In the debugger ">up" and ">frame" select an older function call frame, but get_funccal_local_ht() and the related functions check current_funccal while returning a dictionary of the frame that get_funccal() selected. A :def function keeps its local variables on the vim9 stack, its funccall_T has no l: and a: dictionaries and is allocated cleared, so with such a frame selected the returned hashtab has a NULL ht_array and evaluating any variable name at the debug prompt crashes in hash_lookup(). Solution: Check the funccal that is actually used and return NULL when it has no l: variables, so that the variable is reported as undefined instead. closes: vim/vim#21111 https://github.com/vim/vim/commit/5ad47b07af72950624904a9ec0bc9f2d4ebc77a6 Co-authored-by: Christian Brabandt Co-authored-by: Claude Opus 5 (1M context) --- src/nvim/eval/userfunc.c | 41 ++++++++++++++++----------- test/old/testdir/test_debugger.vim | 45 ++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 16 deletions(-) diff --git a/src/nvim/eval/userfunc.c b/src/nvim/eval/userfunc.c index b37d8ff92a..b3e49ead18 100644 --- a/src/nvim/eval/userfunc.c +++ b/src/nvim/eval/userfunc.c @@ -3944,14 +3944,29 @@ funccall_T *get_funccal(void) return funccal; } +/// Get the function call environment to use for the l: and a: variables, based +/// on the backtrace debug level. +/// Returns NULL if there is no current funccal. +static funccall_T *get_funccal_for_vars(void) +{ + funccall_T *funccal = NULL; + + if (current_funccal == NULL) { + return NULL; + } + funccal = get_funccal(); + if (funccal == NULL || funccal->fc_l_vars.dv_refcount == 0) { + return NULL; + } + return funccal; +} + /// @return dict used for local variables in the current funccal or /// NULL if there is no current funccal. dict_T *get_funccal_local_dict(void) { - if (current_funccal == NULL || current_funccal->fc_l_vars.dv_refcount == 0) { - return NULL; - } - return &get_funccal()->fc_l_vars; + funccall_T *funccal = get_funccal_for_vars(); + return funccal == NULL ? NULL : &funccal->fc_l_vars; } /// @return hashtable used for local variables in the current funccal or @@ -3966,20 +3981,16 @@ hashtab_T *get_funccal_local_ht(void) /// NULL if there is no current funccal. dictitem_T *get_funccal_local_var(void) { - if (current_funccal == NULL || current_funccal->fc_l_vars.dv_refcount == 0) { - return NULL; - } - return (dictitem_T *)&get_funccal()->fc_l_vars_var; + funccall_T *funccal = get_funccal_for_vars(); + return funccal == NULL ? NULL : (dictitem_T *)&funccal->fc_l_vars_var; } /// @return the dict used for argument in the current funccal or /// NULL if there is no current funccal. dict_T *get_funccal_args_dict(void) { - if (current_funccal == NULL || current_funccal->fc_l_vars.dv_refcount == 0) { - return NULL; - } - return &get_funccal()->fc_l_avars; + funccall_T *funccal = get_funccal_for_vars(); + return funccal == NULL ? NULL : &funccal->fc_l_avars; } /// @return the hashtable used for argument in the current funccal or @@ -3994,10 +4005,8 @@ hashtab_T *get_funccal_args_ht(void) /// NULL if there is no current funccal. dictitem_T *get_funccal_args_var(void) { - if (current_funccal == NULL || current_funccal->fc_l_vars.dv_refcount == 0) { - return NULL; - } - return (dictitem_T *)&get_funccal()->fc_l_avars_var; + funccall_T *funccal = get_funccal_for_vars(); + return funccal == NULL ? NULL : (dictitem_T *)&funccal->fc_l_avars_var; } /// List function variables, if there is a function. diff --git a/test/old/testdir/test_debugger.vim b/test/old/testdir/test_debugger.vim index 926f33364e..3131a7bee4 100644 --- a/test/old/testdir/test_debugger.vim +++ b/test/old/testdir/test_debugger.vim @@ -1107,6 +1107,51 @@ func Test_debug_def_and_legacy_function() call StopVimInTerminal(buf) endfunc +" Test for using "up" in the debugger to select the frame of a :def function +" and then evaluating a variable name +func Test_debug_backtrace_up_def_function() + CheckRunVimInTerminal + CheckCWD + let file =<< trim END + vim9script + def g:DefFunc() + var defvar = 'in def' + g:LegacyFunc('arg') + enddef + + func g:LegacyFunc(text) + let legacyvar = 'in legacy' + echo legacyvar + endfunc + + breakadd func 2 g:LegacyFunc + END + call writefile(file, 'XtestDebugUp.vim', 'D') + + let buf = RunVimInTerminal('-S XtestDebugUp.vim', {}) + + call s:RunDbgCmd(buf, ':call DefFunc()', ['line 2: echo legacyvar']) + call s:RunDbgCmd(buf, 'echo legacyvar', ['in legacy']) + call s:RunDbgCmd(buf, 'echo l:legacyvar', ['in legacy']) + call s:RunDbgCmd(buf, 'echo a:text', ['arg']) + + " Move up to the frame of the :def function, the local variables of the + " legacy function are not available there. This used to crash. + call s:RunDbgCmd(buf, 'up') + call s:RunDbgCmd(buf, 'echo legacyvar', + \ ['E121: Undefined variable: legacyvar']) + call s:RunDbgCmd(buf, 'echo l:legacyvar', + \ ['E121: Undefined variable: l:legacyvar']) + call s:RunDbgCmd(buf, 'echo a:text', ['E121: Undefined variable: a:text']) + + " Back in the frame of the legacy function the variables are found again. + call s:RunDbgCmd(buf, 'down') + call s:RunDbgCmd(buf, 'echo legacyvar', ['in legacy']) + + call s:RunDbgCmd(buf, 'cont') + call StopVimInTerminal(buf) +endfunc + func Test_debug_def_function() CheckRunVimInTerminal CheckCWD