changeset 31231:684e6dfa2fba v9.0.0949

patch 9.0.0949: crash when unletting a variable while listing variables Commit: https://github.com/vim/vim/commit/ef2c325f5e3c437b722bb96bf369ba2a5c541163 Author: Bram Moolenaar <Bram@vim.org> Date: Fri Nov 25 16:31:51 2022 +0000 patch 9.0.0949: crash when unletting a variable while listing variables Problem: Crash when unletting a variable while listing variables. Solution: Disallow changing a hashtable while going over the entries. (closes #11435)
author Bram Moolenaar <Bram@vim.org>
date Fri, 25 Nov 2022 17:45:04 +0100
parents e93800d75da2
children c4a37ba73128
files src/buffer.c src/dict.c src/errors.h src/evalvars.c src/hashtab.c src/if_lua.c src/if_py_both.h src/if_ruby.c src/proto/dict.pro src/proto/hashtab.pro src/sign.c src/spellfile.c src/structs.h src/syntax.c src/terminal.c src/testdir/test_autocmd.vim src/textprop.c src/userfunc.c src/version.c src/vim9execute.c src/vim9script.c
diffstat 21 files changed, 143 insertions(+), 68 deletions(-) [+]
line wrap: on
line diff
--- a/src/buffer.c
+++ b/src/buffer.c
@@ -434,7 +434,7 @@ static hashtab_T buf_hashtab;
 buf_hashtab_add(buf_T *buf)
 {
     sprintf((char *)buf->b_key, "%x", buf->b_fnum);
-    if (hash_add(&buf_hashtab, buf->b_key) == FAIL)
+    if (hash_add(&buf_hashtab, buf->b_key, "create buffer") == FAIL)
 	emsg(_(e_buffer_cannot_be_registered));
 }
 
@@ -444,7 +444,7 @@ buf_hashtab_remove(buf_T *buf)
     hashitem_T *hi = hash_find(&buf_hashtab, buf->b_key);
 
     if (!HASHITEM_EMPTY(hi))
-	hash_remove(&buf_hashtab, hi);
+	hash_remove(&buf_hashtab, hi, "close buffer");
 }
 
 /*
@@ -925,7 +925,7 @@ free_buffer(buf_T *buf)
     free_buffer_stuff(buf, TRUE);
 #ifdef FEAT_EVAL
     // b:changedtick uses an item in buf_T, remove it now
-    dictitem_remove(buf->b_vars, (dictitem_T *)&buf->b_ct_di);
+    dictitem_remove(buf->b_vars, (dictitem_T *)&buf->b_ct_di, "free buffer");
     unref_var_dict(buf->b_vars);
     remove_listeners(buf);
 #endif
--- a/src/dict.c
+++ b/src/dict.c
@@ -122,6 +122,9 @@ hashtab_free_contents(hashtab_T *ht)
     hashitem_T	*hi;
     dictitem_T	*di;
 
+    if (check_hashtab_frozen(ht, "clear dict"))
+	return;
+
     // Lock the hashtab, we don't want it to resize while freeing items.
     hash_lock(ht);
     todo = (int)ht->ht_used;
@@ -132,7 +135,7 @@ hashtab_free_contents(hashtab_T *ht)
 	    // Remove the item before deleting it, just in case there is
 	    // something recursive causing trouble.
 	    di = HI2DI(hi);
-	    hash_remove(ht, hi);
+	    hash_remove(ht, hi, "clear dict");
 	    dictitem_free(di);
 	    --todo;
 	}
@@ -256,9 +259,10 @@ dictitem_copy(dictitem_T *org)
 
 /*
  * Remove item "item" from Dictionary "dict" and free it.
+ * "command" is used for the error message when the hashtab if frozen.
  */
     void
-dictitem_remove(dict_T *dict, dictitem_T *item)
+dictitem_remove(dict_T *dict, dictitem_T *item, char *command)
 {
     hashitem_T	*hi;
 
@@ -266,7 +270,7 @@ dictitem_remove(dict_T *dict, dictitem_T
     if (HASHITEM_EMPTY(hi))
 	internal_error("dictitem_remove()");
     else
-	hash_remove(&dict->dv_hashtab, hi);
+	hash_remove(&dict->dv_hashtab, hi, command);
     dictitem_free(item);
 }
 
@@ -375,7 +379,7 @@ dict_add(dict_T *d, dictitem_T *item)
 {
     if (dict_wrong_func_name(d, &item->di_tv, item->di_key))
 	return FAIL;
-    return hash_add(&d->dv_hashtab, item->di_key);
+    return hash_add(&d->dv_hashtab, item->di_key, "add to dictionary");
 }
 
 /*
@@ -1094,14 +1098,21 @@ dict_extend(dict_T *d1, dict_T *d2, char
     char_u	*arg_errmsg = (char_u *)N_("extend() argument");
     type_T	*type;
 
+    if (check_hashtab_frozen(&d1->dv_hashtab, "extend"))
+	return;
+
+    if (*action == 'm')
+    {
+	if (check_hashtab_frozen(&d2->dv_hashtab, "extend"))
+	    return;
+	hash_lock(&d2->dv_hashtab);  // don't rehash on hash_remove()
+    }
+
     if (d1->dv_type != NULL && d1->dv_type->tt_member != NULL)
 	type = d1->dv_type->tt_member;
     else
 	type = NULL;
 
-    if (*action == 'm')
-	hash_lock(&d2->dv_hashtab);  // don't rehash on hash_remove()
-
     todo = (int)d2->dv_hashtab.ht_used;
     for (hashitem_T *hi2 = d2->dv_hashtab.ht_array; todo > 0; ++hi2)
     {
@@ -1126,7 +1137,7 @@ dict_extend(dict_T *d1, dict_T *d2, char
 		    // If dict_add() fails then "d2" won't be empty.
 		    di1 = HI2DI(hi2);
 		    if (dict_add(d1, di1) == OK)
-			hash_remove(&d2->dv_hashtab, hi2);
+			hash_remove(&d2->dv_hashtab, hi2, "extend");
 		}
 		else
 		{
@@ -1406,7 +1417,7 @@ dict_filter_map(
 		if (var_check_fixed(di->di_flags, arg_errmsg, TRUE)
 			|| var_check_ro(di->di_flags, arg_errmsg, TRUE))
 		    break;
-		dictitem_remove(d, di);
+		dictitem_remove(d, di, "filter");
 	    }
 	}
     }
@@ -1453,7 +1464,7 @@ dict_remove(typval_T *argvars, typval_T 
 
     *rettv = di->di_tv;
     init_tv(&di->di_tv);
-    dictitem_remove(d, di);
+    dictitem_remove(d, di, "remove()");
 }
 
 typedef enum {
--- a/src/errors.h
+++ b/src/errors.h
@@ -3343,3 +3343,5 @@ EXTERN char e_cannot_change_user_command
 	INIT(= N_("E1311: Cannot change user commands while listing"));
 EXTERN char e_not_allowed_to_change_window_layout_in_this_autocmd[]
 	INIT(= N_("E1312: Not allowed to change the window layout in this autocmd"));
+EXTERN char e_not_allowed_to_add_or_remove_entries_str[]
+	INIT(= N_("E1313: Not allowed to add or remove entries (%s)"));
--- a/src/evalvars.c
+++ b/src/evalvars.c
@@ -217,10 +217,10 @@ evalvars_init(void)
 
 	// add to v: scope dict, unless the value is not always available
 	if (p->vv_tv_type != VAR_UNKNOWN)
-	    hash_add(&vimvarht, p->vv_di.di_key);
+	    hash_add(&vimvarht, p->vv_di.di_key, "initialization");
 	if (p->vv_flags & VV_COMPAT)
 	    // add to compat scope dict
-	    hash_add(&compat_hashtab, p->vv_di.di_key);
+	    hash_add(&compat_hashtab, p->vv_di.di_key, "initialization");
     }
     set_vim_var_nr(VV_VERSION, VIM_VERSION_100);
     set_vim_var_nr(VV_VERSIONLONG, VIM_VERSION_100 * 10000 + highest_patch());
@@ -562,7 +562,7 @@ prepare_vimvar(int idx, typval_T *save_t
     *save_tv = vimvars[idx].vv_tv;
     vimvars[idx].vv_str = NULL;  // don't free it now
     if (vimvars[idx].vv_tv_type == VAR_UNKNOWN)
-	hash_add(&vimvarht, vimvars[idx].vv_di.di_key);
+	hash_add(&vimvarht, vimvars[idx].vv_di.di_key, "prepare vimvar");
 }
 
 /*
@@ -582,7 +582,7 @@ restore_vimvar(int idx, typval_T *save_t
 	if (HASHITEM_EMPTY(hi))
 	    internal_error("restore_vimvar()");
 	else
-	    hash_remove(&vimvarht, hi);
+	    hash_remove(&vimvarht, hi, "restore vimvar");
     }
 }
 
@@ -1380,6 +1380,9 @@ list_hashtable_vars(
     int		todo;
     char_u	buf[IOSIZE];
 
+    int save_ht_flags = ht->ht_flags;
+    ht->ht_flags |= HTFLAGS_FROZEN;
+
     todo = (int)ht->ht_used;
     for (hi = ht->ht_array; todo > 0 && !got_int; ++hi)
     {
@@ -1399,6 +1402,8 @@ list_hashtable_vars(
 		list_one_var(di, prefix, first);
 	}
     }
+
+    ht->ht_flags = save_ht_flags;
 }
 
 /*
@@ -2008,7 +2013,7 @@ do_unlet_var(
 	listitem_remove(lp->ll_list, lp->ll_li);
     else
 	// unlet a Dictionary item.
-	dictitem_remove(lp->ll_dict, lp->ll_di);
+	dictitem_remove(lp->ll_dict, lp->ll_di, "unlet");
 
     return ret;
 }
@@ -2095,7 +2100,8 @@ do_unlet(char_u *name, int forceit)
 	    di = HI2DI(hi);
 	    if (var_check_fixed(di->di_flags, name, FALSE)
 		    || var_check_ro(di->di_flags, name, FALSE)
-		    || value_check_lock(d->dv_lock, name, FALSE))
+		    || value_check_lock(d->dv_lock, name, FALSE)
+		    || check_hashtab_frozen(ht, "unlet"))
 		return FAIL;
 
 	    delete_var(ht, hi);
@@ -3554,9 +3560,11 @@ delete_var(hashtab_T *ht, hashitem_T *hi
 {
     dictitem_T	*di = HI2DI(hi);
 
-    hash_remove(ht, hi);
-    clear_tv(&di->di_tv);
-    vim_free(di);
+    if (hash_remove(ht, hi, "delete variable") == OK)
+    {
+	clear_tv(&di->di_tv);
+	vim_free(di);
+    }
 }
 
 /*
@@ -3895,6 +3903,9 @@ set_var_const(
 	    goto failed;
 	}
 
+	if (check_hashtab_frozen(ht, "add variable"))
+	    goto failed;
+
 	// Can't add "v:" or "a:" variable.
 	if (ht == &vimvarht || ht == get_funccal_args_ht())
 	{
@@ -3913,7 +3924,7 @@ set_var_const(
 	if (di == NULL)
 	    goto failed;
 	STRCPY(di->di_key, varname);
-	if (hash_add(ht, DI2HIKEY(di)) == FAIL)
+	if (hash_add(ht, DI2HIKEY(di), "add variable") == FAIL)
 	{
 	    vim_free(di);
 	    goto failed;
--- a/src/hashtab.c
+++ b/src/hashtab.c
@@ -71,6 +71,20 @@ hash_init(hashtab_T *ht)
 }
 
 /*
+ * If "ht->ht_flags" has HTFLAGS_FROZEN then give an error message using
+ * "command" and return TRUE.
+ */
+    int
+check_hashtab_frozen(hashtab_T *ht, char *command)
+{
+    if ((ht->ht_flags & HTFLAGS_FROZEN) == 0)
+	return FALSE;
+
+    semsg(_(e_not_allowed_to_add_or_remove_entries_str), command);
+    return TRUE;
+}
+
+/*
  * Free the array of a hash table.  Does not free the items it contains!
  * If "ht" is not freed then you should call hash_init() next!
  */
@@ -201,14 +215,17 @@ hash_debug_results(void)
 
 /*
  * Add item with key "key" to hashtable "ht".
+ * "command" is used for the error message when the hashtab if frozen.
  * Returns FAIL when out of memory or the key is already present.
  */
     int
-hash_add(hashtab_T *ht, char_u *key)
+hash_add(hashtab_T *ht, char_u *key, char *command)
 {
     hash_T	hash = hash_hash(key);
     hashitem_T	*hi;
 
+    if (check_hashtab_frozen(ht, command))
+	return FAIL;
     hi = hash_lookup(ht, key, hash);
     if (!HASHITEM_EMPTY(hi))
     {
@@ -232,7 +249,7 @@ hash_add_item(
     hash_T	hash)
 {
     // If resizing failed before and it fails again we can't add an item.
-    if (ht->ht_error && hash_may_resize(ht, 0) == FAIL)
+    if ((ht->ht_flags & HTFLAGS_ERROR) && hash_may_resize(ht, 0) == FAIL)
 	return FAIL;
 
     ++ht->ht_used;
@@ -266,15 +283,19 @@ hash_set(hashitem_T *hi, char_u *key)
 /*
  * Remove item "hi" from  hashtable "ht".  "hi" must have been obtained with
  * hash_lookup().
+ * "command" is used for the error message when the hashtab if frozen.
  * The caller must take care of freeing the item itself.
  */
-    void
-hash_remove(hashtab_T *ht, hashitem_T *hi)
+    int
+hash_remove(hashtab_T *ht, hashitem_T *hi, char *command)
 {
+    if (check_hashtab_frozen(ht, command))
+	return FAIL;
     --ht->ht_used;
     ++ht->ht_changed;
     hi->hi_key = HI_KEY_REMOVED;
     hash_may_resize(ht, 0);
+    return OK;
 }
 
 /*
@@ -407,11 +428,11 @@ hash_may_resize(
 	if (newarray == NULL)
 	{
 	    // Out of memory.  When there are NULL items still return OK.
-	    // Otherwise set ht_error, because lookup may result in a hang if
-	    // we add another item.
+	    // Otherwise set ht_flags to HTFLAGS_ERROR, because lookup may
+	    // result in a hang if we add another item.
 	    if (ht->ht_filled < ht->ht_mask)
 		return OK;
-	    ht->ht_error = TRUE;
+	    ht->ht_flags |= HTFLAGS_ERROR;
 	    return FAIL;
 	}
 	oldarray = ht->ht_array;
@@ -453,7 +474,7 @@ hash_may_resize(
     ht->ht_mask = newmask;
     ht->ht_filled = ht->ht_used;
     ++ht->ht_changed;
-    ht->ht_error = FALSE;
+    ht->ht_flags &= ~HTFLAGS_ERROR;
 
     return OK;
 }
--- a/src/if_lua.c
+++ b/src/if_lua.c
@@ -1150,7 +1150,7 @@ luaV_dict_newindex(lua_State *L)
     if (lua_isnil(L, 3)) // remove?
     {
 	hashitem_T *hi = hash_find(&d->dv_hashtab, di->di_key);
-	hash_remove(&d->dv_hashtab, hi);
+	hash_remove(&d->dv_hashtab, hi, "Lua new index");
 	dictitem_free(di);
     }
     else
@@ -1838,9 +1838,8 @@ luaV_setvar(lua_State *L)
 	if (di == NULL)
 	    // Doesn't exist, nothing to do
 	    return 0;
-	else
-	    // Delete the entry
-	    dictitem_remove(dict, di);
+	// Delete the entry
+	dictitem_remove(dict, di, "Lua delete variable");
     }
     else
     {
--- a/src/if_py_both.h
+++ b/src/if_py_both.h
@@ -1768,7 +1768,7 @@ DictionaryLength(DictionaryObject *self)
 	    return NULL;
 	}
 
-	hash_remove(&dict->dv_hashtab, hi);
+	hash_remove(&dict->dv_hashtab, hi, "Python remove variable");
 	dictitem_free(di);
     }
 
@@ -1893,7 +1893,7 @@ DictionaryAssItem(
 	    return -1;
 	}
 	hi = hash_find(&dict->dv_hashtab, di->di_key);
-	hash_remove(&dict->dv_hashtab, hi);
+	hash_remove(&dict->dv_hashtab, hi, "Python remove item");
 	dictitem_free(di);
 	Py_XDECREF(todecref);
 	return 0;
@@ -2194,7 +2194,7 @@ DictionaryPopItem(DictionaryObject *self
 	return NULL;
     }
 
-    hash_remove(&self->dict->dv_hashtab, hi);
+    hash_remove(&self->dict->dv_hashtab, hi, "Python pop item");
     dictitem_free(di);
 
     return ret;
--- a/src/if_ruby.c
+++ b/src/if_ruby.c
@@ -1799,7 +1799,7 @@ convert_hash2dict(VALUE key, VALUE val, 
     if (di == NULL || ruby_convert_to_vim_value(val, &di->di_tv) != OK
 						     || dict_add(d, di) != OK)
     {
-	d->dv_hashtab.ht_error = TRUE;
+	d->dv_hashtab.ht_flags |= HTFLAGS_ERROR;
 	return ST_STOP;
     }
     return ST_CONTINUE;
@@ -1879,7 +1879,7 @@ ruby_convert_to_vim_value(VALUE val, typ
 		    return FAIL;
 
 		rb_hash_foreach(val, convert_hash2dict, (VALUE)d);
-		if (d->dv_hashtab.ht_error)
+		if (d->dv_hashtab.ht_flags & HTFLAGS_ERROR)
 		{
 		    dict_unref(d);
 		    return FAIL;
--- a/src/proto/dict.pro
+++ b/src/proto/dict.pro
@@ -10,7 +10,7 @@ void dict_unref(dict_T *d);
 int dict_free_nonref(int copyID);
 void dict_free_items(int copyID);
 dictitem_T *dictitem_alloc(char_u *key);
-void dictitem_remove(dict_T *dict, dictitem_T *item);
+void dictitem_remove(dict_T *dict, dictitem_T *item, char *command);
 void dictitem_free(dictitem_T *item);
 dict_T *dict_copy(dict_T *orig, int deep, int top, int copyID);
 int dict_wrong_func_name(dict_T *d, typval_T *tv, char_u *name);
--- a/src/proto/hashtab.pro
+++ b/src/proto/hashtab.pro
@@ -1,13 +1,14 @@
 /* hashtab.c */
 void hash_init(hashtab_T *ht);
+int check_hashtab_frozen(hashtab_T *ht, char *command);
 void hash_clear(hashtab_T *ht);
 void hash_clear_all(hashtab_T *ht, int off);
 hashitem_T *hash_find(hashtab_T *ht, char_u *key);
 hashitem_T *hash_lookup(hashtab_T *ht, char_u *key, hash_T hash);
 void hash_debug_results(void);
-int hash_add(hashtab_T *ht, char_u *key);
+int hash_add(hashtab_T *ht, char_u *key, char *command);
 int hash_add_item(hashtab_T *ht, hashitem_T *hi, char_u *key, hash_T hash);
-void hash_remove(hashtab_T *ht, hashitem_T *hi);
+int hash_remove(hashtab_T *ht, hashitem_T *hi, char *command);
 void hash_lock(hashtab_T *ht);
 void hash_lock_size(hashtab_T *ht, int size);
 void hash_unlock(hashtab_T *ht);
--- a/src/sign.c
+++ b/src/sign.c
@@ -126,7 +126,7 @@ sign_group_unref(char_u *groupname)
 	if (group->sg_refcount == 0)
 	{
 	    // All the signs in this group are removed
-	    hash_remove(&sg_table, hi);
+	    hash_remove(&sg_table, hi, "sign remove");
 	    vim_free(group);
 	}
     }
--- a/src/spellfile.c
+++ b/src/spellfile.c
@@ -2643,7 +2643,7 @@ spell_read_aff(spellinfo_T *spin, char_u
 			smsg(_("Affix also used for BAD/RARE/KEEPCASE/NEEDAFFIX/NEEDCOMPOUND/NOSUGGEST in %s line %d: %s"),
 						       fname, lnum, items[1]);
 		    STRCPY(cur_aff->ah_key, items[1]);
-		    hash_add(tp, cur_aff->ah_key);
+		    hash_add(tp, cur_aff->ah_key, "spelling");
 
 		    cur_aff->ah_combine = (*items[2] == 'Y');
 		}
@@ -2994,7 +2994,7 @@ spell_read_aff(spellinfo_T *spin, char_u
 			p = vim_strsave(items[i]);
 			if (p == NULL)
 			    break;
-			hash_add(&spin->si_commonwords, p);
+			hash_add(&spin->si_commonwords, p, "spelling");
 		    }
 		}
 	    }
@@ -3312,7 +3312,7 @@ process_compflags(
 			id = spin->si_newcompID--;
 		    } while (vim_strchr((char_u *)"/?*+[]\\-^", id) != NULL);
 		    ci->ci_newID = id;
-		    hash_add(&aff->af_comp, ci->ci_key);
+		    hash_add(&aff->af_comp, ci->ci_key, "spelling");
 		}
 		*tp++ = id;
 	    }
--- a/src/structs.h
+++ b/src/structs.h
@@ -1313,6 +1313,12 @@ typedef struct hashitem_S
 // This allows for storing 10 items (2/3 of 16) before a resize is needed.
 #define HT_INIT_SIZE 16
 
+// flags used for ht_flags
+#define HTFLAGS_ERROR	0x01	// Set when growing failed, can't add more
+				// items before growing works.
+#define HTFLAGS_FROZEN	0x02	// Trying to add or remove an item will result
+				// in an error message.
+
 typedef struct hashtable_S
 {
     long_u	ht_mask;	// mask used for hash value (nr of items in
@@ -1321,8 +1327,7 @@ typedef struct hashtable_S
     long_u	ht_filled;	// number of items used + removed
     int		ht_changed;	// incremented when adding or removing an item
     int		ht_locked;	// counter for hash_lock()
-    int		ht_error;	// when set growing failed, can't add more
-				// items before growing works
+    int		ht_flags;	// HTFLAGS_ values
     hashitem_T	*ht_array;	// points to the array, allocated when it's
 				// not "ht_smallarray"
     hashitem_T	ht_smallarray[HT_INIT_SIZE];   // initial array
--- a/src/syntax.c
+++ b/src/syntax.c
@@ -4339,7 +4339,7 @@ syn_clear_keyword(int id, hashtab_T *ht)
 		    if (kp_prev == NULL)
 		    {
 			if (kp_next == NULL)
-			    hash_remove(ht, hi);
+			    hash_remove(ht, hi, "syntax clear keyword");
 			else
 			    hi->hi_key = KE2HIKEY(kp_next);
 		    }
--- a/src/terminal.c
+++ b/src/terminal.c
@@ -1020,7 +1020,7 @@ term_write_session(FILE *fd, win_T *wp, 
 	char *hash_key = alloc(NUMBUFLEN);
 
 	vim_snprintf(hash_key, NUMBUFLEN, "%d", bufnr);
-	hash_add(terminal_bufs, (char_u *)hash_key);
+	hash_add(terminal_bufs, (char_u *)hash_key, "terminal session");
     }
 
     return put_eol(fd);
--- a/src/testdir/test_autocmd.vim
+++ b/src/testdir/test_autocmd.vim
@@ -2326,6 +2326,28 @@ func Test_autocmd_user_clear_group()
   call StopVimInTerminal(buf)
 endfunc
 
+func Test_autocmd_CmdlineLeave_unlet()
+  CheckRunVimInTerminal
+
+  let lines =<< trim END
+      for i in range(1, 999)
+        exe 'let g:var' .. i '=' i
+      endfor
+      au CmdlineLeave : call timer_start(0, {-> execute('unlet g:var990')})
+  END
+  call writefile(lines, 'XleaveUnlet', 'D')
+  let buf = RunVimInTerminal('-S XleaveUnlet', {'rows': 10})
+
+  " this was using freed memory
+  call term_sendkeys(buf, ":let g:\<CR>")
+  call TermWait(buf, 50)
+  call term_sendkeys(buf, "G")
+  call TermWait(buf, 50)
+  call term_sendkeys(buf, "\<CR>")  " for the hit-enter prompt
+
+  call StopVimInTerminal(buf)
+endfunc
+
 function s:Before_test_dirchanged()
   augroup test_dirchanged
     autocmd!
--- a/src/textprop.c
+++ b/src/textprop.c
@@ -1789,7 +1789,7 @@ prop_type_set(typval_T *argvars, int add
 	    }
 	    hash_init(*htp);
 	}
-	hash_add(*htp, PT2HIKEY(prop));
+	hash_add(*htp, PT2HIKEY(prop), "prop type");
     }
     else
     {
@@ -1924,7 +1924,7 @@ f_prop_type_delete(typval_T *argvars, ty
 	    ht = buf->b_proptypes;
 	    VIM_CLEAR(buf->b_proparray);
 	}
-	hash_remove(ht, hi);
+	hash_remove(ht, hi, "prop type delete");
 	vim_free(prop);
     }
 }
--- a/src/userfunc.c
+++ b/src/userfunc.c
@@ -585,7 +585,7 @@ register_cfunc(cfunc_T cb, cfunc_free_T 
     fp->uf_cb_state = state;
 
     set_ufunc_name(fp, name);
-    hash_add(&func_hashtab, UF2HIKEY(fp));
+    hash_add(&func_hashtab, UF2HIKEY(fp), "add C function");
 
     return name;
 }
@@ -1278,7 +1278,7 @@ lambda_function_body(
     if (ufunc == NULL)
 	goto erret;
     set_ufunc_name(ufunc, name);
-    if (hash_add(&func_hashtab, UF2HIKEY(ufunc)) == FAIL)
+    if (hash_add(&func_hashtab, UF2HIKEY(ufunc), "add function") == FAIL)
 	goto erret;
     ufunc->uf_flags = FC_LAMBDA;
     ufunc->uf_refcount = 1;
@@ -1572,7 +1572,7 @@ get_lambda_tv(
 	rettv->vval.v_partial = pt;
 	rettv->v_type = VAR_PARTIAL;
 
-	hash_add(&func_hashtab, UF2HIKEY(fp));
+	hash_add(&func_hashtab, UF2HIKEY(fp), "add lambda");
     }
 
 theend:
@@ -2128,7 +2128,7 @@ add_nr_var(
 {
     STRCPY(v->di_key, name);
     v->di_flags = DI_FLAGS_RO | DI_FLAGS_FIX;
-    hash_add(&dp->dv_hashtab, DI2HIKEY(v));
+    hash_add(&dp->dv_hashtab, DI2HIKEY(v), "add variable");
     v->di_tv.v_type = VAR_NUMBER;
     v->di_tv.v_lock = VAR_FIXED;
     v->di_tv.vval.v_number = nr;
@@ -2348,7 +2348,7 @@ func_remove(ufunc_T *fp)
 	    fp->uf_flags |= FC_DEAD;
 	    return FALSE;
 	}
-	hash_remove(&func_hashtab, hi);
+	hash_remove(&func_hashtab, hi, "remove function");
 	fp->uf_flags |= FC_DELETED;
 	return TRUE;
     }
@@ -2510,7 +2510,7 @@ copy_lambda_to_global_func(
 
     fp->uf_refcount = 1;
     STRCPY(fp->uf_name, global);
-    hash_add(&func_hashtab, UF2HIKEY(fp));
+    hash_add(&func_hashtab, UF2HIKEY(fp), "copy lambda");
 
     // the referenced dfunc_T is now used one more time
     link_def_function(fp);
@@ -2718,7 +2718,7 @@ call_user_func(
 	name = v->di_key;
 	STRCPY(name, "self");
 	v->di_flags = DI_FLAGS_RO | DI_FLAGS_FIX;
-	hash_add(&fc->fc_l_vars.dv_hashtab, DI2HIKEY(v));
+	hash_add(&fc->fc_l_vars.dv_hashtab, DI2HIKEY(v), "set self dictionary");
 	v->di_tv.v_type = VAR_DICT;
 	v->di_tv.v_lock = 0;
 	v->di_tv.vval.v_dict = selfdict;
@@ -2744,7 +2744,7 @@ call_user_func(
 	name = v->di_key;
 	STRCPY(name, "000");
 	v->di_flags = DI_FLAGS_RO | DI_FLAGS_FIX;
-	hash_add(&fc->fc_l_avars.dv_hashtab, DI2HIKEY(v));
+	hash_add(&fc->fc_l_avars.dv_hashtab, DI2HIKEY(v), "function argument");
 	v->di_tv.v_type = VAR_LIST;
 	v->di_tv.v_lock = VAR_FIXED;
 	v->di_tv.vval.v_list = &fc->fc_l_varlist;
@@ -2838,10 +2838,10 @@ call_user_func(
 	    // Named arguments should be accessed without the "a:" prefix in
 	    // lambda expressions.  Add to the l: dict.
 	    copy_tv(&v->di_tv, &v->di_tv);
-	    hash_add(&fc->fc_l_vars.dv_hashtab, DI2HIKEY(v));
+	    hash_add(&fc->fc_l_vars.dv_hashtab, DI2HIKEY(v), "local variable");
 	}
 	else
-	    hash_add(&fc->fc_l_avars.dv_hashtab, DI2HIKEY(v));
+	    hash_add(&fc->fc_l_avars.dv_hashtab, DI2HIKEY(v), "add variable");
 
 	if (ai >= 0 && ai < MAX_FUNC_ARGS)
 	{
@@ -5060,7 +5060,7 @@ define_function(exarg_T *eap, char_u *na
 	    hi = hash_find(&func_hashtab, name);
 	    hi->hi_key = UF2HIKEY(fp);
 	}
-	else if (hash_add(&func_hashtab, UF2HIKEY(fp)) == FAIL)
+	else if (hash_add(&func_hashtab, UF2HIKEY(fp), "add function") == FAIL)
 	{
 	    free_fp = TRUE;
 	    goto erret;
@@ -5462,7 +5462,7 @@ ex_delfunction(exarg_T *eap)
 	{
 	    // Delete the dict item that refers to the function, it will
 	    // invoke func_unref() and possibly delete the function.
-	    dictitem_remove(fudi.fd_dict, fudi.fd_di);
+	    dictitem_remove(fudi.fd_dict, fudi.fd_di, "delfunction");
 	}
 	else
 	{
--- a/src/version.c
+++ b/src/version.c
@@ -696,6 +696,8 @@ static char *(features[]) =
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    949,
+/**/
     948,
 /**/
     947,
--- a/src/vim9execute.c
+++ b/src/vim9execute.c
@@ -2366,7 +2366,7 @@ execute_unletindex(isn_T *iptr, ectx_T *
 						  NULL, FALSE))
 		    status = FAIL;
 		else
-		    dictitem_remove(d, di);
+		    dictitem_remove(d, di, "unlet");
 	    }
 	}
     }
--- a/src/vim9script.c
+++ b/src/vim9script.c
@@ -942,7 +942,8 @@ update_vim9_script_var(
 
 	    if (HASHITEM_EMPTY(hi))
 		// new variable name
-		hash_add(&si->sn_all_vars.dv_hashtab, newsav->sav_key);
+		hash_add(&si->sn_all_vars.dv_hashtab, newsav->sav_key,
+							       "add variable");
 	    else if (sav != NULL)
 		// existing name in a new block, append to the list
 		sav->sav_next = newsav;
@@ -1033,7 +1034,7 @@ hide_script_var(scriptitem_T *si, int id
 	    else
 	    {
 		if (sav_prev == NULL)
-		    hash_remove(all_ht, all_hi);
+		    hash_remove(all_ht, all_hi, "hide variable");
 		else
 		    sav_prev->sav_next = sav->sav_next;
 		sv->sv_name = NULL;