commit ed8fbd2e2992cb264cb62585098a1c7acc5c4585
parent 72eb9fe87ce9c224ec6118ae92100ee2f1093a9b
Author: zeertzjq <zeertzjq@outlook.com>
Date: Sun, 8 Feb 2026 07:04:36 +0800
vim-patch:9.1.2138: win_execute() and 'autochdir' can corrupt buffer name (#37767)
Problem: With 'autochdir' win_execute() can corrupt the buffer name,
causing :write to use wrong path.
Solution: Save and restore b_fname when 'autochdir' is active
(Ingo Karkat).
This is caused by a bad interaction of the 'autochdir' behavior,
overriding of the current directory via :lchdir, and the temporary
window switching done by win_execute(), manifesting when e.g. a custom
completion inspects other buffers:
1. In the initial state after the :lcd .. we have curbuf->b_fname =
"Xsubdir/file".
2. do_autochdir() is invoked, temporarily undoing the :lcd .., changing
back into the Xsubdir/ subdirectory.
3. win_execute() switches windows, triggering win_enter_ext() →
win_fix_current_dir() → shorten_fnames(TRUE)
4. shorten_fnames() processes *all* buffers
5. shorten_buf_fname() makes the filename relative to the current
(wrong) directory; b_fname becomes "file" instead of "Xsubdir/file"
6. Directory restoration correctly restores working directory via
mch_chdir() (skipping a second do_autochdir() invocation because
apply_acd is FALSE), but b_fname remains corrupted, with the
"Xsubdir/" part missing.
7. expand("%:p") (and commands like :write) continue to use the
corrupted filename, resolving to a wrong path that's missing the
"Xsubdir/" part.
To fix the problem the short filename is saved if its in effect (i.e.
pointed to by curbuf->b_fname) and 'autochdir' happened. It's then
restored in case of a local cwd override. The conditions limit this
workaround to when 'autochdir' is active *and* overridden by a :lchdir.
closes: vim/vim#19343
https://github.com/vim/vim/commit/abb4d740338e667991656e3ca575e623aba7bd2a
Co-authored-by: Ingo Karkat <swdev@ingo-karkat.de>
Diffstat:
4 files changed, 60 insertions(+), 0 deletions(-)
diff --git a/src/nvim/eval/window.c b/src/nvim/eval/window.c
@@ -509,6 +509,7 @@ bool win_execute_before(win_execute_T *args, win_T *wp, tabpage_T *tp)
args->curpos = wp->w_cursor;
args->cwd_status = FAIL;
args->apply_acd = false;
+ args->save_sfname = NULL;
// Getting and setting directory can be slow on some systems, only do
// this when the current or target window/tab have a local directory or
@@ -523,6 +524,9 @@ bool win_execute_before(win_execute_T *args, win_T *wp, tabpage_T *tp)
// If 'acd' is set, check we are using that directory. If yes, then
// apply 'acd' afterwards, otherwise restore the current directory.
if (args->cwd_status == OK && p_acd) {
+ if (curbuf->b_sfname != NULL && curbuf->b_fname == curbuf->b_sfname) {
+ args->save_sfname = xstrdup(curbuf->b_sfname);
+ }
do_autochdir();
char autocwd[MAXPATHL];
if (os_dirname(autocwd, MAXPATHL) == OK) {
@@ -543,9 +547,15 @@ void win_execute_after(win_execute_T *args)
restore_win_noblock(&args->switchwin, true);
if (args->apply_acd) {
+ xfree(args->save_sfname);
do_autochdir();
} else if (args->cwd_status == OK) {
os_chdir(args->cwd);
+ if (args->save_sfname != NULL) {
+ xfree(curbuf->b_sfname);
+ curbuf->b_sfname = args->save_sfname;
+ curbuf->b_fname = curbuf->b_sfname;
+ }
}
// Update the status line if the cursor moved.
diff --git a/src/nvim/eval/window.h b/src/nvim/eval/window.h
@@ -23,6 +23,7 @@ typedef struct {
char cwd[MAXPATHL];
int cwd_status;
bool apply_acd;
+ char *save_sfname;
switchwin_T switchwin;
} win_execute_T;
diff --git a/test/functional/legacy/autochdir_spec.lua b/test/functional/legacy/autochdir_spec.lua
@@ -32,6 +32,7 @@ describe('autochdir behavior', function()
eq(dir, eval([[substitute(getcwd(), '.*/\(\k*\)', '\1', '')]]))
end)
+ -- oldtest: Test_set_filename_other_window()
it(':file in win_execute() does not cause wrong directory', function()
command('cd ' .. dir)
source([[
@@ -64,6 +65,7 @@ describe('autochdir behavior', function()
expected_empty()
end)
+ -- oldtest: Test_acd_win_execute()
it('win_execute() does not change directory', function()
local subdir = 'Xfile'
command('cd ' .. dir)
@@ -78,6 +80,7 @@ describe('autochdir behavior', function()
matches(dir .. '$', eval('getcwd()'))
end)
+ -- oldtest: Test_verbose_pwd()
it(':verbose pwd shows whether autochdir is used', function()
local subdir = 'Xautodir'
command('cd ' .. dir)
@@ -115,4 +118,28 @@ describe('autochdir behavior', function()
command('wincmd w')
matches('%[window%].*' .. dir .. '/' .. subdir .. '$', exec_capture('verbose pwd'))
end)
+
+ it('overriding via :lcd is not clobbered by win_execute()', function()
+ command('cd ' .. dir)
+ source([[
+ func Test_lcd_win_execute()
+ let startdir = getcwd()
+ call mkdir('Xsubdir', 'R')
+ set autochdir
+ edit Xsubdir/file
+ call assert_match('_autochdir.Xsubdir.file$', expand('%:p'))
+ split
+ lcd ..
+ call assert_match('_autochdir.Xsubdir.file$', expand('%:p'))
+ call win_execute(win_getid(2), "")
+ call assert_match('_autochdir.Xsubdir.file$', expand('%:p'))
+
+ set noautochdir
+ bwipe!
+ call chdir(startdir)
+ endfunc
+ ]])
+ call('Test_lcd_win_execute')
+ expected_empty()
+ end)
end)
diff --git a/test/old/testdir/test_cd.vim b/test/old/testdir/test_cd.vim
@@ -192,6 +192,28 @@ func Test_lcd_split()
quit!
endfunc
+" Test that a temporary override of 'autochdir' via :lcd isn't clobbered by win_execute() in a split window.
+func Test_lcd_win_execute()
+ CheckFunction test_autochdir
+ CheckOption autochdir
+
+ let startdir = getcwd()
+ call mkdir('Xsubdir', 'R')
+ call test_autochdir()
+ set autochdir
+ edit Xsubdir/file
+ call assert_match('testdir.Xsubdir.file$', expand('%:p'))
+ split
+ lcd ..
+ call assert_match('testdir.Xsubdir.file$', expand('%:p'))
+ call win_execute(win_getid(2), "")
+ call assert_match('testdir.Xsubdir.file$', expand('%:p'))
+
+ set noautochdir
+ bwipe!
+ call chdir(startdir)
+endfunc
+
func Test_cd_from_non_existing_dir()
CheckNotMSWindows