From ed8fbd2e2992cb264cb62585098a1c7acc5c4585 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Sun, 8 Feb 2026 07:04:36 +0800 Subject: [PATCH] vim-patch:9.1.2138: win_execute() and 'autochdir' can corrupt buffer name (#37767) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/nvim/eval/window.c | 10 +++++++++ src/nvim/eval/window.h | 1 + test/functional/legacy/autochdir_spec.lua | 27 +++++++++++++++++++++++ test/old/testdir/test_cd.vim | 22 ++++++++++++++++++ 4 files changed, 60 insertions(+) diff --git a/src/nvim/eval/window.c b/src/nvim/eval/window.c index 218920965c..1d56e593b4 100644 --- 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 index 07a1327526..888c7a596a 100644 --- 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 index 3bfa8bcd1e..7e8f109334 100644 --- 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 index 0c5ce294b3..03f114cf41 100644 --- 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