From 7037e1effe9e3c98d43b36a287dd39e87eaf1bb2 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Sat, 22 Aug 2026 15:07:45 -0400 Subject: [PATCH] fix(write): :write can target a garbage filename #41432 Problem: `buf_write()` captures the buffer's `fname`/`sfname`/`ffname`, then emits its progress-message before opening the file. This may run user code synchronously: the `Progress` autocmd, and the `msg_show` handler of an in-process UI (ui2). Either can change the CWD, and `shorten_fnames()` then frees/reallocs every buffer's short name. The rest of `buf_write()` reads the freed name... "foldtext()" [New] 41L, 997B written E212: Can't open file for writing: illegal byte sequence ASAN, with ui2 enabled and a `BufEnter` handler that runs `:lcd`: READ path_skip_sep <- path_tail <- match_file_list <- buf_write FREE shorten_buf_fname <- shorten_fnames <- update_cwd <- set_curbuf <- win_set_buf <- nvim_open_win <- ui2 msg_show handler <- ui_call_msg_show <- msg_ext_ui_flush <- buf_write Solution: Copy the names after the `*Pre` autocmds. Note: `readfile()` has the same shape, but its messages pass no progress-id, so they skip `msg_progress()`. Safe, for now... --- src/nvim/bufwrite.c | 13 +++++++++++++ test/functional/ex_cmds/write_spec.lua | 19 +++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/nvim/bufwrite.c b/src/nvim/bufwrite.c index d2e2ff5032..62c72ace3e 100644 --- a/src/nvim/bufwrite.c +++ b/src/nvim/bufwrite.c @@ -1071,6 +1071,15 @@ int buf_write(buf_T *buf, char *fname, char *sfname, linenr_T start, linenr_T en return res; } + // Emitting a msg below can run user code (ui2 `msg_show` handler), which may change CWD and thus + // reallocate the buffer names. #41417 + char *const fname_copy = xstrdup(fname); + char *const sfname_copy = xstrdup(sfname); + char *const ffname_copy = xstrdup(ffname); + fname = fname_copy; + sfname = sfname_copy; + ffname = ffname_copy; + if (cmdmod.cmod_flags & CMOD_LOCKMARKS) { // restore the original '[ and '] positions buf->b_op_start = orig_start; @@ -1858,5 +1867,9 @@ nofail: got_int |= prev_got_int; + xfree(fname_copy); + xfree(sfname_copy); + xfree(ffname_copy); + return retval; } diff --git a/test/functional/ex_cmds/write_spec.lua b/test/functional/ex_cmds/write_spec.lua index e8b60e4978..98f07521b7 100644 --- a/test/functional/ex_cmds/write_spec.lua +++ b/test/functional/ex_cmds/write_spec.lua @@ -264,6 +264,25 @@ describe(':write', function() ) end) + it('unaffected if a Progress handler changes CWD #41417', function() + -- ASAN catches the read of the freed name. Also assert the written file name/contents. + local dir = vim.fs.normalize(t.tmpname(false)) + t.mkdir(dir) + local file = dir .. '/f.txt' + write_file(file, 'one\n') + command('edit ' .. file) + command('lcd ' .. dir) + eq('f.txt', fn.bufname('%')) + -- Frees every buffer's short name, twice; the net CWD is unchanged. + command(('autocmd Progress * lcd %s | lcd %s'):format(vim.fs.dirname(dir), dir)) + api.nvim_buf_set_lines(0, 0, -1, true, { 'one', 'two' }) + command('write') + + eq('f.txt', fn.bufname('%')) + eq({ 'one', 'two' }, fn.readfile(file)) + eq({ 'f.txt' }, fn.readdir(dir)) + end) + it('handles a multi-byte sequence crossing the buffer boundary converting with iconv', function() local content = string.rep('a', 1024 * 8 - 1) .. 'Дbbbbb' api.nvim_buf_set_lines(0, 0, 1, true, { content })