From 393f687503a319a6f521e8335b4dd8030e3ea67b Mon Sep 17 00:00:00 2001 From: Barrett Ruth <62671086+barrettruth@users.noreply.github.com> Date: Fri, 24 Apr 2026 14:13:24 -0400 Subject: [PATCH] fix(api): leak preview callback LuaRef in nvim_create_user_command #39357 Problem: Invalid `nvim_create_user_command` calls can leak the `preview` callback reference after Neovim has taken ownership of it. 1. build with {a,l}san 2. run: ```sh --headless -u NONE --clean +'lua for i = 1, 100 do pcall(vim.api.nvim_create_user_command, "some very epic stuff" .. i, {}, -- NOTE: this is INVALID (not a function or string) { preview = function() end }) end vim.cmd("qa!") ' +qa ``` 3. see: ``` 100 lua references were leaked! ``` Solution: Clear `preview_luaref` in `err:`. --- src/nvim/api/command.c | 4 +++- test/functional/api/command_spec.lua | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/nvim/api/command.c b/src/nvim/api/command.c index 50febf1af4..063b99c794 100644 --- a/src/nvim/api/command.c +++ b/src/nvim/api/command.c @@ -1279,7 +1279,8 @@ void create_user_command(uint64_t channel_id, String name, Union(String, LuaRef) if (uc_add_command(name.data, name.size, rep, argt, def, flags, context, compl_arg, compl_luaref, preview_luaref, addr_type_arg, luaref, force) != OK) { api_set_error(err, kErrorTypeException, "Failed to create user command"); - // Do not goto err, since uc_add_command now owns luaref, compl_luaref, and compl_arg + // Do not goto err, since uc_add_command now owns luaref, compl_luaref, preview_luaref, + // and compl_arg } }); @@ -1288,6 +1289,7 @@ void create_user_command(uint64_t channel_id, String name, Union(String, LuaRef) err: NLUA_CLEAR_REF(luaref); NLUA_CLEAR_REF(compl_luaref); + NLUA_CLEAR_REF(preview_luaref); xfree(compl_arg); } /// Gets a map of global (non-buffer-local) Ex commands. diff --git a/test/functional/api/command_spec.lua b/test/functional/api/command_spec.lua index 503b4a48b3..f02f041b24 100644 --- a/test/functional/api/command_spec.lua +++ b/test/functional/api/command_spec.lua @@ -274,6 +274,25 @@ describe('nvim_create_user_command', function() eq(42, api.nvim_eval('g:command_fired')) end) + it('does not leak `preview` LuaRef on invalid `cmd`', function() + local released = exec_lua(function() + local weak = setmetatable({}, { __mode = 'v' }) + for i = 1, 10 do + local cb = function() end + weak[i] = cb + pcall(vim.api.nvim_create_user_command, 'Bogus' .. i, {}, { preview = cb }) + end + collectgarbage('collect') + collectgarbage('collect') + local n = 0 + for _ in pairs(weak) do + n = n + 1 + end + return n + end) + eq(0, released) + end) + it('works with Lua functions', function() exec_lua [[ result = {}