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...
This commit is contained in:
Justin M. Keyes
2026-08-22 15:07:45 -04:00
committed by GitHub
parent c149f4b93a
commit 7037e1effe
2 changed files with 32 additions and 0 deletions

View File

@@ -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;
}

View File

@@ -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 })