backport: fix(api): leak preview callback LuaRef in nvim_create_user_command (#39377)

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
    <path/to/nvim> --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:`.

(cherry picked from commit 393f687503)

Co-authored-by: Barrett Ruth <62671086+barrettruth@users.noreply.github.com>
This commit is contained in:
neovim-backports[bot]
2026-04-24 17:27:46 -04:00
committed by GitHub
parent 4df16ecdb9
commit e5d6d2e769
2 changed files with 22 additions and 1 deletions

View File

@@ -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.

View File

@@ -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 = {}