mirror of
https://github.com/neovim/neovim.git
synced 2026-08-26 09:01:57 +00:00
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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
|
||||
Reference in New Issue
Block a user