commit 4399250e906397f9ea359d9d4934c2f2dea1a0a5
parent a4ee34a165ef5b190378740d2c4ce15410f0f477
Author: zeertzjq <zeertzjq@outlook.com>
Date: Mon, 12 Jan 2026 08:33:19 +0800
fix(channel): unreference list after callback finishes (#37358)
Diffstat:
4 files changed, 53 insertions(+), 5 deletions(-)
diff --git a/src/nvim/channel.c b/src/nvim/channel.c
@@ -807,6 +807,10 @@ static void channel_callback_call(Channel *chan, CallbackReader *reader)
typval_T rettv = TV_INITIAL_VALUE;
callback_call(cb, 3, argv, &rettv);
tv_clear(&rettv);
+
+ if (reader) {
+ tv_list_unref(argv[1].vval.v_list);
+ }
}
/// Open terminal for channel
diff --git a/src/nvim/eval/gc.c b/src/nvim/eval/gc.c
@@ -5,6 +5,6 @@
#include "eval/gc.c.generated.h" // IWYU pragma: export
/// Head of list of all dictionaries
-dict_T *gc_first_dict = NULL;
+DLLEXPORT dict_T *gc_first_dict = NULL;
/// Head of list of all lists
-list_T *gc_first_list = NULL;
+DLLEXPORT list_T *gc_first_list = NULL;
diff --git a/src/nvim/eval/gc.h b/src/nvim/eval/gc.h
@@ -2,7 +2,7 @@
#include "nvim/eval/typval_defs.h"
-extern dict_T *gc_first_dict;
-extern list_T *gc_first_list;
-
#include "eval/gc.h.generated.h"
+
+DLLEXPORT extern dict_T *gc_first_dict;
+DLLEXPORT extern list_T *gc_first_list;
diff --git a/test/functional/core/job_spec.lua b/test/functional/core/job_spec.lua
@@ -734,6 +734,50 @@ describe('jobs', function()
)
end)
+ it('lists passed to callbacks are freed if not stored #25891', function()
+ if not exec_lua('return pcall(require, "ffi")') then
+ pending('missing LuaJIT FFI')
+ end
+
+ source([[
+ let g:stdout = ''
+ func AppendStrOnEvent(id, data, event)
+ let g:stdout ..= join(a:data, "\n")
+ endfunc
+ let g:job_opts = {'on_stdout': function('AppendStrOnEvent')}
+ ]])
+ local job = eval([[jobstart(['cat', '-'], g:job_opts)]])
+
+ exec_lua(function()
+ local ffi = require('ffi')
+ ffi.cdef([[
+ typedef struct listvar_S list_T;
+ list_T *gc_first_list;
+ list_T *tv_list_alloc(ptrdiff_t len);
+ void tv_list_free(list_T *const l);
+ ]])
+ _G.L = ffi.C.tv_list_alloc(1)
+ _G.L_val = ffi.cast('uintptr_t', _G.L)
+ assert(ffi.cast('uintptr_t', ffi.C.gc_first_list) == _G.L_val)
+ end)
+
+ local str_all = ''
+ for _, str in ipairs({ 'LINE1\nLINE2\nLINE3\n', 'LINE4\n', 'LINE5\nLINE6\n' }) do
+ str_all = str_all .. str
+ api.nvim_chan_send(job, str)
+ retry(nil, 1000, function()
+ eq(str_all, api.nvim_get_var('stdout'))
+ end)
+ end
+
+ exec_lua(function()
+ local ffi = require('ffi')
+ assert(ffi.cast('uintptr_t', ffi.C.gc_first_list) == _G.L_val)
+ ffi.C.tv_list_free(_G.L)
+ assert(ffi.cast('uintptr_t', ffi.C.gc_first_list) ~= _G.L_val)
+ end)
+ end)
+
it('jobstart() environment: $NVIM, $NVIM_LISTEN_ADDRESS #11009', function()
local function get_child_env(envname, env)
return exec_lua(