Version in base suite: 9.1.1230-2 Base version: vim_9.1.1230-2 Target version: vim_9.1.1230-2+deb13u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/v/vim/vim_9.1.1230-2.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/v/vim/vim_9.1.1230-2+deb13u1.dsc /srv/release.debian.org/tmp/T35rpTUl_N/vim-9.1.1230/debian/patches/CVE-2026-28421.patch |binary /srv/release.debian.org/tmp/T35rpTUl_N/vim-9.1.1230/debian/patches/CVE-2026-35177.patch |binary vim-9.1.1230/debian/changelog | 46 vim-9.1.1230/debian/gbp.conf | 2 vim-9.1.1230/debian/patches/CVE-2025-53905.patch | 1010 ++++++++++ vim-9.1.1230/debian/patches/CVE-2025-53906.patch | 389 +++ vim-9.1.1230/debian/patches/CVE-2026-25749.patch | 62 vim-9.1.1230/debian/patches/CVE-2026-26269.patch | 132 + vim-9.1.1230/debian/patches/CVE-2026-28417+regression-fix.patch | 113 + vim-9.1.1230/debian/patches/CVE-2026-28418.patch | 68 vim-9.1.1230/debian/patches/CVE-2026-28419.patch | 75 vim-9.1.1230/debian/patches/CVE-2026-28420.patch | 166 + vim-9.1.1230/debian/patches/CVE-2026-28422.patch | 47 vim-9.1.1230/debian/patches/CVE-2026-32249.patch | 116 + vim-9.1.1230/debian/patches/CVE-2026-33412.patch | 45 vim-9.1.1230/debian/patches/CVE-2026-34982+regression-fix.patch | 112 + vim-9.1.1230/debian/patches/CVE-2026-39881.patch | 198 + vim-9.1.1230/debian/patches/CVE-2026-41411.patch | 74 vim-9.1.1230/debian/patches/CVE-2026-42307.patch | 219 ++ vim-9.1.1230/debian/patches/CVE-2026-43961.patch | 97 vim-9.1.1230/debian/patches/CVE-2026-44656.patch | 135 + vim-9.1.1230/debian/patches/CVE-2026-45130.patch | 115 + vim-9.1.1230/debian/patches/CVE-2026-46483.patch | 82 vim-9.1.1230/debian/patches/CVE-2026-47162.patch | 71 vim-9.1.1230/debian/patches/CVE-2026-47167.patch | 99 vim-9.1.1230/debian/patches/CVE-2026-52858+regression-fix.patch | 236 ++ vim-9.1.1230/debian/patches/CVE-2026-52859.patch | 274 ++ vim-9.1.1230/debian/patches/CVE-2026-52860.patch | 451 ++++ vim-9.1.1230/debian/patches/CVE-2026-55693.patch | 89 vim-9.1.1230/debian/patches/CVE-2026-55892.patch | 81 vim-9.1.1230/debian/patches/CVE-2026-55895.patch | 85 vim-9.1.1230/debian/patches/CVE-2026-57452.patch | 77 vim-9.1.1230/debian/patches/CVE-2026-57455.patch | 73 vim-9.1.1230/debian/patches/CVE-2026-57456.patch | 150 + vim-9.1.1230/debian/patches/CVE-2026-59856.patch | 101 + vim-9.1.1230/debian/patches/CVE-2026-59857.patch | 109 + vim-9.1.1230/debian/patches/CVE-2026-59858.patch | 133 + vim-9.1.1230/debian/patches/CVE-2026-73072+regression-fix.patch | 165 + vim-9.1.1230/debian/patches/CVE-2026-73073.patch | 99 vim-9.1.1230/debian/patches/CVE-2026-73076.patch | 269 ++ vim-9.1.1230/debian/patches/CVE-2026-73077+regression-fix.patch | 123 + vim-9.1.1230/debian/patches/CVE-2026-73078.patch | 95 vim-9.1.1230/debian/patches/series | 40 vim-9.1.1230/debian/salsa-ci.yml | 2 vim-9.1.1230/debian/source/include-binaries | 2 45 files changed, 6126 insertions(+), 1 deletion(-) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpvmqsv4o8/vim_9.1.1230-2.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpvmqsv4o8/vim_9.1.1230-2+deb13u1.dsc: no acceptable signature found diff -Nru vim-9.1.1230/debian/changelog vim-9.1.1230/debian/changelog --- vim-9.1.1230/debian/changelog 2025-05-23 00:48:59.000000000 +0000 +++ vim-9.1.1230/debian/changelog 2026-09-25 20:25:55.000000000 +0000 @@ -1,3 +1,49 @@ +vim (2:9.1.1230-2+deb13u1) trixie-security; urgency=medium + + * Fix CVE-2025-53905 + * Fix CVE-2025-53906 + * Fix CVE-2026-25749 + * Fix CVE-2026-26269 + * Fix CVE-2026-28417 + fix regression + * Fix CVE-2026-28418 + * Fix CVE-2026-28419 + * Fix CVE-2026-28420 + * Fix CVE-2026-28421 + * Fix CVE-2026-28422 + * Fix CVE-2026-32249 + * Fix CVE-2026-33412 + * Fix CVE-2026-34982 + fix regression + * Fix CVE-2026-35177 + * Fix CVE-2026-39881 + * Fix CVE-2026-41411 + * Fix CVE-2026-42307 + * Fix CVE-2026-43961 + * Fix CVE-2026-44656 + * Fix CVE-2026-45130 + * Fix CVE-2026-46483 + * Fix CVE-2026-47162 + * Fix CVE-2026-47167 + * Fix CVE-2026-52858 + fix regression + * Fix CVE-2026-52859 + * Fix CVE-2026-52860 + * Fix CVE-2026-55693 + * Fix CVE-2026-55892 + * Fix CVE-2026-55895 + * Fix CVE-2026-57452 + * Fix CVE-2026-57455 + * Fix CVE-2026-57456 + * Fix CVE-2026-59856 + * Fix CVE-2026-59857 + * Fix CVE-2026-59858 + * Update gbp and salsa CI config to match trixie + * Fix CVE-2026-73072 + fix regression + * Fix CVE-2026-73073 + * Fix CVE-2026-73076 + * Fix CVE-2026-73077 + fix regression + * Fix CVE-2026-73078 + + -- Lee Garrett Fri, 25 Sep 2026 22:25:55 +0200 + vim (2:9.1.1230-2) unstable; urgency=medium * Backport v9.1.1242 and v9.1.1244 to fix crash when evaluating a variable diff -Nru vim-9.1.1230/debian/gbp.conf vim-9.1.1230/debian/gbp.conf --- vim-9.1.1230/debian/gbp.conf 2025-05-23 00:48:59.000000000 +0000 +++ vim-9.1.1230/debian/gbp.conf 2026-09-25 20:25:55.000000000 +0000 @@ -1,7 +1,7 @@ [DEFAULT] upstream-tag = v%(version)s debian-tag = debian/%(version)s -debian-branch = debian/sid +debian-branch = debian/trixie [pq] patch-numbers = False diff -Nru vim-9.1.1230/debian/patches/CVE-2025-53905.patch vim-9.1.1230/debian/patches/CVE-2025-53905.patch --- vim-9.1.1230/debian/patches/CVE-2025-53905.patch 1970-01-01 00:00:00.000000000 +0000 +++ vim-9.1.1230/debian/patches/CVE-2025-53905.patch 2026-09-25 20:25:55.000000000 +0000 @@ -0,0 +1,1010 @@ +From: Lee Garrett +Date: Fri, 25 Sep 2026 17:26:06 +0200 +Subject: Fix CVE-2025-53905 + +Origin: backport, https://github.com/vim/vim/commit/87757c6b0a4b2c1f71c72ea8e1438b8fb116b239 +Forwarded: not-needed +Reviewed-By: Lee Garrett +Last-Update: 2026-09-25 + +patch 9.1.1552: [security]: path traversal issue in tar.vim +Problem: [security]: path traversal issue in tar.vim + (@ax) +Solution: warn the user for such things, drop leading /, don't + forcefully overwrite files when writing temporary files, + refactor autoload/tar.vim + +tar.vim: drop leading / in path names + +A tar archive containing files with leading `/` may cause confusions as +to where the content is extracted. Let's make sure we drop the leading +`/` and use a relative path instead. + +Also while at it, had to refactor it quite a bit and increase the +minimum supported Vim version to v9. Also add a test for some basic tar +functionality + +closes: #17733 + +This fixes CVE-2025-53905. + +Backport of upstream commit 87757c6b0a4b2c1f71c72ea8e1438b8fb116b239. +- Runtime files (autoload/tar.vim, doc/pi_tar.txt) taken verbatim from + upstream. +- src/version.c and src/po/vim.pot bumps omitted as not relevant + to the runtime fix. +- test_plugin_tar.vim sources check.vim to match this + branch's test harness. +--- + Filelist | 2 + + runtime/autoload/tar.vim | 276 ++++++++++++++++++++-------------------- + runtime/doc/pi_tar.txt | 41 +++--- + runtime/doc/tags | 1 - + runtime/plugin/tarPlugin.vim | 8 +- + src/testdir/Make_all.mak | 2 + + src/testdir/samples/evil.tar | Bin 0 -> 10240 bytes + src/testdir/samples/sample.tar | Bin 0 -> 10240 bytes + src/testdir/test_plugin_tar.vim | 130 +++++++++++++++++++ + 9 files changed, 304 insertions(+), 156 deletions(-) + create mode 100644 src/testdir/samples/evil.tar + create mode 100644 src/testdir/samples/sample.tar + create mode 100644 src/testdir/test_plugin_tar.vim + +diff --git a/Filelist b/Filelist +index 95bed7d..bf54197 100644 +--- a/Filelist ++++ b/Filelist +@@ -223,7 +223,9 @@ SRC_ALL = \ + src/testdir/samples/*.html \ + src/testdir/samples/*.txt \ + src/testdir/samples/*.vim \ ++ src/testdir/samples/evil.tar \ + src/testdir/samples/poc.zip \ ++ src/testdir/samples/sample.tar \ + src/testdir/samples/test000 \ + src/testdir/samples/test.zip \ + src/testdir/samples/test_undo.txt.undo \ +diff --git a/runtime/autoload/tar.vim b/runtime/autoload/tar.vim +index 4aab915..1a0d4f8 100644 +--- a/runtime/autoload/tar.vim ++++ b/runtime/autoload/tar.vim +@@ -14,6 +14,9 @@ + " 2025 Mar 02 by Vim Project: escape the filename before using :read + " 2025 Mar 02 by Vim Project: determine the compression using readblob() + " instead of shelling out to file(1) ++" 2025 Apr 16 by Vim Project: decouple from netrw by adding s:WinPath() ++" 2025 May 19 by Vim Project: restore working directory after read/write ++" 2025 Jul 13 by Vim Project: warn with path traversal attacks + " + " Contains many ideas from Michael Toren's + " +@@ -32,9 +35,9 @@ if &cp || exists("g:loaded_tar") + finish + endif + let g:loaded_tar= "v32b" +-if v:version < 702 ++if v:version < 900 + echohl WarningMsg +- echo "***warning*** this version of tar needs vim 7.2" ++ echo "***warning*** this version of tar needs vim 9.0" + echohl Normal + finish + endif +@@ -44,10 +47,10 @@ set cpo&vim + " --------------------------------------------------------------------- + " Default Settings: {{{1 + if !exists("g:tar_browseoptions") +- let g:tar_browseoptions= "Ptf" ++ let g:tar_browseoptions= "tf" + endif + if !exists("g:tar_readoptions") +- let g:tar_readoptions= "pPxf" ++ let g:tar_readoptions= "pxf" + endif + if !exists("g:tar_cmd") + let g:tar_cmd= "tar" +@@ -56,6 +59,7 @@ if !exists("g:tar_writeoptions") + let g:tar_writeoptions= "uf" + endif + if !exists("g:tar_delfile") ++ " Note: not supported on BSD + let g:tar_delfile="--delete -f" + endif + if !exists("g:netrw_cygwin") +@@ -104,10 +108,26 @@ if !exists("g:tar_shq") + endif + endif + ++let g:tar_secure=' -- ' ++let g:tar_leading_pat='^\%([.]\{,2\}/\)\+' ++ + " ---------------- + " Functions: {{{1 + " ---------------- + ++" --------------------------------------------------------------------- ++" s:Msg: {{{2 ++fun! s:Msg(func, severity, msg) ++ redraw! ++ if a:severity =~? 'error' ++ echohl Error ++ else ++ echohl WarningMsg ++ endif ++ echo $"***{a:severity}*** ({a:func}) {a:msg}" ++ echohl None ++endfunc ++ + " --------------------------------------------------------------------- + " tar#Browse: {{{2 + fun! tar#Browse(tarfile) +@@ -116,16 +136,14 @@ fun! tar#Browse(tarfile) + + " sanity checks + if !executable(g:tar_cmd) +- redraw! +- echohl Error | echo '***error*** (tar#Browse) "'.g:tar_cmd.'" not available on your system' ++ call s:Msg('tar#Browse', 'error', $"{g:tar_cmd} not available on your system") + let &report= repkeep + return + endif + if !filereadable(a:tarfile) + if a:tarfile !~# '^\a\+://' + " if it's an url, don't complain, let url-handlers such as vim do its thing +- redraw! +- echohl Error | echo "***error*** (tar#Browse) File not readable<".a:tarfile.">" | echohl None ++ call s:Msg('tar#Browse', 'error', $"File not readable<{a:tarfile}>") + endif + let &report= repkeep + return +@@ -146,7 +164,7 @@ fun! tar#Browse(tarfile) + let lastline= line("$") + call setline(lastline+1,'" tar.vim version '.g:loaded_tar) + call setline(lastline+2,'" Browsing tarfile '.a:tarfile) +- call setline(lastline+3,'" Select a file with cursor and press ENTER') ++ call setline(lastline+3,'" Select a file with cursor and press ENTER, "x" to extract a file') + keepj $put ='' + keepj sil! 0d + keepj $ +@@ -201,28 +219,18 @@ fun! tar#Browse(tarfile) + exe "sil! r! ".g:tar_cmd." -".g:tar_browseoptions." ".shellescape(tarfile,1) + endif + if v:shell_error != 0 +- redraw! +- echohl WarningMsg | echo "***warning*** (tar#Browse) please check your g:tar_browseoptions<".g:tar_browseoptions.">" ++ call s:Msg('tar#Browse', 'warning', $"please check your g:tar_browseoptions '<{g:tar_browseoptions}>'") + return + endif +- " +- " The following should not be neccessary, since in case of errors the +- " previous if statement should have caught the problem (because tar exited +- " with a non-zero exit code). +- " if line("$") == curlast || ( line("$") == (curlast + 1) && +- " \ getline("$") =~# '\c\<\%(warning\|error\|inappropriate\|unrecognized\)\>' && +- " \ getline("$") =~ '\s' ) +- " redraw! +- " echohl WarningMsg | echo "***warning*** (tar#Browse) ".a:tarfile." doesn't appear to be a tar file" | echohl None +- " keepj sil! %d +- " let eikeep= &ei +- " set ei=BufReadCmd,FileReadCmd +- " exe "r ".fnameescape(a:tarfile) +- " let &ei= eikeep +- " keepj sil! 1d +- " call Dret("tar#Browse : a:tarfile<".a:tarfile.">") +- " return +- " endif ++ ++ " remove tar: Removing leading '/' from member names ++ " Note: the message could be localized ++ if search('^tar: ') > 0 || search(g:tar_leading_pat) > 0 ++ call append(3,'" Note: Path Traversal Attack detected!') ++ let b:leading_slash = 1 ++ " remove the message output ++ sil g/^tar: /d ++ endif + + " set up maps supported for tar + setlocal noma nomod ro +@@ -241,12 +249,7 @@ fun! s:TarBrowseSelect() + let repkeep= &report + set report=10 + let fname= getline(".") +- +- if !exists("g:tar_secure") && fname =~ '^\s*-\|\s\+-' +- redraw! +- echohl WarningMsg | echo '***warning*** (tar#BrowseSelect) rejecting tarfile member<'.fname.'> because of embedded "-"' +- return +- endif ++ let ls= get(b:, 'leading_slash', 0) + + " sanity check + if fname =~ '^"' +@@ -268,7 +271,8 @@ fun! s:TarBrowseSelect() + wincmd _ + endif + let s:tblfile_{winnr()}= curfile +- call tar#Read("tarfile:".tarfile.'::'.fname,1) ++ let b:leading_slash= ls ++ call tar#Read("tarfile:".tarfile.'::'.fname) + filetype detect + set nomod + exe 'com! -buffer -nargs=? -complete=file TarDiff :call tar#Diff(,"'.fnameescape(fname).'")' +@@ -278,26 +282,18 @@ endfun + + " --------------------------------------------------------------------- + " tar#Read: {{{2 +-fun! tar#Read(fname,mode) ++fun! tar#Read(fname) + let repkeep= &report + set report=10 + let tarfile = substitute(a:fname,'tarfile:\(.\{-}\)::.*$','\1','') + let fname = substitute(a:fname,'tarfile:.\{-}::\(.*\)$','\1','') + " be careful not to execute special crafted files +- let escape_file = fname->fnameescape() +- +- " changing the directory to the temporary earlier to allow tar to extract the file with permissions intact +- if !exists("*mkdir") +- redraw! +- echohl Error | echo "***error*** (tar#Write) sorry, mkdir() doesn't work on your system" | echohl None +- let &report= repkeep +- return +- endif ++ let escape_file = fname->substitute(g:tar_leading_pat, '', '')->fnameescape() + + let curdir= getcwd() ++ let b:curdir= curdir + let tmpdir= tempname() +- let b:curdir= tmpdir +- let b:tmpdir= curdir ++ let b:tmpdir= tmpdir + if tmpdir =~ '\.' + let tmpdir= substitute(tmpdir,'\.[^.]*$','','e') + endif +@@ -305,10 +301,9 @@ fun! tar#Read(fname,mode) + + " attempt to change to the indicated directory + try +- exe "cd ".fnameescape(tmpdir) ++ exe "lcd ".fnameescape(tmpdir) + catch /^Vim\%((\a\+)\)\=:E344/ +- redraw! +- echohl Error | echo "***error*** (tar#Write) cannot cd to temporary directory" | Echohl None ++ call s:Msg('tar#Read', 'error', "cannot lcd to temporary directory") + let &report= repkeep + return + endtry +@@ -318,7 +313,7 @@ fun! tar#Read(fname,mode) + call s:Rmdir("_ZIPVIM_") + endif + call mkdir("_ZIPVIM_") +- cd _ZIPVIM_ ++ lcd _ZIPVIM_ + + if has("win32unix") && executable("cygpath") + " assuming cygwin +@@ -331,7 +326,7 @@ fun! tar#Read(fname,mode) + elseif fname =~ '\.bz3$' && executable("bz3cat") + let decmp= "|bz3cat" + let doro = 1 +- elseif fname =~ '\.t\=gz$' && executable("zcat") ++ elseif fname =~ '\.t\=gz$' && executable("zcat") + let decmp= "|zcat" + let doro = 1 + elseif fname =~ '\.lzma$' && executable("lzcat") +@@ -354,68 +349,66 @@ fun! tar#Read(fname,mode) + endif + endif + +- if exists("g:tar_secure") +- let tar_secure= " -- " +- else +- let tar_secure= " " +- endif + + if tarfile =~# '\.bz2$' +- exe "sil! r! bzip2 -d -c -- ".shellescape(tarfile,1)."| ".g:tar_cmd." -".g:tar_readoptions." - ".tar_secure.shellescape(fname,1).decmp ++ exe "sil! r! bzip2 -d -c -- ".shellescape(tarfile,1)."| ".g:tar_cmd." -".g:tar_readoptions." - ".g:tar_secure.shellescape(fname,1).decmp + exe "read ".escape_file + elseif tarfile =~# '\.bz3$' +- exe "sil! r! bzip3 -d -c -- ".shellescape(tarfile,1)."| ".g:tar_cmd." -".g:tar_readoptions." - ".tar_secure.shellescape(fname,1).decmp ++ exe "sil! r! bzip3 -d -c -- ".shellescape(tarfile,1)."| ".g:tar_cmd." -".g:tar_readoptions." - ".g:tar_secure.shellescape(fname,1).decmp + exe "read ".escape_file + elseif tarfile =~# '\.\(gz\)$' +- exe "sil! r! gzip -d -c -- ".shellescape(tarfile,1)."| ".g:tar_cmd." -".g:tar_readoptions." - ".tar_secure.shellescape(fname,1).decmp ++ exe "sil! r! gzip -d -c -- ".shellescape(tarfile,1)."| ".g:tar_cmd." -".g:tar_readoptions." - ".g:tar_secure.shellescape(fname,1).decmp + exe "read ".escape_file + elseif tarfile =~# '\(\.tgz\|\.tbz\|\.txz\)' + let filekind= s:Header(tarfile) + if filekind =~? "bzip2" +- exe "sil! r! bzip2 -d -c -- ".shellescape(tarfile,1)."| ".g:tar_cmd." -".g:tar_readoptions." - ".tar_secure.shellescape(fname,1).decmp ++ exe "sil! r! bzip2 -d -c -- ".shellescape(tarfile,1)."| ".g:tar_cmd." -".g:tar_readoptions." - ".g:tar_secure.shellescape(fname,1).decmp + exe "read ".escape_file + elseif filekind =~ "bzip3" +- exe "sil! r! bzip3 -d -c -- ".shellescape(tarfile,1)."| ".g:tar_cmd." -".g:tar_readoptions." - ".tar_secure.shellescape(fname,1).decmp ++ exe "sil! r! bzip3 -d -c -- ".shellescape(tarfile,1)."| ".g:tar_cmd." -".g:tar_readoptions." - ".g:tar_secure.shellescape(fname,1).decmp + exe "read ".escape_file + elseif filekind =~? "xz" +- exe "sil! r! xz -d -c -- ".shellescape(tarfile,1)."| ".g:tar_cmd." -".g:tar_readoptions." - ".tar_secure.shellescape(fname,1).decmp ++ exe "sil! r! xz -d -c -- ".shellescape(tarfile,1)."| ".g:tar_cmd." -".g:tar_readoptions." - ".g:tar_secure.shellescape(fname,1).decmp + exe "read ".escape_file + elseif filekind =~? "zstd" +- exe "sil! r! zstd --decompress --stdout -- ".shellescape(tarfile,1)."| ".g:tar_cmd." -".g:tar_readoptions." - ".tar_secure.shellescape(fname,1).decmp ++ exe "sil! r! zstd --decompress --stdout -- ".shellescape(tarfile,1)."| ".g:tar_cmd." -".g:tar_readoptions." - ".g:tar_secure.shellescape(fname,1).decmp + exe "read ".escape_file + elseif filekind =~? "gzip" +- exe "sil! r! gzip -d -c -- ".shellescape(tarfile,1)."| ".g:tar_cmd." -".g:tar_readoptions." - ".tar_secure.shellescape(fname,1).decmp ++ exe "sil! r! gzip -d -c -- ".shellescape(tarfile,1)."| ".g:tar_cmd." -".g:tar_readoptions." - ".g:tar_secure.shellescape(fname,1).decmp + exe "read ".escape_file + endif + + elseif tarfile =~# '\.lrp$' +- exe "sil! r! cat -- ".shellescape(tarfile,1)." | gzip -d -c - | ".g:tar_cmd." -".g:tar_readoptions." - ".tar_secure.shellescape(fname,1).decmp ++ exe "sil! r! cat -- ".shellescape(tarfile,1)." | gzip -d -c - | ".g:tar_cmd." -".g:tar_readoptions." - ".g:tar_secure.shellescape(fname,1).decmp + exe "read ".escape_file + elseif tarfile =~# '\.lzma$' +- exe "sil! r! lzma -d -c -- ".shellescape(tarfile,1)."| ".g:tar_cmd." -".g:tar_readoptions." - ".tar_secure.shellescape(fname,1).decmp ++ exe "sil! r! lzma -d -c -- ".shellescape(tarfile,1)."| ".g:tar_cmd." -".g:tar_readoptions." - ".g:tar_secure.shellescape(fname,1).decmp + exe "read ".escape_file + elseif tarfile =~# '\.\(xz\|txz\)$' +- exe "sil! r! xz --decompress --stdout -- ".shellescape(tarfile,1)." | ".g:tar_cmd." -".g:tar_readoptions." - ".tar_secure.shellescape(fname,1).decmp ++ exe "sil! r! xz --decompress --stdout -- ".shellescape(tarfile,1)." | ".g:tar_cmd." -".g:tar_readoptions." - ".g:tar_secure.shellescape(fname,1).decmp + exe "read ".escape_file + elseif tarfile =~# '\.\(lz4\|tlz4\)$' +- exe "sil! r! lz4 --decompress --stdout -- ".shellescape(tarfile,1)." | ".g:tar_cmd." -".g:tar_readoptions." - ".tar_secure.shellescape(fname,1).decmp ++ exe "sil! r! lz4 --decompress --stdout -- ".shellescape(tarfile,1)." | ".g:tar_cmd." -".g:tar_readoptions." - ".g:tar_secure.shellescape(fname,1).decmp + exe "read ".escape_file + else + if tarfile =~ '^\s*-' + " A file name starting with a dash is taken as an option. Prepend ./ to avoid that. + let tarfile = substitute(tarfile, '-', './-', '') + endif +- exe "silent r! ".g:tar_cmd." -".g:tar_readoptions.shellescape(tarfile,1)." ".tar_secure.shellescape(fname,1).decmp ++ exe "silent r! ".g:tar_cmd." -".g:tar_readoptions.shellescape(tarfile,1)." ".g:tar_secure.shellescape(fname,1).decmp + exe "read ".escape_file + endif ++ if get(b:, 'leading_slash', 0) ++ sil g/^tar: /d ++ endif + + redraw! + +-if v:shell_error != 0 +- cd .. ++ if v:shell_error != 0 ++ lcd .. + call s:Rmdir("_ZIPVIM_") +- exe "cd ".fnameescape(curdir) +- echohl Error | echo "***error*** (tar#Read) sorry, unable to open or extract ".tarfile." with ".fname | echohl None ++ exe "lcd ".fnameescape(curdir) ++ call s:Msg('tar#Read', 'error', $"sorry, unable to open or extract {tarfile} with {fname}") + endif + + if doro +@@ -424,40 +417,54 @@ if v:shell_error != 0 + endif + + let b:tarfile= a:fname +- exe "file tarfile::".fnameescape(fname) + + " cleanup + keepj sil! 0d + set nomod + + let &report= repkeep ++ exe "lcd ".fnameescape(curdir) ++ silent exe "file tarfile::". fname->fnameescape() + endfun + + " --------------------------------------------------------------------- + " tar#Write: {{{2 + fun! tar#Write(fname) ++ let pwdkeep= getcwd() + let repkeep= &report + set report=10 +- " temporary buffer variable workaround because too fucking tired. but it works now + let curdir= b:curdir + let tmpdir= b:tmpdir + +- if !exists("g:tar_secure") && a:fname =~ '^\s*-\|\s\+-' +- redraw! +- echohl WarningMsg | echo '***warning*** (tar#Write) rejecting tarfile member<'.a:fname.'> because of embedded "-"' +- return +- endif +- + " sanity checks + if !executable(g:tar_cmd) + redraw! + let &report= repkeep + return + endif +- + let tarfile = substitute(b:tarfile,'tarfile:\(.\{-}\)::.*$','\1','') + let fname = substitute(b:tarfile,'tarfile:.\{-}::\(.*\)$','\1','') + ++ if get(b:, 'leading_slash', 0) ++ call s:Msg('tar#Write', 'error', $"sorry, not attempting to update {tarfile} with {fname}") ++ let &report= repkeep ++ return ++ endif ++ ++ if !isdirectory(fnameescape(tmpdir)) ++ call mkdir(fnameescape(tmpdir), 'p') ++ endif ++ exe $"lcd {fnameescape(tmpdir)}" ++ if isdirectory("_ZIPVIM_") ++ call s:Rmdir("_ZIPVIM_") ++ endif ++ call mkdir("_ZIPVIM_") ++ lcd _ZIPVIM_ ++ let dir = fnamemodify(fname, ':p:h') ++ if dir !~# '_ZIPVIM_$' ++ call mkdir(dir) ++ endif ++ + " handle compressed archives + if tarfile =~# '\.bz2' + call system("bzip2 -d -- ".shellescape(tarfile,0)) +@@ -496,8 +503,7 @@ fun! tar#Write(fname) + " Note: no support for name.tar.tbz/.txz/.tgz/.tlz4/.tzst + + if v:shell_error != 0 +- redraw! +- echohl Error | echo "***error*** (tar#Write) sorry, unable to update ".tarfile." with ".fname | echohl None ++ call s:Msg('tar#Write', 'error', $"sorry, unable to update {tarfile} with {fname}") + else + + if fname =~ '/' +@@ -515,28 +521,22 @@ fun! tar#Write(fname) + let tarfile = substitute(tarfile, '-', './-', '') + endif + +- if exists("g:tar_secure") +- let tar_secure= " -- " +- else +- let tar_secure= " " +- endif +- exe "w! ".fnameescape(fname) ++ " don't overwrite a file forcefully ++ exe "w ".fnameescape(fname) + if has("win32unix") && executable("cygpath") + let tarfile = substitute(system("cygpath ".shellescape(tarfile,0)),'\n','','e') + endif + + " delete old file from tarfile +- call system(g:tar_cmd." ".g:tar_delfile." ".shellescape(tarfile,0).tar_secure.shellescape(fname,0)) ++ " Note: BSD tar does not support --delete flag ++ call system(g:tar_cmd." ".g:tar_delfile." ".shellescape(tarfile,0).g:tar_secure.shellescape(fname,0)) + if v:shell_error != 0 +- redraw! +- echohl Error | echo "***error*** (tar#Write) sorry, unable to update ".fnameescape(tarfile)." with ".fnameescape(fname) | echohl None ++ call s:Msg('tar#Write', 'error', $"sorry, unable to update {fnameescape(tarfile)} with {fnameescape(fname)} --delete not supported?") + else +- + " update tarfile with new file +- call system(g:tar_cmd." -".g:tar_writeoptions." ".shellescape(tarfile,0).tar_secure.shellescape(fname,0)) ++ call system(g:tar_cmd." -".g:tar_writeoptions." ".shellescape(tarfile,0).g:tar_secure.shellescape(fname,0)) + if v:shell_error != 0 +- redraw! +- echohl Error | echo "***error*** (tar#Write) sorry, unable to update ".fnameescape(tarfile)." with ".fnameescape(fname) | echohl None ++ call s:Msg('tar#Write', 'error', $"sorry, unable to update {fnameescape(tarfile)} with {fnameescape(fname)}") + elseif exists("compress") + call system(compress) + if exists("tgz") +@@ -562,9 +562,9 @@ fun! tar#Write(fname) + endif + + " cleanup and restore current directory +- cd .. ++ lcd .. + call s:Rmdir("_ZIPVIM_") +- exe "cd ".fnameescape(curdir) ++ exe "lcd ".fnameescape(pwdkeep) + setlocal nomod + + let &report= repkeep +@@ -577,6 +577,7 @@ fun! tar#Diff(userfname,fname) + if a:userfname != "" + let fname= a:userfname + endif ++ exe "lcd ".fnameescape(b:tmpdir). '/_ZIPVIM_' + if filereadable(fname) + " sets current file (from tarball) for diff'ing + " splits window vertically +@@ -600,12 +601,6 @@ fun! tar#Extract() + set report=10 + let fname= getline(".") + +- if !exists("g:tar_secure") && fname =~ '^\s*-\|\s\+-' +- redraw! +- echohl WarningMsg | echo '***warning*** (tar#BrowseSelect) rejecting tarfile member<'.fname.'> because of embedded "-"' +- return +- endif +- + " sanity check + if fname =~ '^"' + let &report= repkeep +@@ -615,20 +610,20 @@ fun! tar#Extract() + let tarball = expand("%") + let tarbase = substitute(tarball,'\..*$','','') + +- let extractcmd= netrw#WinPath(g:tar_extractcmd) ++ let extractcmd= s:WinPath(g:tar_extractcmd) + if filereadable(tarbase.".tar") + call system(extractcmd." ".shellescape(tarbase).".tar ".shellescape(fname)) + if v:shell_error != 0 +- echohl Error | echo "***error*** ".extractcmd." ".tarbase.".tar ".fname.": failed!" | echohl NONE ++ call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarbase}.tar {fname}: failed!") + else +- echo "***note*** successfully extracted ".fname ++ echo "***note*** successfully extracted ". fname + endif + + elseif filereadable(tarbase.".tgz") + let extractcmd= substitute(extractcmd,"-","-z","") + call system(extractcmd." ".shellescape(tarbase).".tgz ".shellescape(fname)) + if v:shell_error != 0 +- echohl Error | echo "***error*** ".extractcmd." ".tarbase.".tgz ".fname.": failed!" | echohl NONE ++ call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarbase}.tgz {fname}: failed!") + else + echo "***note*** successfully extracted ".fname + endif +@@ -637,7 +632,7 @@ fun! tar#Extract() + let extractcmd= substitute(extractcmd,"-","-z","") + call system(extractcmd." ".shellescape(tarbase).".tar.gz ".shellescape(fname)) + if v:shell_error != 0 +- echohl Error | echo "***error*** ".extractcmd." ".tarbase.".tar.gz ".fname.": failed!" | echohl NONE ++ call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarbase}.tar.gz {fname}: failed!") + else + echo "***note*** successfully extracted ".fname + endif +@@ -646,7 +641,7 @@ fun! tar#Extract() + let extractcmd= substitute(extractcmd,"-","-j","") + call system(extractcmd." ".shellescape(tarbase).".tbz ".shellescape(fname)) + if v:shell_error != 0 +- echohl Error | echo "***error*** ".extractcmd."j ".tarbase.".tbz ".fname.": failed!" | echohl NONE ++ call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarbase}.tbz {fname}: failed!") + else + echo "***note*** successfully extracted ".fname + endif +@@ -655,7 +650,7 @@ fun! tar#Extract() + let extractcmd= substitute(extractcmd,"-","-j","") + call system(extractcmd." ".shellescape(tarbase).".tar.bz2 ".shellescape(fname)) + if v:shell_error != 0 +- echohl Error | echo "***error*** ".extractcmd."j ".tarbase.".tar.bz2 ".fname.": failed!" | echohl NONE ++ call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarbase}.tar.bz2 {fname}: failed!") + else + echo "***note*** successfully extracted ".fname + endif +@@ -664,7 +659,7 @@ fun! tar#Extract() + let extractcmd= substitute(extractcmd,"-","-j","") + call system(extractcmd." ".shellescape(tarbase).".tar.bz3 ".shellescape(fname)) + if v:shell_error != 0 +- echohl Error | echo "***error*** ".extractcmd."j ".tarbase.".tar.bz3 ".fname.": failed!" | echohl NONE ++ call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarbase}.tar.bz3 {fname}: failed!") + else + echo "***note*** successfully extracted ".fname + endif +@@ -673,7 +668,7 @@ fun! tar#Extract() + let extractcmd= substitute(extractcmd,"-","-J","") + call system(extractcmd." ".shellescape(tarbase).".txz ".shellescape(fname)) + if v:shell_error != 0 +- echohl Error | echo "***error*** ".extractcmd." ".tarbase.".txz ".fname.": failed!" | echohl NONE ++ call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarbase}.txz {fname}: failed!") + else + echo "***note*** successfully extracted ".fname + endif +@@ -682,7 +677,7 @@ fun! tar#Extract() + let extractcmd= substitute(extractcmd,"-","-J","") + call system(extractcmd." ".shellescape(tarbase).".tar.xz ".shellescape(fname)) + if v:shell_error != 0 +- echohl Error | echo "***error*** ".extractcmd." ".tarbase.".tar.xz ".fname.": failed!" | echohl NONE ++ call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarbase}.tar.xz {fname}: failed!") + else + echo "***note*** successfully extracted ".fname + endif +@@ -691,7 +686,7 @@ fun! tar#Extract() + let extractcmd= substitute(extractcmd,"-","--zstd","") + call system(extractcmd." ".shellescape(tarbase).".tzst ".shellescape(fname)) + if v:shell_error != 0 +- echohl Error | echo "***error*** ".extractcmd." ".tarbase.".tzst ".fname.": failed!" | echohl NONE ++ call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarbase}.tzst {fname}: failed!") + else + echo "***note*** successfully extracted ".fname + endif +@@ -700,7 +695,7 @@ fun! tar#Extract() + let extractcmd= substitute(extractcmd,"-","--zstd","") + call system(extractcmd." ".shellescape(tarbase).".tar.zst ".shellescape(fname)) + if v:shell_error != 0 +- echohl Error | echo "***error*** ".extractcmd." ".tarbase.".tar.zst ".fname.": failed!" | echohl NONE ++ call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarbase}.tar.zst {fname}: failed!") + else + echo "***note*** successfully extracted ".fname + endif +@@ -709,7 +704,7 @@ fun! tar#Extract() + let extractcmd= substitute(extractcmd,"-","-I lz4","") + call system(extractcmd." ".shellescape(tarbase).".tlz4 ".shellescape(fname)) + if v:shell_error != 0 +- echohl Error | echo "***error*** ".extractcmd." ".tarbase.".tlz4 ".fname.": failed!" | echohl NONE ++ call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarbase}.tlz4 {fname}: failed!") + else + echo "***note*** successfully extracted ".fname + endif +@@ -718,7 +713,7 @@ fun! tar#Extract() + let extractcmd= substitute(extractcmd,"-","-I lz4","") + call system(extractcmd." ".shellescape(tarbase).".tar.lz4".shellescape(fname)) + if v:shell_error != 0 +- echohl Error | echo "***error*** ".extractcmd." ".tarbase.".tar.lz4 ".fname.": failed!" | echohl NONE ++ call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarbase}.tar.lz4 {fname}: failed!") + else + echo "***note*** successfully extracted ".fname + endif +@@ -731,15 +726,7 @@ endfun + " --------------------------------------------------------------------- + " s:Rmdir: {{{2 + fun! s:Rmdir(fname) +- if has("unix") +- call system("/bin/rm -rf -- ".shellescape(a:fname,0)) +- elseif has("win32") || has("win95") || has("win64") || has("win16") +- if &shell =~? "sh$" +- call system("/bin/rm -rf -- ".shellescape(a:fname,0)) +- else +- call system("del /S ".shellescape(a:fname,0)) +- endif +- endif ++ call delete(a:fname, 'rf') + endfun + + " s:FileHeader: {{{2 +@@ -765,6 +752,25 @@ fun! s:Header(fname) + return "unknown" + endfun + ++" --------------------------------------------------------------------- ++" s:WinPath: {{{2 ++fun! s:WinPath(path) ++ if (!g:netrw_cygwin || &shell !~ '\%(\\|\\)\%(\.exe\)\=$') && has("win32") ++ " remove cygdrive prefix, if present ++ let path = substitute(a:path, '/cygdrive/\(.\)', '\1:', '') ++ " remove trailing slash (Win95) ++ let path = substitute(path, '\(\\\|/\)$', '', 'g') ++ " remove escaped spaces ++ let path = substitute(path, '\ ', ' ', 'g') ++ " convert slashes to backslashes ++ let path = substitute(path, '/', '\', 'g') ++ else ++ let path = a:path ++ endif ++ ++ return path ++endfun ++ + " --------------------------------------------------------------------- + " tar#Vimuntar: installs a tarball in the user's .vim / vimfiles directory {{{2 + fun! tar#Vimuntar(...) +@@ -786,8 +792,8 @@ fun! tar#Vimuntar(...) + + if simplify(curdir) != simplify(vimhome) + " copy (possibly compressed) tarball to .vim/vimfiles +- call system(netrw#WinPath(g:tar_copycmd)." ".shellescape(tartail)." ".shellescape(vimhome)) +- exe "cd ".fnameescape(vimhome) ++ call system(s:WinPath(g:tar_copycmd)." ".shellescape(tartail)." ".shellescape(vimhome)) ++ exe "lcd ".fnameescape(vimhome) + endif + + " if necessary, decompress the tarball; then, extract it +@@ -801,14 +807,14 @@ fun! tar#Vimuntar(...) + if simplify(curdir) != simplify(tarhome) + " remove decompressed tarball, restore directory + call delete(tartail.".tar") +- exe "cd ".fnameescape(curdir) ++ exe "lcd ".fnameescape(curdir) + endif + return + endif + else + call vimball#Decompress(tartail,0) + endif +- let extractcmd= netrw#WinPath(g:tar_extractcmd) ++ let extractcmd= s:WinPath(g:tar_extractcmd) + call system(extractcmd." ".shellescape(tarbase.".tar")) + + " set up help +@@ -819,7 +825,7 @@ fun! tar#Vimuntar(...) + if simplify(tarhome) != simplify(vimhome) + " remove decompressed tarball, restore directory + call delete(vimhome."/".tarbase.".tar") +- exe "cd ".fnameescape(curdir) ++ exe "lcd ".fnameescape(curdir) + endif + endfun + +diff --git a/runtime/doc/pi_tar.txt b/runtime/doc/pi_tar.txt +index 5b317d6..52706e9 100644 +--- a/runtime/doc/pi_tar.txt ++++ b/runtime/doc/pi_tar.txt +@@ -1,11 +1,10 @@ +-*pi_tar.txt* For Vim version 9.1. Last change: 2024 May 11 ++*pi_tar.txt* For Vim version 9.1. Last change: 2025 Jul 15 + + +====================+ + | Tar File Interface | + +====================+ + +-Author: Charles E. Campbell +- (remove NOSPAM from Campbell's email first) ++Original Author: Charles E. Campbell + Copyright 2005-2017: *tar-copyright* + The VIM LICENSE (see |copyright|) applies to the files in this + package, including tarPlugin.vim, tar.vim, and pi_tar.txt. Like +@@ -61,7 +60,7 @@ Copyright 2005-2017: *tar-copyright* + the file mentioned in the tarball. If the current directory is not + correct for that path, :TarDiff will fail to find the associated file. + +- If the [filename] is given, that that filename (and path) will be used ++ If the [filename] is given, that filename (and path) will be used + to specify the associated file. + + +@@ -74,6 +73,15 @@ Copyright 2005-2017: *tar-copyright* + let g:loaded_tarPlugin= 1 + let g:loaded_tar = 1 + < ++ *tar-mappings* ++ MAPPINGS~ ++ ++ The following (buffer-local) mappings are available in a tar buffer: ++ ++ Open selected file for editing, any changes will be ++ written back to the archive. ++ same as ++ x Extract selected file. + + ============================================================================== + 3. Options *tar-options* +@@ -86,24 +94,25 @@ Copyright 2005-2017: *tar-copyright* + *g:tar_readoptions* "OPxf" used to extract a file from a tarball + *g:tar_cmd* "tar" the name of the tar program + *g:tar_nomax* 0 if true, file window will not be maximized +- *g:tar_secure* undef if exists: +- "--"s will be used to prevent unwanted +- option expansion in tar commands. +- Please be sure that your tar command +- accepts "--"; Posix compliant tar +- utilities do accept them. +- if not exists: +- The tar plugin will reject any tar +- files or member files that begin with +- "-" +- Not all tar's support the "--" which is why +- it isn't default. + *g:tar_writeoptions* "uf" used to update/replace a file + + + ============================================================================== + 4. History *tar-history* + ++ unreleased: ++ Jul 13, 2025 * drop leading / ++ May 19, 2025 * restore working directory after read/write ++ Apr 16, 2025 * decouple from netrw by adding s:WinPath() ++ instead of shelling out to file(1) ++ Mar 02, 2025 * determine the compression using readblob() ++ Mar 02, 2025 * escape the filename before using :read ++ Mar 01, 2025 * fix syntax error in tar#Read() ++ Feb 28, 2025 * add support for bzip3 (#16755) ++ Feb 06, 2025 * add support for lz4 (#16591) ++ Nov 11, 2024 * support permissions (#7379) ++ Feb 19, 2024 * announce adoption ++ Jan 08, 2024 * fix a few problems (#138331, #12637, #8109) + v31 Apr 02, 2017 * (klartext) reported that browsing encrypted + files in a zip archive created unencrypted + swap files. I am applying a similar fix +diff --git a/runtime/doc/tags b/runtime/doc/tags +index 1eebe14..f752533 100644 +--- a/runtime/doc/tags ++++ b/runtime/doc/tags +@@ -7774,7 +7774,6 @@ g:tar_copycmd pi_tar.txt /*g:tar_copycmd* + g:tar_extractcmd pi_tar.txt /*g:tar_extractcmd* + g:tar_nomax pi_tar.txt /*g:tar_nomax* + g:tar_readoptions pi_tar.txt /*g:tar_readoptions* +-g:tar_secure pi_tar.txt /*g:tar_secure* + g:tar_writeoptions pi_tar.txt /*g:tar_writeoptions* + g:termdebug_config terminal.txt /*g:termdebug_config* + g:termdebugger terminal.txt /*g:termdebugger* +diff --git a/runtime/plugin/tarPlugin.vim b/runtime/plugin/tarPlugin.vim +index 825b7ae..e55a367 100644 +--- a/runtime/plugin/tarPlugin.vim ++++ b/runtime/plugin/tarPlugin.vim +@@ -23,14 +23,14 @@ set cpo&vim + " Public Interface: {{{1 + augroup tar + au! +- au BufReadCmd tarfile::* call tar#Read(expand(""), 1) +- au FileReadCmd tarfile::* call tar#Read(expand(""), 0) ++ au BufReadCmd tarfile::* call tar#Read(expand("")) ++ au FileReadCmd tarfile::* call tar#Read(expand("")) + au BufWriteCmd tarfile::* call tar#Write(expand("")) + au FileWriteCmd tarfile::* call tar#Write(expand("")) + + if has("unix") +- au BufReadCmd tarfile::*/* call tar#Read(expand(""), 1) +- au FileReadCmd tarfile::*/* call tar#Read(expand(""), 0) ++ au BufReadCmd tarfile::*/* call tar#Read(expand("")) ++ au FileReadCmd tarfile::*/* call tar#Read(expand("")) + au BufWriteCmd tarfile::*/* call tar#Write(expand("")) + au FileWriteCmd tarfile::*/* call tar#Write(expand("")) + endif +diff --git a/src/testdir/Make_all.mak b/src/testdir/Make_all.mak +index 7d50a7e..5ceacd7 100644 +--- a/src/testdir/Make_all.mak ++++ b/src/testdir/Make_all.mak +@@ -243,6 +243,7 @@ NEW_TESTS = \ + test_plugin_glvs \ + test_plugin_man \ + test_plugin_matchparen \ ++ test_plugin_tar \ + test_plugin_termdebug \ + test_plugin_tohtml \ + test_plugin_zip \ +@@ -507,6 +508,7 @@ NEW_TESTS_RES = \ + test_plugin_glvs.res \ + test_plugin_man.res \ + test_plugin_matchparen.res \ ++ test_plugin_tar.res \ + test_plugin_termdebug.res \ + test_plugin_tohtml.res \ + test_plugin_zip.res \ +diff --git a/src/testdir/samples/evil.tar b/src/testdir/samples/evil.tar +new file mode 100644 +index 0000000..8cbc061 +--- /dev/null ++++ b/src/testdir/samples/evil.tar +@@ -0,0 +1,2 @@ ++/etc/ax-pwn0000664000175000017500000000001215035456532013224 0ustar chrisbrachrisbrasomething ++ +\ No newline at end of file +diff --git a/src/testdir/samples/sample.tar b/src/testdir/samples/sample.tar +new file mode 100644 +index 0000000..4da3bf3 +--- /dev/null ++++ b/src/testdir/samples/sample.tar +@@ -0,0 +1,2 @@ ++testtar/0000775000175000017500000000000015035512437012727 5ustar chrisbrachrisbratesttar/file1.txt0000664000175000017500000000001115035512437014460 0ustar chrisbrachrisbratestfile ++ +\ No newline at end of file +diff --git a/src/testdir/test_plugin_tar.vim b/src/testdir/test_plugin_tar.vim +new file mode 100644 +index 0000000..ea84ffe +--- /dev/null ++++ b/src/testdir/test_plugin_tar.vim +@@ -0,0 +1,130 @@ ++vim9script ++ ++source check.vim ++ ++CheckExecutable tar ++CheckNotMSWindows ++ ++runtime plugin/tarPlugin.vim ++ ++def CopyFile(source: string) ++ if !filecopy($"samples/{source}", "X.tar") ++ assert_report($"Can't copy samples/{source}") ++ endif ++enddef ++ ++def g:Test_tar_basic() ++ CopyFile("sample.tar") ++ defer delete("X.tar") ++ defer delete("./testtar", 'rf') ++ e X.tar ++ ++ ### Check header ++ assert_match('^" tar\.vim version v\d\+', getline(1)) ++ assert_match('^" Browsing tarfile .*/X.tar', getline(2)) ++ assert_match('^" Select a file with cursor and press ENTER, "x" to extract a file', getline(3)) ++ assert_match('^$', getline(4)) ++ assert_match('testtar/', getline(5)) ++ assert_match('testtar/file1.txt', getline(6)) ++ ++ ### Check ENTER on header ++ :1 ++ exe ":normal \" ++ assert_equal("X.tar", @%) ++ ++ ### Check ENTER on file ++ :6 ++ exe ":normal \" ++ assert_equal("tarfile::testtar/file1.txt", @%) ++ ++ ++ ### Check editing file ++ ### Note: deleting entries not supported on BSD ++ if has("mac") ++ return ++ endif ++ if has("bsd") ++ return ++ endif ++ s/.*/some-content/ ++ assert_equal("some-content", getline(1)) ++ w! ++ assert_equal("tarfile::testtar/file1.txt", @%) ++ bw! ++ close ++ bw! ++ ++ e X.tar ++ :6 ++ exe "normal \" ++ assert_equal("some-content", getline(1)) ++ bw! ++ close ++ ++ ### Check extracting file ++ :5 ++ normal x ++ assert_true(filereadable("./testtar/file1.txt")) ++ bw! ++enddef ++ ++def g:Test_tar_evil() ++ CopyFile("evil.tar") ++ defer delete("X.tar") ++ defer delete("./etc", 'rf') ++ e X.tar ++ ++ ### Check header ++ assert_match('^" tar\.vim version v\d\+', getline(1)) ++ assert_match('^" Browsing tarfile .*/X.tar', getline(2)) ++ assert_match('^" Select a file with cursor and press ENTER, "x" to extract a file', getline(3)) ++ assert_match('^" Note: Path Traversal Attack detected', getline(4)) ++ assert_match('^$', getline(5)) ++ assert_match('/etc/ax-pwn', getline(6)) ++ ++ ### Check ENTER on header ++ :1 ++ exe ":normal \" ++ assert_equal("X.tar", @%) ++ assert_equal(1, b:leading_slash) ++ ++ ### Check ENTER on file ++ :6 ++ exe ":normal \" ++ assert_equal(1, b:leading_slash) ++ assert_equal("tarfile::/etc/ax-pwn", @%) ++ ++ ++ ### Check editing file ++ ### Note: deleting entries not supported on BSD ++ if has("mac") ++ return ++ endif ++ if has("bsd") ++ return ++ endif ++ s/.*/none/ ++ assert_equal("none", getline(1)) ++ w! ++ assert_equal(1, b:leading_slash) ++ assert_equal("tarfile::/etc/ax-pwn", @%) ++ bw! ++ close ++ bw! ++ ++ # Writing was aborted ++ e X.tar ++ assert_match('^" Note: Path Traversal Attack detected', getline(4)) ++ :6 ++ exe "normal \" ++ assert_equal("something", getline(1)) ++ bw! ++ close ++ ++ ### Check extracting file ++ :5 ++ normal x ++ assert_true(filereadable("./etc/ax-pwn")) ++ ++ bw! ++enddef diff -Nru vim-9.1.1230/debian/patches/CVE-2025-53906.patch vim-9.1.1230/debian/patches/CVE-2025-53906.patch --- vim-9.1.1230/debian/patches/CVE-2025-53906.patch 1970-01-01 00:00:00.000000000 +0000 +++ vim-9.1.1230/debian/patches/CVE-2025-53906.patch 2026-09-25 20:25:55.000000000 +0000 @@ -0,0 +1,389 @@ +From: Lee Garrett +Date: Fri, 25 Sep 2026 17:26:06 +0200 +Subject: Fix CVE-2025-53906 + +Origin: backport, https://github.com/vim/vim/commit/586294a04179d855c3d1d4ee5ea83931963680b8 +Forwarded: not-needed +Reviewed-By: Lee Garrett +Last-Update: 2026-09-25 + +patch 9.1.1551: [security]: path traversal issue in zip.vim +Problem: [security]: path traversal issue in zip.vim (@ax) +Solution: drop leading ../ on write of zipfiles, don't forcefully + overwrite existing files + +A zip plugin which contains filenames with leading '../' may cause +confusion as to where the content will be extracted. Let's drop such +things and make sure we use a relative filename instead and don't +forcefully overwrite temporary files. Also, warn the user of such +things. + +related: #17733 + +This fixes CVE-2025-53906. + +Backport of upstream commit 586294a04179d855c3d1d4ee5ea83931963680b8. +- Runtime files (autoload/zip.vim, doc/pi_zip.txt) taken verbatim from + upstream. +- src/version.c and src/po/vim.pot bumps omitted as not relevant + to the runtime fix. +- test_plugin_zip.vim sources check.vim to match this branch's test + harness. +--- + Filelist | 1 + + runtime/autoload/zip.vim | 94 ++++++++++++++++++++++------------------ + runtime/doc/pi_zip.txt | 26 ++++++++--- + src/testdir/samples/evil.zip | Bin 0 -> 148 bytes + src/testdir/test_plugin_zip.vim | 58 ++++++++++++++++--------- + 5 files changed, 110 insertions(+), 69 deletions(-) + create mode 100644 src/testdir/samples/evil.zip + +diff --git a/Filelist b/Filelist +index bf54197..2fdfa78 100644 +--- a/Filelist ++++ b/Filelist +@@ -224,6 +224,7 @@ SRC_ALL = \ + src/testdir/samples/*.txt \ + src/testdir/samples/*.vim \ + src/testdir/samples/evil.tar \ ++ src/testdir/samples/evil.zip \ + src/testdir/samples/poc.zip \ + src/testdir/samples/sample.tar \ + src/testdir/samples/test000 \ +diff --git a/runtime/autoload/zip.vim b/runtime/autoload/zip.vim +index dae4dde..c46ec44 100644 +--- a/runtime/autoload/zip.vim ++++ b/runtime/autoload/zip.vim +@@ -15,6 +15,7 @@ + " 2024 Aug 18 by Vim Project: correctly handle special globbing chars + " 2024 Aug 21 by Vim Project: simplify condition to detect MS-Windows + " 2025 Mar 11 by Vim Project: handle filenames with leading '-' correctly ++" 2025 Jul 12 by Vim Project: drop ../ on write to prevent path traversal attacks + " License: Vim License (see vim's :help license) + " Copyright: Copyright (C) 2005-2019 Charles E. Campbell {{{1 + " Permission is hereby granted to use and distribute this code, +@@ -236,59 +237,62 @@ endfun + " zip#Write: {{{2 + fun! zip#Write(fname) + let dict = s:SetSaneOpts() ++ let need_rename = 0 + defer s:RestoreOpts(dict) + + " sanity checks + if !executable(substitute(g:zip_zipcmd,'\s\+.*$','','')) +- call s:Mess('Error', "***error*** (zip#Write) sorry, your system doesn't appear to have the ".g:zip_zipcmd." program") +- return +- endif +- if !exists("*mkdir") +- call s:Mess('Error', "***error*** (zip#Write) sorry, mkdir() doesn't work on your system") +- return ++ call s:Mess('Error', "***error*** (zip#Write) sorry, your system doesn't appear to have the ".g:zip_zipcmd." program") ++ return + endif + + let curdir= getcwd() + let tmpdir= tempname() + if tmpdir =~ '\.' +- let tmpdir= substitute(tmpdir,'\.[^.]*$','','e') ++ let tmpdir= substitute(tmpdir,'\.[^.]*$','','e') + endif + call mkdir(tmpdir,"p") + + " attempt to change to the indicated directory + if s:ChgDir(tmpdir,s:ERROR,"(zip#Write) cannot cd to temporary directory") +- return ++ return + endif + + " place temporary files under .../_ZIPVIM_/ + if isdirectory("_ZIPVIM_") +- call delete("_ZIPVIM_", "rf") ++ call delete("_ZIPVIM_", "rf") + endif + call mkdir("_ZIPVIM_") + cd _ZIPVIM_ + + if has("unix") +- let zipfile = substitute(a:fname,'zipfile://\(.\{-}\)::[^\\].*$','\1','') +- let fname = substitute(a:fname,'zipfile://.\{-}::\([^\\].*\)$','\1','') ++ let zipfile = substitute(a:fname,'zipfile://\(.\{-}\)::[^\\].*$','\1','') ++ let fname = substitute(a:fname,'zipfile://.\{-}::\([^\\].*\)$','\1','') + else +- let zipfile = substitute(a:fname,'^.\{-}zipfile://\(.\{-}\)::[^\\].*$','\1','') +- let fname = substitute(a:fname,'^.\{-}zipfile://.\{-}::\([^\\].*\)$','\1','') ++ let zipfile = substitute(a:fname,'^.\{-}zipfile://\(.\{-}\)::[^\\].*$','\1','') ++ let fname = substitute(a:fname,'^.\{-}zipfile://.\{-}::\([^\\].*\)$','\1','') ++ endif ++ if fname =~ '^[.]\{1,2}/' ++ call system(g:zip_zipcmd." -d ".s:Escape(fnamemodify(zipfile,":p"),0)." ".s:Escape(fname,0)) ++ let fname = fname->substitute('^\([.]\{1,2}/\)\+', '', 'g') ++ let need_rename = 1 + endif + + if fname =~ '/' +- let dirpath = substitute(fname,'/[^/]\+$','','e') +- if has("win32unix") && executable("cygpath") ++ let dirpath = substitute(fname,'/[^/]\+$','','e') ++ if has("win32unix") && executable("cygpath") + let dirpath = substitute(system("cygpath ".s:Escape(dirpath,0)),'\n','','e') +- endif +- call mkdir(dirpath,"p") ++ endif ++ call mkdir(dirpath,"p") + endif + if zipfile !~ '/' +- let zipfile= curdir.'/'.zipfile ++ let zipfile= curdir.'/'.zipfile + endif + +- exe "w! ".fnameescape(fname) ++ " don't overwrite files forcefully ++ exe "w ".fnameescape(fname) + if has("win32unix") && executable("cygpath") +- let zipfile = substitute(system("cygpath ".s:Escape(zipfile,0)),'\n','','e') ++ let zipfile = substitute(system("cygpath ".s:Escape(zipfile,0)),'\n','','e') + endif + + if (has("win32") || has("win95") || has("win64") || has("win16")) && &shell !~? 'sh$' +@@ -297,21 +301,24 @@ fun! zip#Write(fname) + + call system(g:zip_zipcmd." -u ".s:Escape(fnamemodify(zipfile,":p"),0)." ".s:Escape(fname,0)) + if v:shell_error != 0 +- call s:Mess('Error', "***error*** (zip#Write) sorry, unable to update ".zipfile." with ".fname) ++ call s:Mess('Error', "***error*** (zip#Write) sorry, unable to update ".zipfile." with ".fname) + + elseif s:zipfile_{winnr()} =~ '^\a\+://' +- " support writing zipfiles across a network +- let netzipfile= s:zipfile_{winnr()} +- 1split|enew +- let binkeep= &binary +- let eikeep = &ei +- set binary ei=all +- exe "noswapfile e! ".fnameescape(zipfile) +- call netrw#NetWrite(netzipfile) +- let &ei = eikeep +- let &binary = binkeep +- q! +- unlet s:zipfile_{winnr()} ++ " support writing zipfiles across a network ++ let netzipfile= s:zipfile_{winnr()} ++ 1split|enew ++ let binkeep= &binary ++ let eikeep = &ei ++ set binary ei=all ++ exe "noswapfile e! ".fnameescape(zipfile) ++ call netrw#NetWrite(netzipfile) ++ let &ei = eikeep ++ let &binary = binkeep ++ q! ++ unlet s:zipfile_{winnr()} ++ elseif need_rename ++ exe $"sil keepalt file {fnameescape($"zipfile://{zipfile}::{fname}")}" ++ call s:Mess('Warning', "***error*** (zip#Browse) Path Traversal Attack detected, dropping relative path") + endif + + " cleanup and restore current directory +@@ -320,7 +327,6 @@ fun! zip#Write(fname) + call s:ChgDir(curdir,s:WARNING,"(zip#Write) unable to return to ".curdir."!") + call delete(tmpdir, "rf") + setlocal nomod +- + endfun + + " --------------------------------------------------------------------- +@@ -333,15 +339,18 @@ fun! zip#Extract() + + " sanity check + if fname =~ '^"' +- return ++ return + endif + if fname =~ '/$' +- call s:Mess('Error', "***error*** (zip#Extract) Please specify a file, not a directory") +- return ++ call s:Mess('Error', "***error*** (zip#Extract) Please specify a file, not a directory") ++ return ++ elseif fname =~ '^[.]\?[.]/' ++ call s:Mess('Error', "***error*** (zip#Browse) Path Traversal Attack detected, not extracting!") ++ return + endif + if filereadable(fname) +- call s:Mess('Error', "***error*** (zip#Extract) <" .. fname .."> already exists in directory, not overwriting!") +- return ++ call s:Mess('Error', "***error*** (zip#Extract) <" .. fname .."> already exists in directory, not overwriting!") ++ return + endif + let target = fname->substitute('\[', '[[]', 'g') + " unzip 6.0 does not support -- to denote end-of-arguments +@@ -363,13 +372,12 @@ fun! zip#Extract() + " extract the file mentioned under the cursor + call system($"{g:zip_extractcmd} -o {shellescape(b:zipfile)} {target}") + if v:shell_error != 0 +- call s:Mess('Error', "***error*** ".g:zip_extractcmd." ".b:zipfile." ".fname.": failed!") ++ call s:Mess('Error', "***error*** ".g:zip_extractcmd." ".b:zipfile." ".fname.": failed!") + elseif !filereadable(fname) +- call s:Mess('Error', "***error*** attempted to extract ".fname." but it doesn't appear to be present!") ++ call s:Mess('Error', "***error*** attempted to extract ".fname." but it doesn't appear to be present!") + else +- echomsg "***note*** successfully extracted ".fname ++ echomsg "***note*** successfully extracted ".fname + endif +- + endfun + + " --------------------------------------------------------------------- +diff --git a/runtime/doc/pi_zip.txt b/runtime/doc/pi_zip.txt +index 2e98bec..0f7ef4e 100644 +--- a/runtime/doc/pi_zip.txt ++++ b/runtime/doc/pi_zip.txt +@@ -1,4 +1,4 @@ +-*pi_zip.txt* For Vim version 9.1. Last change: 2023 Nov 05 ++*pi_zip.txt* For Vim version 9.1. Last change: 2025 Jul 15 + + +====================+ + | Zip File Interface | +@@ -100,15 +100,29 @@ Copyright: Copyright (C) 2005-2015 Charles E Campbell *zip-copyright* + should be treated as zip files. + + Alternatively, one may change *g:zipPlugin_ext* in one's .vimrc. +- Currently (11/30/15) it holds: > ++ Currently (as of April 2025) it holds: > + +- let g:zipPlugin_ext= '*.zip,*.jar,*.xpi,*.ja,*.war,*.ear,*.celzip, +- \ *.oxt,*.kmz,*.wsz,*.xap,*.docx,*.docm,*.dotx,*.dotm,*.potx,*.potm, +- \ *.ppsx,*.ppsm,*.pptx,*.pptm,*.ppam,*.sldx,*.thmx,*.xlam,*.xlsx,*.xlsm, +- \ *.xlsb,*.xltx,*.xltm,*.xlam,*.crtx,*.vdw,*.glox,*.gcsx,*.gqsx,*.epub' ++ let g:zipPlugin_ext='*.aar,*.apk,*.celzip,*.crtx,*.docm,*.docx, ++ \ *.dotm,*.dotx,*.ear,*.epub,*.gcsx,*.glox,*.gqsx,*.ja,*.jar,*.kmz, ++ \ *.odb,*.odc,*.odf,*.odg,*.odi,*.odm,*.odp,*.ods,*.odt,*.otc,*.otf, ++ \ *.otg,*.oth,*.oti,*.otp,*.ots,*.ott,*.oxt,*.potm,*.potx,*.ppam, ++ \ *.ppsm,*.ppsx,*.pptm,*.pptx,*.sldx,*.thmx,*.vdw,*.war,*.whl,*.wsz, ++ \ *.xap,*.xlam,*.xlsb,*.xlsm,*.xlsx,*.xltm,*.xltx,*.xpi,*.zip' + + ============================================================================== + 4. History *zip-history* {{{1 ++ unreleased: ++ Jul 12, 2025 * drop ../ on write to prevent path traversal attacks ++ Mar 11, 2025 * handle filenames with leading '-' correctly ++ Aug 21, 2024 * simplify condition to detect MS-Windows ++ Aug 18, 2024 * correctly handle special globbing chars ++ Aug 05, 2024 * clean-up and make it work with shellslash on Windows ++ Aug 05, 2024 * workaround for the FreeBSD's unzip ++ Aug 04, 2024 * escape '[' in name of file to be extracted ++ Jul 30, 2024 * fix opening remote zipfile ++ Jul 24, 2024 * use delete() function ++ Jul 23, 2024 * fix 'x' command ++ Jun 16, 2024 * handle whitespace on Windows properly (#14998) + v33 Dec 07, 2021 * *.xlam mentioned twice in zipPlugin + v32 Oct 22, 2021 * to avoid an issue with a vim 8.2 patch, zipfile: has + been changed to zipfile:// . This often shows up +diff --git a/src/testdir/samples/evil.zip b/src/testdir/samples/evil.zip +new file mode 100644 +index 0000000..e0a7f96 +--- /dev/null ++++ b/src/testdir/samples/evil.zip +@@ -0,0 +1,2 @@ ++PKhìZû^³…../../../../etc/ax-pwnpwned ++PKhìZû^³…´�../../../../etc/ax-pwnPKD: +\ No newline at end of file +diff --git a/src/testdir/test_plugin_zip.vim b/src/testdir/test_plugin_zip.vim +index 2050b4c..2adb48b 100644 +--- a/src/testdir/test_plugin_zip.vim ++++ b/src/testdir/test_plugin_zip.vim +@@ -1,23 +1,25 @@ +-so check.vim ++vim9script ++ ++source check.vim + + CheckExecutable unzip + +-if 0 " Find uncovered line ++if 0 # Find uncovered line + profile start zip_profile + profile! file */zip*.vim + endif + + runtime plugin/zipPlugin.vim + +-def Test_zip_basic() +- +- ### get our zip file +- if !filecopy("samples/test.zip", "X.zip") +- assert_report("Can't copy samples/test.zip") +- return ++def CopyZipFile(source: string) ++ if !filecopy($"samples/{source}", "X.zip") ++ assert_report($"Can't copy samples/{source}.zip") + endif +- defer delete("X.zip") ++enddef + ++def g:Test_zip_basic() ++ CopyZipFile("test.zip") ++ defer delete("X.zip") + e X.zip + + ### Check header +@@ -138,15 +140,11 @@ def Test_zip_basic() + bw + enddef + +-def Test_zip_glob_fname() ++def g:Test_zip_glob_fname() + CheckNotMSWindows + # does not work on Windows, why? + +- ### copy sample zip file +- if !filecopy("samples/testa.zip", "X.zip") +- assert_report("Can't copy samples/testa.zip") +- return +- endif ++ CopyZipFile("testa.zip") + defer delete("X.zip") + defer delete('zipglob', 'rf') + +@@ -236,14 +234,11 @@ def Test_zip_glob_fname() + bw + enddef + +-def Test_zip_fname_leading_hyphen() ++def g:Test_zip_fname_leading_hyphen() + CheckNotMSWindows + + ### copy sample zip file +- if !filecopy("samples/poc.zip", "X.zip") +- assert_report("Can't copy samples/poc.zip") +- return +- endif ++ CopyZipFile("poc.zip") + defer delete("X.zip") + defer delete('-d', 'rf') + defer delete('/tmp/pwned', 'rf') +@@ -258,3 +253,26 @@ def Test_zip_fname_leading_hyphen() + assert_false(filereadable('/tmp/pwned')) + bw + enddef ++ ++def g:Test_zip_fname_evil_path() ++ CheckNotMSWindows ++ # needed for writing the zip file ++ CheckExecutable zip ++ ++ CopyZipFile("evil.zip") ++ defer delete("X.zip") ++ e X.zip ++ ++ :1 ++ var fname = 'pwn' ++ search('\V' .. fname) ++ normal x ++ assert_false(filereadable('/etc/ax-pwn')) ++ var mess = execute(':mess') ++ assert_match('Path Traversal Attack', mess) ++ ++ exe ":normal \" ++ :w ++ assert_match('zipfile://.*::etc/ax-pwn', @%) ++ bw ++enddef diff -Nru vim-9.1.1230/debian/patches/CVE-2026-25749.patch vim-9.1.1230/debian/patches/CVE-2026-25749.patch --- vim-9.1.1230/debian/patches/CVE-2026-25749.patch 1970-01-01 00:00:00.000000000 +0000 +++ vim-9.1.1230/debian/patches/CVE-2026-25749.patch 2026-09-25 20:25:55.000000000 +0000 @@ -0,0 +1,62 @@ +From: Lee Garrett +Date: Fri, 25 Sep 2026 17:26:06 +0200 +Subject: Fix CVE-2026-25749 + +Origin: backport, https://github.com/vim/vim/commit/0714b15940b245108e6e9d7aa2260dd849a26fa9 +Bug: https://github.com/vim/vim/security/advisories/GHSA-5w93-4g67-mm43 +Forwarded: not-needed +Reviewed-By: Lee Garrett +Last-Update: 2026-09-25 + +patch 9.1.2132: [security]: buffer-overflow in 'helpfile' option handling +Problem: [security]: buffer-overflow in 'helpfile' option handling by + using strcpy without bound checks (Rahul Hoysala) +Solution: Limit strncpy to the length of the buffer (MAXPATHL) + +Github Advisory: +https://github.com/vim/vim/security/advisories/GHSA-5w93-4g67-mm43 + +This fixes CVE-2026-25749. + +Backport of upstream commit 0714b15940b245108e6e9d7aa2260dd849a26fa9. +- src/tag.c fix and src/testdir/test_help.vim test taken verbatim from + upstream. +- src/version.c and runtime/doc/version9.txt bumps omitted as not + relevant to this branch. +--- + src/tag.c | 2 +- + src/testdir/test_help.vim | 9 +++++++++ + 2 files changed, 10 insertions(+), 1 deletion(-) + +diff --git a/src/tag.c b/src/tag.c +index f61f116..21ac1f1 100644 +--- a/src/tag.c ++++ b/src/tag.c +@@ -3346,7 +3346,7 @@ get_tagfname( + if (tnp->tn_hf_idx > tag_fnames.ga_len || *p_hf == NUL) + return FAIL; + ++tnp->tn_hf_idx; +- STRCPY(buf, p_hf); ++ vim_strncpy(buf, p_hf, MAXPATHL - 1); + STRCPY(gettail(buf), "tags"); + #ifdef BACKSLASH_IN_FILENAME + slash_adjust(buf); +diff --git a/src/testdir/test_help.vim b/src/testdir/test_help.vim +index 6c8b3ab..2904b3a 100644 +--- a/src/testdir/test_help.vim ++++ b/src/testdir/test_help.vim +@@ -205,5 +205,14 @@ func Test_help_using_visual_match() + call v9.CheckScriptFailure(lines, 'E149:') + endfunc + ++" This caused a buffer overflow ++func Test_helpfile_overflow() ++ let _helpfile = &helpfile ++ let &helpfile = repeat('A', 5000) ++ help ++ helpclose ++ let &helpfile = _helpfile ++endfunc ++ + + " vim: shiftwidth=2 sts=2 expandtab diff -Nru vim-9.1.1230/debian/patches/CVE-2026-26269.patch vim-9.1.1230/debian/patches/CVE-2026-26269.patch --- vim-9.1.1230/debian/patches/CVE-2026-26269.patch 1970-01-01 00:00:00.000000000 +0000 +++ vim-9.1.1230/debian/patches/CVE-2026-26269.patch 2026-09-25 20:25:55.000000000 +0000 @@ -0,0 +1,132 @@ +From: Lee Garrett +Date: Fri, 25 Sep 2026 17:26:06 +0200 +Subject: Fix CVE-2026-26269 + +Origin: backport, https://github.com/vim/vim/commit/c5f312aad8e4179e437f81ad39a860cd0ef11970 +Bug: https://github.com/vim/vim/security/advisories/GHSA-9w5c-hwr9-hc68 +Forwarded: not-needed +Reviewed-By: Lee Garrett +Last-Update: 2026-09-25 + +patch 9.1.2148: [security]: Buffer overflow in netbeans interface +Problem: [security]: Buffer overflow in netbeans special_keys() handling +Solution: Limit writing to max KEYBUFLEN bytes to prevent writing out of + bounds. + +Github Advisory: +https://github.com/vim/vim/security/advisories/GHSA-9w5c-hwr9-hc68 + +This fixes CVE-2026-26269. + +Backport of upstream commit c5f312aad8e4179e437f81ad39a860cd0ef11970. +- src/netbeans.c fix and src/testdir/test_netbeans.{vim,py} tests taken + verbatim from upstream. +- src/version.c and runtime/doc/version9.txt bumps omitted as not + relevant to this branch. +--- + src/netbeans.c | 2 +- + src/testdir/test_netbeans.py | 4 ++- + src/testdir/test_netbeans.vim | 57 +++++++++++++++++++++++++++++++++++++++++++ + 3 files changed, 61 insertions(+), 2 deletions(-) + +diff --git a/src/netbeans.c b/src/netbeans.c +index 781caa8..f27904f 100644 +--- a/src/netbeans.c ++++ b/src/netbeans.c +@@ -2299,7 +2299,7 @@ special_keys(char_u *args) + if ((sep = strchr(tok, '-')) != NULL) + { + *sep = NUL; +- while (*tok) ++ while (*tok && i + 2 < KEYBUFLEN) + { + switch (*tok) + { +diff --git a/src/testdir/test_netbeans.py b/src/testdir/test_netbeans.py +index 0d6b096..585886f 100644 +--- a/src/testdir/test_netbeans.py ++++ b/src/testdir/test_netbeans.py +@@ -112,7 +112,9 @@ class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler): + 'startAtomic_Test' : '0:startAtomic!94\n', + 'endAtomic_Test' : '0:endAtomic!95\n', + 'AnnoScale_Test' : "".join(['2:defineAnnoType!60 ' + str(i) + ' "s' + str(i) + '" "x" "=>" blue none\n' for i in range(2, 26)]), +- 'detach_Test' : '2:close!96\n1:close!97\nDETACH\n' ++ 'detach_Test' : '2:close!96\n1:close!97\nDETACH\n', ++ 'specialKeys_overflow_Test' : '0:specialKeys!200 "' + 'A'*80 + '-X"\n' ++ + } + # execute the specified test + if cmd not in testmap: +diff --git a/src/testdir/test_netbeans.vim b/src/testdir/test_netbeans.vim +index e458e38..fad24d0 100644 +--- a/src/testdir/test_netbeans.vim ++++ b/src/testdir/test_netbeans.vim +@@ -961,6 +961,58 @@ func Nb_bwipe_buffer(port) + sleep 10m + endfunc + ++func Nb_specialKeys_overflow(port) ++ call delete("Xnetbeans") ++ call writefile([], "Xnetbeans") ++ ++ " Last line number in the Xnetbeans file. Used to verify the result of the ++ " communication with the netbeans server ++ let g:last = 0 ++ ++ " Establish the connection with the netbeans server ++ exe 'nbstart :localhost:' .. a:port .. ':bunny' ++ call WaitFor('len(ReadXnetbeans()) > (g:last + 2)') ++ let l = ReadXnetbeans() ++ call assert_equal(['AUTH bunny', ++ \ '0:version=0 "2.5"', ++ \ '0:startupDone=0'], l[-3:]) ++ let g:last += 3 ++ ++ " Open the command buffer to communicate with the server ++ split Xcmdbuf ++ let cmdbufnr = bufnr() ++ call WaitFor('len(ReadXnetbeans()) > (g:last + 2)') ++ let l = ReadXnetbeans() ++ call assert_equal('0:fileOpened=0 "Xcmdbuf" T F', ++ \ substitute(l[-3], '".*/', '"', '')) ++ call assert_equal('send: 1:putBufferNumber!15 "Xcmdbuf"', ++ \ substitute(l[-2], '".*/', '"', '')) ++ call assert_equal('1:startDocumentListen!16', l[-1]) ++ let g:last += 3 ++ ++ " Keep the command buffer loaded for communication ++ hide ++ ++ sleep 1m ++ ++ " Open the command buffer to communicate with the server ++ split Xcmdbuf ++ let cmdbufnr = bufnr() ++ call appendbufline(cmdbufnr, '$', 'specialKeys_overflow_Test') ++ call WaitFor('len(ReadXnetbeans()) >= (g:last + 6)') ++ call WaitForAssert({-> assert_match('send: 0:specialKeys!200 "A\{80}-X"', ++ \ ReadXnetbeans()[-1])}) ++ ++ " Verify that specialKeys test, still works after the previous junk ++ call appendbufline(cmdbufnr, '$', 'specialKeys_Test') ++ call WaitFor('len(ReadXnetbeans()) >= (g:last + 1)') ++ call WaitForAssert({-> assert_match('^send: 0:specialKeys!91 "F12 F13 C-F13"$', ++ \ ReadXnetbeans()[-1])}) ++ let g:last += 1 ++ ++ sleep 10m ++endfunc ++ + " This test used to reference a buffer after it was freed leading to an ASAN + " error. + func Test_nb_bwipe_buffer() +@@ -970,4 +1022,9 @@ func Test_nb_bwipe_buffer() + nbclose + endfunc + ++" Verify that the specialKeys argument does not overflow ++func Test_nb_specialKeys_overflow() ++ call s:run_server('Nb_specialKeys_overflow') ++endfunc ++ + " vim: shiftwidth=2 sts=2 expandtab diff -Nru vim-9.1.1230/debian/patches/CVE-2026-28417+regression-fix.patch vim-9.1.1230/debian/patches/CVE-2026-28417+regression-fix.patch --- vim-9.1.1230/debian/patches/CVE-2026-28417+regression-fix.patch 1970-01-01 00:00:00.000000000 +0000 +++ vim-9.1.1230/debian/patches/CVE-2026-28417+regression-fix.patch 2026-09-25 20:25:55.000000000 +0000 @@ -0,0 +1,113 @@ +From: Lee Garrett +Date: Fri, 25 Sep 2026 17:26:06 +0200 +Subject: Fix CVE-2026-28417 + fix regression + +Origin: backport, https://github.com/vim/vim/commit/79348dbbc09332130f4c86045e1541d68514fcc1 + https://github.com/vim/vim/commit/a6198523fb28a50d96945458792cdb4787d3cdda +Bug: https://github.com/vim/vim/security/advisories/GHSA-m3xh-9434-g336 +Forwarded: not-needed +Reviewed-By: Lee Garrett +Last-Update: 2026-09-25 + +patch 9.2.0073: [security]: possible command injection using netrw + +Problem: [security]: Insufficient validation of hostname and port in + netrw URIs allows command injection via shell metacharacters + (ehdgks0627, un3xploitable). +Solution: Implement stricter RFC1123 hostname and IP validation. + Use shellescape() for the provided hostname and port. + +Github Advisory: +https://github.com/vim/vim/security/advisories/GHSA-m3xh-9434-g336 + +This fixes CVE-2026-28417. + +Backport of upstream commit 79348dbbc09332130f4c86045e1541d68514fcc1. +- runtime/pack/dist/opt/netrw/autoload/netrw.vim fix taken verbatim from + upstream. +- src/testdir/test_plugin_netrw.vim test omitted as the file does not + exist on this branch. +- src/version.c bump omitted as not relevant to the runtime fix. + +####################################################################### + +patch 9.2.0089: netrw: does not take port into account in hostname validation + +Problem: netrw: does not take port into account in hostname validation + (after v9.2.0073) +Solution: Update hostname validation check and test for an optional port + number (Miguel Barro) + +closes: #19533 + +Backport of upstream commit a6198523fb28a50d96945458792cdb4787d3cdda. +- runtime/pack/dist/opt/netrw/autoload/netrw.vim fix taken verbatim from + upstream. Also fixes a regression in v9.2.0073 where the unescaped + '{,62}' multi was treated literally and rejected all normal hostnames. +- src/testdir/test_plugin_netrw.vim test omitted as the file does not + exist on this branch. +- src/version.c bump omitted as not relevant to the runtime fix. +--- + runtime/pack/dist/opt/netrw/autoload/netrw.vim | 36 +++++++++++++++++++------- + 1 file changed, 26 insertions(+), 10 deletions(-) + +diff --git a/runtime/pack/dist/opt/netrw/autoload/netrw.vim b/runtime/pack/dist/opt/netrw/autoload/netrw.vim +index feee23d..15e986e 100644 +--- a/runtime/pack/dist/opt/netrw/autoload/netrw.vim ++++ b/runtime/pack/dist/opt/netrw/autoload/netrw.vim +@@ -3100,13 +3100,29 @@ endfun + + " s:NetrwValidateHostname: Validate that the hostname is valid {{{2 + " Input: +-" hostname ++" hostname, may include an optional username and port number, e.g. ++" user@hostname:port ++" allow a alphanumeric hostname or an IPv(4/6) address + " Output: + " true if g:netrw_machine is valid according to RFC1123 #Section 2 + fun! s:NetrwValidateHostname(hostname) +- " RFC1123#section-2 mandates, a valid hostname starts with letters or digits +- " so reject everyhing else +- return a:hostname =~? '^[a-z0-9]' ++ " Username: ++ let user_pat = '\%([a-zA-Z0-9._-]\+@\)\?' ++ " Hostname: 1-64 chars, alphanumeric/dots/hyphens. ++ " No underscores. No leading/trailing dots/hyphens. ++ let host_pat = '[a-zA-Z0-9]\%([-a-zA-Z0-9.]\{0,62}[a-zA-Z0-9]\)\?' ++ " Port: 16 bit unsigned integer ++ let port_pat = '\%(:\d\{1,5\}\)\?$' ++ ++ " IPv4: 1-3 digits separated by dots ++ let ipv4_pat = '\%(\d\{1,3}\.\)\{3\}\d\{1,3\}' ++ ++ " IPv6: Hex, colons, and optional brackets ++ let ipv6_pat = '\[\?\%([a-fA-F0-9:]\{2,}\)\+\]\?' ++ ++ return a:hostname =~? '^'.user_pat.host_pat.port_pat || ++ \ a:hostname =~? '^'.user_pat.ipv4_pat.port_pat || ++ \ a:hostname =~? '^'.user_pat.ipv6_pat.port_pat + endfun + + " --------------------------------------------------------------------- +@@ -10568,15 +10584,15 @@ endfun + " a correct command for use with a system() call + fun! s:MakeSshCmd(sshcmd) + " call Dfunc("s:MakeSshCmd(sshcmd<".a:sshcmd.">) user<".s:user."> machine<".s:machine.">") +- if s:user == "" +- let sshcmd = substitute(a:sshcmd,'\',s:machine,'') +- else +- let sshcmd = substitute(a:sshcmd,'\',s:user."@".s:machine,'') ++ let machine = shellescape(s:machine, 1) ++ if s:user != '' ++ let machine = shellescape(s:user, 1).'@'.machine + endif ++ let sshcmd = substitute(a:sshcmd,'\',machine,'') + if exists("g:netrw_port") && g:netrw_port != "" +- let sshcmd= substitute(sshcmd,"USEPORT",g:netrw_sshport.' '.g:netrw_port,'') ++ let sshcmd= substitute(sshcmd,"USEPORT",g:netrw_sshport.' '.shellescape(g:netrw_port,1),'') + elseif exists("s:port") && s:port != "" +- let sshcmd= substitute(sshcmd,"USEPORT",g:netrw_sshport.' '.s:port,'') ++ let sshcmd= substitute(sshcmd,"USEPORT",g:netrw_sshport.' '.shellescape(s:port,1),'') + else + let sshcmd= substitute(sshcmd,"USEPORT ",'','') + endif diff -Nru vim-9.1.1230/debian/patches/CVE-2026-28418.patch vim-9.1.1230/debian/patches/CVE-2026-28418.patch --- vim-9.1.1230/debian/patches/CVE-2026-28418.patch 1970-01-01 00:00:00.000000000 +0000 +++ vim-9.1.1230/debian/patches/CVE-2026-28418.patch 2026-09-25 20:25:55.000000000 +0000 @@ -0,0 +1,68 @@ +From: Lee Garrett +Date: Fri, 25 Sep 2026 17:26:06 +0200 +Subject: Fix CVE-2026-28418 + +Origin: backport, https://github.com/vim/vim/commit/f6a7f469a9c0d09e84cd6cb46c3a9e76f684da2d +Bug: https://github.com/vim/vim/security/advisories/GHSA-h4mf-vg97-hj8j +Forwarded: not-needed +Reviewed-By: Lee Garrett +Last-Update: 2026-09-25 + +patch 9.2.0074: [security]: Crash with overlong emacs tag file +Problem: Crash with overlong emacs tag file, because of an OOB buffer + read (ehdgks0627, un3xploitable) +Solution: Check for end of buffer and return early. + +Github Advisory: +https://github.com/vim/vim/security/advisories/GHSA-h4mf-vg97-hj8j + +This fixes CVE-2026-28418. + +Backport of upstream commit f6a7f469a9c0d09e84cd6cb46c3a9e76f684da2d. +- src/tag.c fix and src/testdir/test_taglist.vim test taken verbatim + from upstream. +- src/version.c and runtime/doc/version9.txt bumps omitted as not + relevant to this branch. +--- + src/tag.c | 3 +++ + src/testdir/test_taglist.vim | 15 +++++++++++++++ + 2 files changed, 18 insertions(+) + +diff --git a/src/tag.c b/src/tag.c +index 21ac1f1..7c83ee2 100644 +--- a/src/tag.c ++++ b/src/tag.c +@@ -1900,6 +1900,9 @@ emacs_tags_new_filename(findtags_state_T *st) + + for (p = st->ebuf; *p && *p != ','; p++) + ; ++ // invalid ++ if (*p == NUL) ++ return; + *p = NUL; + + // check for an included tags file. +diff --git a/src/testdir/test_taglist.vim b/src/testdir/test_taglist.vim +index c5c7df2..6dbff33 100644 +--- a/src/testdir/test_taglist.vim ++++ b/src/testdir/test_taglist.vim +@@ -304,4 +304,19 @@ func Test_tag_complete_with_overlong_line() + set tags& + endfunc + ++" This used to crash Vim ++func Test_evil_emacs_tagfile() ++ CheckFeature emacs_tags ++ let longline = repeat('a', 515) ++ call writefile([ ++ \ "\x0c", ++ \ longline ++ \ ], 'Xtags', 'D') ++ set tags=Xtags ++ ++ call assert_fails(':tag a', 'E426:') ++ ++ set tags& ++endfunc ++ + " vim: shiftwidth=2 sts=2 expandtab diff -Nru vim-9.1.1230/debian/patches/CVE-2026-28419.patch vim-9.1.1230/debian/patches/CVE-2026-28419.patch --- vim-9.1.1230/debian/patches/CVE-2026-28419.patch 1970-01-01 00:00:00.000000000 +0000 +++ vim-9.1.1230/debian/patches/CVE-2026-28419.patch 2026-09-25 20:25:55.000000000 +0000 @@ -0,0 +1,75 @@ +From: Lee Garrett +Date: Fri, 25 Sep 2026 17:26:06 +0200 +Subject: Fix CVE-2026-28419 + +Origin: backport, https://github.com/vim/vim/commit/9b7dfa2948c9e1e5e32a5812812d580c7879f4a0 +Bug: https://github.com/vim/vim/security/advisories/GHSA-xcc8-r6c5-hvwv +Forwarded: not-needed +Reviewed-By: Lee Garrett +Last-Update: 2026-09-25 + +patch 9.2.0075: [security]: Buffer underflow with emacs tag file +Problem: When parsing a malformed Emacs-style tags file, a 1-byte + heap-buffer-underflow read occurs if the 0x7f delimiter + appears at the very beginning of a line. This happens + because the code attempts to scan backward for a tag + name from the delimiter without checking if space exists. + (ehdgks0627, un3xploitable) +Solution: Add a check to ensure the delimiter (p_7f) is not at the + start of the buffer (lbuf) before attempting to isolate + the tag name. + +Github Advisory: +https://github.com/vim/vim/security/advisories/GHSA-xcc8-r6c5-hvwv + +This fixes CVE-2026-28419. + +Backport of upstream commit 9b7dfa2948c9e1e5e32a5812812d580c7879f4a0. +- src/tag.c fix and src/testdir/test_taglist.vim test taken verbatim + from upstream. +- src/version.c and runtime/doc/version9.txt bumps omitted as not + relevant to this branch. +--- + src/tag.c | 3 +++ + src/testdir/test_taglist.vim | 16 ++++++++++++++++ + 2 files changed, 19 insertions(+) + +diff --git a/src/tag.c b/src/tag.c +index 7c83ee2..85acbe8 100644 +--- a/src/tag.c ++++ b/src/tag.c +@@ -2021,6 +2021,9 @@ etag_fail: + } + else // second format: isolate tagname + { ++ if (p_7f == lbuf) ++ goto etag_fail; ++ + // find end of tagname + for (p = p_7f - 1; !vim_iswordc(*p); --p) + if (p == lbuf) +diff --git a/src/testdir/test_taglist.vim b/src/testdir/test_taglist.vim +index 6dbff33..9d666d5 100644 +--- a/src/testdir/test_taglist.vim ++++ b/src/testdir/test_taglist.vim +@@ -319,4 +319,20 @@ func Test_evil_emacs_tagfile() + set tags& + endfunc + ++" This used to crash Vim due to a heap-buffer-underflow ++func Test_emacs_tagfile_underflow() ++ CheckFeature emacs_tags ++ " The sequence from the crash artifact: ++ let lines = [ ++ \ "\x0c\xff\xffT\x19\x8a", ++ \ "\x19\x19\x0dtags\x19\x19\x19\x00\xff\xff\xff", ++ \ "\x7f3\x0c" ++ \ ] ++ call writefile(lines, 'Xtags', 'D') ++ set tags=Xtags ++ call assert_fails(':tag a', 'E431:') ++ ++ set tags& ++endfunc ++ + " vim: shiftwidth=2 sts=2 expandtab diff -Nru vim-9.1.1230/debian/patches/CVE-2026-28420.patch vim-9.1.1230/debian/patches/CVE-2026-28420.patch --- vim-9.1.1230/debian/patches/CVE-2026-28420.patch 1970-01-01 00:00:00.000000000 +0000 +++ vim-9.1.1230/debian/patches/CVE-2026-28420.patch 2026-09-25 20:25:55.000000000 +0000 @@ -0,0 +1,166 @@ +From: Lee Garrett +Date: Fri, 25 Sep 2026 17:26:06 +0200 +Subject: Fix CVE-2026-28420 + +Origin: backport, https://github.com/vim/vim/commit/bb6de2105b160e729c340631435cd62f3e69bd32 +Bug: https://github.com/vim/vim/security/advisories/GHSA-rvj2-jrf9-2phg +Forwarded: not-needed +Reviewed-By: Lee Garrett +Last-Update: 2026-09-25 + +patch 9.2.0076: [security]: buffer-overflow in terminal handling +Problem: When processing terminal output with many combining characters + from supplementary planes (4-byte UTF-8), a heap-buffer + overflow occurs. Additionally, the loop iterating over + cell characters can read past the end of the vterm array + (ehdgks0627, un3xploitable). +Solution: Use VTERM_MAX_CHARS_PER_CELL * 4 for ga_grow() to ensure + sufficient space. Add a boundary check to the character + loop to prevent index out-of-bounds access. + +Github Advisory: +https://github.com/vim/vim/security/advisories/GHSA-rvj2-jrf9-2phg + +This fixes CVE-2026-28420. + +Backport of upstream commit bb6de2105b160e729c340631435cd62f3e69bd32. +- src/terminal.c fix, src/testdir/test_terminal3.vim test and + src/testdir/samples/terminal_max_combining_chars.txt taken verbatim + from upstream. +- src/version.c bump omitted as not relevant to this branch. +--- + src/terminal.c | 5 +- + .../samples/terminal_max_combining_chars.txt | 80 ++++++++++++++++++++++ + src/testdir/test_terminal3.vim | 14 ++++ + 3 files changed, 97 insertions(+), 2 deletions(-) + create mode 100644 src/testdir/samples/terminal_max_combining_chars.txt + +diff --git a/src/terminal.c b/src/terminal.c +index 6edc21a..27e4c47 100644 +--- a/src/terminal.c ++++ b/src/terminal.c +@@ -3517,12 +3517,13 @@ handle_pushline(int cols, const VTermScreenCell *cells, void *user) + { + for (col = 0; col < len; col += cells[col].width) + { +- if (ga_grow(&ga, MB_MAXBYTES) == FAIL) ++ if (ga_grow(&ga, VTERM_MAX_CHARS_PER_CELL * 4) == FAIL) + { + ga.ga_len = 0; + break; + } +- for (i = 0; (c = cells[col].chars[i]) > 0 || i == 0; ++i) ++ for (i = 0; i < VTERM_MAX_CHARS_PER_CELL && ++ ((c = cells[col].chars[i]) > 0 || i == 0); ++i) + ga.ga_len += utf_char2bytes(c == NUL ? ' ' : c, + (char_u *)ga.ga_data + ga.ga_len); + cell2cellattr(&cells[col], &p[col]); +diff --git a/src/testdir/samples/terminal_max_combining_chars.txt b/src/testdir/samples/terminal_max_combining_chars.txt +new file mode 100644 +index 0000000..a4f508d +--- /dev/null ++++ b/src/testdir/samples/terminal_max_combining_chars.txt +@@ -0,0 +1,80 @@ ++padding line 000 ++padding line 001 ++padding line 002 ++padding line 003 ++padding line 004 ++padding line 005 ++padding line 006 ++padding line 007 ++padding line 008 ++padding line 009 ++padding line 010 ++padding line 011 ++padding line 012 ++padding line 013 ++padding line 014 ++padding line 015 ++padding line 016 ++padding line 017 ++padding line 018 ++padding line 019 ++padding line 020 ++padding line 021 ++padding line 022 ++padding line 023 ++padding line 024 ++padding line 025 ++padding line 026 ++padding line 027 ++padding line 028 ++padding line 029 ++padding line 030 ++padding line 031 ++padding line 032 ++padding line 033 ++padding line 034 ++padding line 035 ++padding line 036 ++padding line 037 ++padding line 038 ++padding line 039 ++padding line 040 ++padding line 041 ++padding line 042 ++padding line 043 ++padding line 044 ++padding line 045 ++padding line 046 ++padding line 047 ++padding line 048 ++padding line 049 ++AAAAAAAAAAAAAAAAAAAAAAAAAAAA𐀀󠄀󠄁󠄂󠄃󠄄 ++AAAAAAAAAAAAAAAAAAAAAAAAAAAA𐀀󠄀󠄁󠄂󠄃󠄄 ++AAAAAAAAAAAAAAAAAAAAAAAAAAAA𐀀󠄀󠄁󠄂󠄃󠄄 ++AAAAAAAAAAAAAAAAAAAAAAAAAAAA𐀀󠄀󠄁󠄂󠄃󠄄 ++AAAAAAAAAAAAAAAAAAAAAAAAAAAA𐀀󠄀󠄁󠄂󠄃󠄄 ++AAAAAAAAAAAAAAAAAAAAAAAAAAAA𐀀󠄀󠄁󠄂󠄃󠄄 ++AAAAAAAAAAAAAAAAAAAAAAAAAAAA𐀀󠄀󠄁󠄂󠄃󠄄 ++AAAAAAAAAAAAAAAAAAAAAAAAAAAA𐀀󠄀󠄁󠄂󠄃󠄄 ++AAAAAAAAAAAAAAAAAAAAAAAAAAAA𐀀󠄀󠄁󠄂󠄃󠄄 ++AAAAAAAAAAAAAAAAAAAAAAAAAAAA𐀀󠄀󠄁󠄂󠄃󠄄 ++AAAAAAAAAAAAAAAAAAAAAAAAAAAA𐀀󠄀󠄁󠄂󠄃󠄄 ++AAAAAAAAAAAAAAAAAAAAAAAAAAAA𐀀󠄀󠄁󠄂󠄃󠄄 ++AAAAAAAAAAAAAAAAAAAAAAAAAAAA𐀀󠄀󠄁󠄂󠄃󠄄 ++AAAAAAAAAAAAAAAAAAAAAAAAAAAA𐀀󠄀󠄁󠄂󠄃󠄄 ++AAAAAAAAAAAAAAAAAAAAAAAAAAAA𐀀󠄀󠄁󠄂󠄃󠄄 ++AAAAAAAAAAAAAAAAAAAAAAAAAAAA𐀀󠄀󠄁󠄂󠄃󠄄 ++AAAAAAAAAAAAAAAAAAAAAAAAAAAA𐀀󠄀󠄁󠄂󠄃󠄄 ++AAAAAAAAAAAAAAAAAAAAAAAAAAAA𐀀󠄀󠄁󠄂󠄃󠄄 ++AAAAAAAAAAAAAAAAAAAAAAAAAAAA𐀀󠄀󠄁󠄂󠄃󠄄 ++AAAAAAAAAAAAAAAAAAAAAAAAAAAA𐀀󠄀󠄁󠄂󠄃󠄄 ++AAAAAAAAAAAAAAAAAAAAAAAAAAAA𐀀󠄀󠄁󠄂󠄃󠄄 ++AAAAAAAAAAAAAAAAAAAAAAAAAAAA𐀀󠄀󠄁󠄂󠄃󠄄 ++AAAAAAAAAAAAAAAAAAAAAAAAAAAA𐀀󠄀󠄁󠄂󠄃󠄄 ++AAAAAAAAAAAAAAAAAAAAAAAAAAAA𐀀󠄀󠄁󠄂󠄃󠄄 ++AAAAAAAAAAAAAAAAAAAAAAAAAAAA𐀀󠄀󠄁󠄂󠄃󠄄 ++AAAAAAAAAAAAAAAAAAAAAAAAAAAA𐀀󠄀󠄁󠄂󠄃󠄄 ++AAAAAAAAAAAAAAAAAAAAAAAAAAAA𐀀󠄀󠄁󠄂󠄃󠄄 ++AAAAAAAAAAAAAAAAAAAAAAAAAAAA𐀀󠄀󠄁󠄂󠄃󠄄 ++AAAAAAAAAAAAAAAAAAAAAAAAAAAA𐀀󠄀󠄁󠄂󠄃󠄄 ++AAAAAAAAAAAAAAAAAAAAAAAAAAAA𐀀󠄀󠄁󠄂󠄃󠄄 +diff --git a/src/testdir/test_terminal3.vim b/src/testdir/test_terminal3.vim +index 0e5f70b..03130e6 100644 +--- a/src/testdir/test_terminal3.vim ++++ b/src/testdir/test_terminal3.vim +@@ -1013,4 +1013,18 @@ func Test_autocmd_buffilepost_with_hidden_term() + bw! XTestFile + endfunc + ++func Test_terminal_max_combining_chars() ++ " somehow doesn't work on MS-Windows ++ CheckUnix ++ let cmd = "cat samples/terminal_max_combining_chars.txt\" ++ let buf = Run_shell_in_terminal({'term_rows': 15, 'term_cols': 35}) ++ call TermWait(buf) ++ call term_sendkeys(buf, cmd) ++ " last char is a space with many combining chars ++ call WaitForAssert({-> assert_match("AAAAAAAAAAAAAAAAAAAAAAAAAAAA.", term_getline(buf, 14))}) ++ ++ call term_sendkeys(buf, "exit\r") ++ exe buf . "bwipe!" ++endfunc ++ + " vim: shiftwidth=2 sts=2 expandtab Binary files /srv/release.debian.org/tmp/9ZqykMqPms/vim-9.1.1230/debian/patches/CVE-2026-28421.patch and /srv/release.debian.org/tmp/T35rpTUl_N/vim-9.1.1230/debian/patches/CVE-2026-28421.patch differ diff -Nru vim-9.1.1230/debian/patches/CVE-2026-28422.patch vim-9.1.1230/debian/patches/CVE-2026-28422.patch --- vim-9.1.1230/debian/patches/CVE-2026-28422.patch 1970-01-01 00:00:00.000000000 +0000 +++ vim-9.1.1230/debian/patches/CVE-2026-28422.patch 2026-09-25 20:25:55.000000000 +0000 @@ -0,0 +1,47 @@ +From: Lee Garrett +Date: Fri, 25 Sep 2026 17:26:06 +0200 +Subject: Fix CVE-2026-28422 + +Origin: backport, https://github.com/vim/vim/commit/4e5b9e31cb7484ad156fba995fdce3c9b075b5fd +Bug: https://github.com/vim/vim/security/advisories/GHSA-gmqx-prf2-8mwf +Forwarded: not-needed +Reviewed-By: Lee Garrett +Last-Update: 2026-09-25 + +patch 9.2.0078: [security]: stack-buffer-overflow in build_stl_str_hl() +Problem: A stack-buffer-overflow occurs when rendering a statusline + with a multi-byte fill character on a very wide terminal. + The size check in build_stl_str_hl() uses the cell width + rather than the byte length, allowing the subsequent fill + loop to write beyond the 4096-byte MAXPATHL buffer + (ehdgks0627, un3xploitable). +Solution: Update the size check to account for the byte length of + the fill character (using MB_CHAR2LEN). + +Github Advisory: +https://github.com/vim/vim/security/advisories/GHSA-gmqx-prf2-8mwf + +This fixes CVE-2026-28422. + +Backport of upstream commit 4e5b9e31cb7484ad156fba995fdce3c9b075b5fd. +- src/buffer.c fix taken from upstream; the size-check condition uses + STRLEN(out) here in place of upstream's outputlen. +- src/version.c bump omitted as not relevant to this branch. +--- + src/buffer.c | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) + +diff --git a/src/buffer.c b/src/buffer.c +index 68388db..ffbb4e4 100644 +--- a/src/buffer.c ++++ b/src/buffer.c +@@ -5169,7 +5169,8 @@ build_stl_str_hl( + } + width = maxwidth; + } +- else if (width < maxwidth && STRLEN(out) + maxwidth - width + 1 < outlen) ++ else if (width < maxwidth && ++ STRLEN(out) + (maxwidth - width) * MB_CHAR2LEN(fillchar) + 1 < outlen) + { + // Find how many separators there are, which we will use when + // figuring out how many groups there are. diff -Nru vim-9.1.1230/debian/patches/CVE-2026-32249.patch vim-9.1.1230/debian/patches/CVE-2026-32249.patch --- vim-9.1.1230/debian/patches/CVE-2026-32249.patch 1970-01-01 00:00:00.000000000 +0000 +++ vim-9.1.1230/debian/patches/CVE-2026-32249.patch 2026-09-25 20:25:55.000000000 +0000 @@ -0,0 +1,116 @@ +From: Lee Garrett +Date: Fri, 25 Sep 2026 17:26:06 +0200 +Subject: Fix CVE-2026-32249 + +Origin: backport, https://github.com/vim/vim/commit/36d6e87542cf823d833e451e09a90ee429899cec +Bug: https://github.com/vim/vim/security/advisories/GHSA-9phh-423r-778r +Forwarded: not-needed +Reviewed-By: Lee Garrett +Last-Update: 2026-09-25 + +patch 9.2.0137: [security]: crash with composing char in collection range +Problem: Using a composing character as the end of a range inside a + collection may corrupt the NFA postfix stack + (Nathan Mills, after v9.1.0011) +Solution: When a character is used as the endpoint of a range, do not emit + its composing characters separately. Range handling only uses + the base codepoint. + +Github Advisory: +https://github.com/vim/vim/security/advisories/GHSA-9phh-423r-778r + +This fixes CVE-2026-32249. + +Backport of upstream commit 36d6e87542cf823d833e451e09a90ee429899cec. +- src/regexp_nfa.c: range_endpoint declared as int using TRUE/FALSE in + place of upstream's bool/true/false, which are not available in this + branch's build. +- src/version.c bump omitted as not relevant to this branch. +--- + src/regexp_nfa.c | 17 +++++++++++++++-- + src/testdir/test_regexp_utf8.vim | 18 ++++++++++++++++++ + 2 files changed, 33 insertions(+), 2 deletions(-) + +diff --git a/src/regexp_nfa.c b/src/regexp_nfa.c +index 557d0e1..80a0ded 100644 +--- a/src/regexp_nfa.c ++++ b/src/regexp_nfa.c +@@ -1765,6 +1765,7 @@ collection: + if (*endp == ']') + { + int plen; ++ int range_endpoint; + /* + * Try to reverse engineer character classes. For example, + * recognize that [0-9] stands for \d and [A-Za-z_] for \h, +@@ -1812,6 +1813,7 @@ collection: + while (regparse < endp) + { + int oldstartc = startc; ++ range_endpoint = FALSE; + + startc = -1; + got_coll_char = FALSE; +@@ -1971,6 +1973,7 @@ collection: + if (emit_range) + { + int endc = startc; ++ range_endpoint = TRUE; + + startc = oldstartc; + if (startc > endc) +@@ -2049,7 +2052,14 @@ collection: + } + } + +- if (enc_utf8 && (utf_ptr2len(regparse) != (plen = utfc_ptr2len(regparse)))) ++ // ++ // If this character was consumed as the end of a range, do not emit its ++ // composing characters separately. Range handling only uses the base ++ // codepoint; emitting the composing part again would duplicate the ++ // character in the postfix stream and corrupt the NFA stack. ++ // ++ if (!range_endpoint && enc_utf8 && ++ (utf_ptr2len(regparse) != (plen = utfc_ptr2len(regparse)))) + { + int i = utf_ptr2len(regparse); + +@@ -3183,7 +3193,10 @@ nfa_max_width(nfa_state_T *startstate, int depth) + ++len; + if (state->c != NFA_ANY) + { +- // skip over the characters ++ // Skip over the compiled collection. ++ // malformed NFAs must not crash width estimation. ++ if (state->out1 == NULL || state->out1->out == NULL) ++ return -1; + state = state->out1->out; + continue; + } +diff --git a/src/testdir/test_regexp_utf8.vim b/src/testdir/test_regexp_utf8.vim +index 51c0984..b1239c0 100644 +--- a/src/testdir/test_regexp_utf8.vim ++++ b/src/testdir/test_regexp_utf8.vim +@@ -619,4 +619,22 @@ func Test_search_multibyte_match_ascii() + bw! + endfunc + ++func Test_regex_collection_range_with_composing_crash() ++ " Regression test: composing char in collection range caused NFA crash/E874 ++ new ++ call setline(1, ['00', '0ֻ', '01']) ++ let patterns = [ '0[0-0ֻ]\@", 'E486:') ++ endfor ++ endfor ++ ++ bwipe! ++endfunc ++ + " vim: shiftwidth=2 sts=2 expandtab diff -Nru vim-9.1.1230/debian/patches/CVE-2026-33412.patch vim-9.1.1230/debian/patches/CVE-2026-33412.patch --- vim-9.1.1230/debian/patches/CVE-2026-33412.patch 1970-01-01 00:00:00.000000000 +0000 +++ vim-9.1.1230/debian/patches/CVE-2026-33412.patch 2026-09-25 20:25:55.000000000 +0000 @@ -0,0 +1,45 @@ +From: Lee Garrett +Date: Fri, 25 Sep 2026 17:26:06 +0200 +Subject: Fix CVE-2026-33412 + +Origin: backport, https://github.com/vim/vim/commit/645ed6597d1ea896c712cd7ddbb6edee79577e9a +Bug: https://github.com/vim/vim/security/advisories/GHSA-w5jw-f54h-x46c +Forwarded: not-needed +Reviewed-By: Lee Garrett +Last-Update: 2026-09-25 + +patch 9.2.0202: [security]: command injection via newline in glob() +Problem: The glob() function on Unix-like systems does not escape + newline characters when expanding wildcards. A maliciously + crafted string containing '\n' can be used as a command + separator to execute arbitrary shell commands via + mch_expand_wildcards(). This depends on the user's 'shell' + setting. +Solution: Add the newline character ('\n') to the SHELL_SPECIAL + definition to ensure it is properly escaped before being + passed to the shell (pyllyukko). + +Github Advisory: +https://github.com/vim/vim/security/advisories/GHSA-w5jw-f54h-x46c + +This fixes CVE-2026-33412. + +Backport of upstream commit 645ed6597d1ea896c712cd7ddbb6edee79577e9a. +- src/version.c bump omitted as not relevant to this branch. +--- + src/os_unix.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/src/os_unix.c b/src/os_unix.c +index 95b21d2..99f59f1 100644 +--- a/src/os_unix.c ++++ b/src/os_unix.c +@@ -6861,7 +6861,7 @@ mch_expandpath( + # define SEEK_END 2 + #endif + +-#define SHELL_SPECIAL (char_u *)"\t \"&'$;<>()\\|" ++#define SHELL_SPECIAL (char_u *)"\t \"&'$;<>()\\|\n" + + int + mch_expand_wildcards( diff -Nru vim-9.1.1230/debian/patches/CVE-2026-34982+regression-fix.patch vim-9.1.1230/debian/patches/CVE-2026-34982+regression-fix.patch --- vim-9.1.1230/debian/patches/CVE-2026-34982+regression-fix.patch 1970-01-01 00:00:00.000000000 +0000 +++ vim-9.1.1230/debian/patches/CVE-2026-34982+regression-fix.patch 2026-09-25 20:25:55.000000000 +0000 @@ -0,0 +1,112 @@ +From: Lee Garrett +Date: Fri, 25 Sep 2026 17:26:06 +0200 +Subject: Fix CVE-2026-34982 + fix regression + +Origin: backport, https://github.com/vim/vim/commit/75661a66a1db1e1f3f1245c615f13a7de44c0587 + https://github.com/vim/vim/commit/8c8772c6b321d4955c8f09926e3eda2b4cd83680 +Bug: https://github.com/vim/vim/security/advisories/GHSA-8h6p-m6gr-mpw9 +Forwarded: not-needed +Reviewed-By: Lee Garrett +Last-Update: 2026-09-25 + +patch 9.2.0276: [security]: modeline security bypass + +Problem: [security]: modeline security bypass +Solution: disallow mapset() from secure mode, set the P_MLE flag for the + 'complete', 'guitabtooltip' and 'printheader' options. + +Github Advisory: +https://github.com/vim/vim/security/advisories/GHSA-8h6p-m6gr-mpw9 + +This fixes CVE-2026-34982. + +Backport of upstream commit 75661a66a1db1e1f3f1245c615f13a7de44c0587. +- src/version.c bump omitted as not relevant to this branch. + +####################################################################### + +patch 9.2.0277: tests: test_modeline.vim fails + +Problem: tests: test_modeline.vim fails (after v9.2.0276) +Solution: Rewrite the tests to use the existing s:modeline_fails() + function, update documentation (zeertzjq). + +This fixes the test regression introduced by the CVE-2026-34982 backport +(patch 9.2.0276). + +Backport of upstream commit 8c8772c6b321d4955c8f09926e3eda2b4cd83680. +- runtime/doc/options.txt changes omitted: the 'complete' doc anchor + is not present on this branch and the notes are not needed for the fix. +- src/version.c bump omitted as not relevant to this branch. +--- + src/map.c | 3 +++ + src/optiondefs.h | 6 +++--- + src/testdir/test_modeline.vim | 3 +++ + 3 files changed, 9 insertions(+), 3 deletions(-) + +diff --git a/src/map.c b/src/map.c +index 209d9d2..04f6485 100644 +--- a/src/map.c ++++ b/src/map.c +@@ -2735,6 +2735,9 @@ f_mapset(typval_T *argvars, typval_T *rettv UNUSED) + int dict_only; + mapblock_T *mp_result[2] = {NULL, NULL}; + ++ if (check_secure()) ++ return; ++ + // If first arg is a dict, then that's the only arg permitted. + dict_only = argvars[0].v_type == VAR_DICT; + if (in_vim9script() +diff --git a/src/optiondefs.h b/src/optiondefs.h +index 38e871c..b01591e 100644 +--- a/src/optiondefs.h ++++ b/src/optiondefs.h +@@ -642,7 +642,7 @@ static struct vimoption options[] = + {"compatible", "cp", P_BOOL|P_RALL, + (char_u *)&p_cp, PV_NONE, did_set_compatible, NULL, + {(char_u *)TRUE, (char_u *)FALSE} SCTX_INIT}, +- {"complete", "cpt", P_STRING|P_ALLOCED|P_VI_DEF|P_ONECOMMA|P_NODUP, ++ {"complete", "cpt", P_STRING|P_ALLOCED|P_VI_DEF|P_ONECOMMA|P_NODUP|P_MLE, + (char_u *)&p_cpt, PV_CPT, did_set_complete, expand_set_complete, + {(char_u *)".,w,b,u,t,i", (char_u *)0L} + SCTX_INIT}, +@@ -1275,7 +1275,7 @@ static struct vimoption options[] = + {(char_u *)NULL, (char_u *)0L} + #endif + SCTX_INIT}, +- {"guitabtooltip", "gtt", P_STRING|P_VI_DEF|P_RWIN, ++ {"guitabtooltip", "gtt", P_STRING|P_VI_DEF|P_RWIN|P_MLE, + #if defined(FEAT_GUI_TABLINE) + (char_u *)&p_gtt, PV_NONE, NULL, NULL, + {(char_u *)"", (char_u *)0L} +@@ -1977,7 +1977,7 @@ static struct vimoption options[] = + {(char_u *)NULL, (char_u *)0L} + #endif + SCTX_INIT}, +- {"printheader", "pheader", P_STRING|P_VI_DEF|P_GETTEXT, ++ {"printheader", "pheader", P_STRING|P_VI_DEF|P_GETTEXT|P_MLE, + #ifdef FEAT_PRINTER + (char_u *)&p_header, PV_NONE, NULL, NULL, + // untranslated to avoid problems when 'encoding' +diff --git a/src/testdir/test_modeline.vim b/src/testdir/test_modeline.vim +index 0697fbb..e6baf3a 100644 +--- a/src/testdir/test_modeline.vim ++++ b/src/testdir/test_modeline.vim +@@ -263,13 +263,16 @@ endfunc + + func Test_modeline_fails_modelineexpr() + call s:modeline_fails('balloonexpr', 'balloonexpr=Something()', 'E992:') ++ call s:modeline_fails('complete', "complete=FSomething", 'E992:') + call s:modeline_fails('foldexpr', 'foldexpr=Something()', 'E992:') + call s:modeline_fails('foldtext', 'foldtext=Something()', 'E992:') + call s:modeline_fails('formatexpr', 'formatexpr=Something()', 'E992:') + call s:modeline_fails('guitablabel', 'guitablabel=Something()', 'E992:') ++ call s:modeline_fails('guitabtooltip', 'guitabtooltip=Something()', 'E992:') + call s:modeline_fails('iconstring', 'iconstring=Something()', 'E992:') + call s:modeline_fails('includeexpr', 'includeexpr=Something()', 'E992:') + call s:modeline_fails('indentexpr', 'indentexpr=Something()', 'E992:') ++ call s:modeline_fails('printheader', 'printheader=Something()', 'E992:') + call s:modeline_fails('rulerformat', 'rulerformat=Something()', 'E992:') + call s:modeline_fails('statusline', 'statusline=Something()', 'E992:') + call s:modeline_fails('tabline', 'tabline=Something()', 'E992:') Binary files /srv/release.debian.org/tmp/9ZqykMqPms/vim-9.1.1230/debian/patches/CVE-2026-35177.patch and /srv/release.debian.org/tmp/T35rpTUl_N/vim-9.1.1230/debian/patches/CVE-2026-35177.patch differ diff -Nru vim-9.1.1230/debian/patches/CVE-2026-39881.patch vim-9.1.1230/debian/patches/CVE-2026-39881.patch --- vim-9.1.1230/debian/patches/CVE-2026-39881.patch 1970-01-01 00:00:00.000000000 +0000 +++ vim-9.1.1230/debian/patches/CVE-2026-39881.patch 2026-09-25 20:25:55.000000000 +0000 @@ -0,0 +1,198 @@ +From: Lee Garrett +Date: Fri, 25 Sep 2026 17:26:06 +0200 +Subject: Fix CVE-2026-39881 + +Origin: backport, https://github.com/vim/vim/commit/7ab76a86048ed492374ac6b19c6cb52f89a365b4 +Bug: https://github.com/vim/vim/security/advisories/GHSA-mr87-rhgv-7pw6 +Forwarded: not-needed +Reviewed-By: Lee Garrett +Last-Update: 2026-09-25 + +patch 9.2.0316: [security]: command injection in netbeans interface via defineAnnoType +Problem: [security]: The netbeans defineAnnoType command passes typeName, fg and bg + unsanitized to coloncmd(), allowing a malicious server to inject + arbitrary Ex commands via '|'. Similarly, specialKeys does not + validate key tokens before building a map command. +Solution: Validate typeName, fg and bg against an allowlist of safe + characters before passing them to coloncmd() + +Github Advisory: +https://github.com/vim/vim/security/advisories/GHSA-mr87-rhgv-7pw6 + +This fixes CVE-2026-39881. + +Backport of upstream commit 7ab76a86048ed492374ac6b19c6cb52f89a365b4. +- src/version.c bump omitted as not relevant to this branch. +- runtime/doc/{netbeans.txt,tags} and src/po/vim.pot hunks omitted as not + tracked on this branch. +- special_keys() retains the existing strcpy() rather than the upstream + vim_strncpy() (that change belongs to the separate specialKeys overflow + fix, which is not present on this branch); only the injection guard is + added. +--- + src/errors.h | 3 ++- + src/netbeans.c | 50 ++++++++++++++++++++++++++++++++++++++++--- + src/testdir/test_netbeans.py | 4 ++-- + src/testdir/test_netbeans.vim | 39 +++++++++++++++++++++++++++++++++ + 4 files changed, 90 insertions(+), 6 deletions(-) + +diff --git a/src/errors.h b/src/errors.h +index ca5ec85..ae70b18 100644 +--- a/src/errors.h ++++ b/src/errors.h +@@ -1664,7 +1664,8 @@ EXTERN char e_invalid_buffer_identifier_in_setdot[] + INIT(= N_("E647: Invalid buffer identifier in setDot")); + EXTERN char e_invalid_buffer_identifier_in_close[] + INIT(= N_("E648: Invalid buffer identifier in close")); +-// E649 unused ++EXTERN char e_invalid_identifier_in_defineannotype[] ++ INIT(= N_("E649: Invalid identifier name in defineAnnoType")); + EXTERN char e_invalid_buffer_identifier_in_defineannotype[] + INIT(= N_("E650: Invalid buffer identifier in defineAnnoType")); + EXTERN char e_invalid_buffer_identifier_in_addanno[] +diff --git a/src/netbeans.c b/src/netbeans.c +index f27904f..12ab22a 100644 +--- a/src/netbeans.c ++++ b/src/netbeans.c +@@ -40,6 +40,11 @@ + #define GUARDEDOFFSET 1000000 // base for "guarded" sign id's + #define MAX_COLOR_LENGTH 32 // max length of color name in defineAnnoType + ++// Characters valid in a sign/highlight group name ++#define VALID_CHARS (char_u *)"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" ++#define VALID_SIGNNAME_CHARS VALID_CHARS "_" ++#define VALID_COLOR_CHARS VALID_CHARS "#" ++ + // The first implementation (working only with Netbeans) returned "1.1". The + // protocol implemented here also supports A-A-P. + static char *ExtEdProtocolVersion = "2.5"; +@@ -77,6 +82,22 @@ static int dosetvisible = FALSE; + static int needupdate = 0; + static int inAtomic = 0; + ++/* ++ * Return TRUE if "str" contains only characters from "allowed". ++ * Used to validate NetBeans-supplied strings before interpolating them ++ * into Ex commands via coloncmd(). ++ */ ++ static int ++nb_is_safe_string(char_u *str, char_u *allowed) ++{ ++ if (str == NULL) ++ return FALSE; ++ for (char_u *p = str; *p != NUL; p++) ++ if (vim_strchr(allowed, *p) == NULL) ++ return FALSE; ++ return TRUE; ++} ++ + /* + * Callback invoked when the channel is closed. + */ +@@ -1946,6 +1967,15 @@ nb_do_cmd( + VIM_CLEAR(typeName); + parse_error = TRUE; + } ++ else if (!nb_is_safe_string(typeName, VALID_SIGNNAME_CHARS) || ++ (*fg != NUL && !nb_is_safe_string(fg, VALID_COLOR_CHARS)) || ++ (*bg != NUL && !nb_is_safe_string(bg, VALID_COLOR_CHARS))) ++ { ++ nbdebug((" invalid chars in typeName/fg/bg in defineAnnoType\n")); ++ emsg(_(e_invalid_identifier_in_defineannotype)); ++ VIM_CLEAR(typeName); ++ parse_error = TRUE; ++ } + else if (typeName != NULL && tooltip != NULL && glyphFile != NULL) + addsigntype(buf, typeNum, typeName, tooltip, glyphFile, fg, bg); + +@@ -2318,10 +2348,24 @@ special_keys(char_u *args) + + if (strlen(tok) + i < KEYBUFLEN) + { +- strcpy(&keybuf[i], tok); +- vim_snprintf(cmdbuf, sizeof(cmdbuf), ++ // Only allow alphanumeric and function-key name characters. ++ // Reject anything else to prevent map command injection. ++ int safe = TRUE; ++ for (char_u *tp = (char_u *)tok; *tp != NUL; tp++) ++ { ++ if (!ASCII_ISALNUM(*tp) && *tp != '-') ++ { ++ safe = FALSE; ++ break; ++ } ++ } ++ if (safe) ++ { ++ strcpy(&keybuf[i], tok); ++ vim_snprintf(cmdbuf, sizeof(cmdbuf), + "<%s> :nbkey %s", keybuf, keybuf); +- do_map(MAPTYPE_MAP, (char_u *)cmdbuf, MODE_NORMAL, FALSE); ++ do_map(MAPTYPE_MAP, (char_u *)cmdbuf, MODE_NORMAL, FALSE); ++ } + } + tok = strtok(NULL, " "); + } +diff --git a/src/testdir/test_netbeans.py b/src/testdir/test_netbeans.py +index 585886f..fe430e2 100644 +--- a/src/testdir/test_netbeans.py ++++ b/src/testdir/test_netbeans.py +@@ -113,8 +113,8 @@ class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler): + 'endAtomic_Test' : '0:endAtomic!95\n', + 'AnnoScale_Test' : "".join(['2:defineAnnoType!60 ' + str(i) + ' "s' + str(i) + '" "x" "=>" blue none\n' for i in range(2, 26)]), + 'detach_Test' : '2:close!96\n1:close!97\nDETACH\n', +- 'specialKeys_overflow_Test' : '0:specialKeys!200 "' + 'A'*80 + '-X"\n' +- ++ 'specialKeys_overflow_Test' : '0:specialKeys!200 "' + 'A'*80 + '-X"\n', ++ 'defineAnnoType_injection_Test': '1:defineAnnoType!1 "MySign guifg=red|call writefile([\'inject\'],\'Xinject\')|" "tooltip" "glyphFile" 1 2\n' + } + # execute the specified test + if cmd not in testmap: +diff --git a/src/testdir/test_netbeans.vim b/src/testdir/test_netbeans.vim +index fad24d0..d3887a2 100644 +--- a/src/testdir/test_netbeans.vim ++++ b/src/testdir/test_netbeans.vim +@@ -1027,4 +1027,43 @@ func Test_nb_specialKeys_overflow() + call s:run_server('Nb_specialKeys_overflow') + endfunc + ++func Nb_defineAnnoType_injection(port) ++ call writefile([], "Xnetbeans", 'D') ++ let g:last = 0 ++ ++ exe 'nbstart :localhost:' .. a:port .. ':bunny' ++ call assert_true(has("netbeans_enabled")) ++ call WaitFor('len(ReadXnetbeans()) > (g:last + 2)') ++ let g:last += 3 ++ ++ split Xcmdbuf ++ let cmdbufnr = bufnr() ++ call WaitFor('len(ReadXnetbeans()) > (g:last + 2)') ++ let g:last += 3 ++ hide ++ ++ sleep 1m ++ ++ call delete('Xinject') ++ call appendbufline(cmdbufnr, '$', 'defineAnnoType_injection_Test') ++ " E475 from :sign is expected — catch it before RunServer sees it. ++ " give it a bit of time to process it ++ try ++ sleep 500m ++ catch /E475/ ++ catch /E649/ ++ endtry ++ ++ " Injected call must not have created this file ++ call assert_false(filereadable('Xinject')) ++ call delete('Xinject') ++ bwipe! Xcmdbuf ++ nbclose ++endfunc ++ ++func Test_nb_defineAnnoType_injection() ++ call ch_log('Test_nb_defineAnnoType_injection') ++ call s:run_server('Nb_defineAnnoType_injection') ++endfunc ++ + " vim: shiftwidth=2 sts=2 expandtab diff -Nru vim-9.1.1230/debian/patches/CVE-2026-41411.patch vim-9.1.1230/debian/patches/CVE-2026-41411.patch --- vim-9.1.1230/debian/patches/CVE-2026-41411.patch 1970-01-01 00:00:00.000000000 +0000 +++ vim-9.1.1230/debian/patches/CVE-2026-41411.patch 2026-09-25 20:25:55.000000000 +0000 @@ -0,0 +1,74 @@ +From: Lee Garrett +Date: Fri, 25 Sep 2026 17:26:06 +0200 +Subject: Fix CVE-2026-41411 + +Origin: backport, https://github.com/vim/vim/commit/c78194e41d5a0b05b0ddf383b6679b1503f977fb +Bug: https://github.com/vim/vim/security/advisories/GHSA-cwgx-gcj7-6qh8 +Forwarded: not-needed +Reviewed-By: Lee Garrett +Last-Update: 2026-09-25 + +patch 9.2.0357: [security]: command injection via backticks in tag files +Problem: [security]: command injection via backticks in tag files + (Srinivas Piskala Ganesh Babu, Andy Ngo) +Solution: Disallow backticks before attempting to expand filenames. + +Github Advisory: +https://github.com/vim/vim/security/advisories/GHSA-cwgx-gcj7-6qh8 + +This fixes CVE-2026-41411. + +Backport of upstream commit c78194e41d5a0b05b0ddf383b6679b1503f977fb. +- src/version.c bump omitted as not relevant to this branch. +--- + src/tag.c | 4 +++- + src/testdir/test_tagjump.vim | 22 ++++++++++++++++++++++ + 2 files changed, 25 insertions(+), 1 deletion(-) + +diff --git a/src/tag.c b/src/tag.c +index 85acbe8..a15583f 100644 +--- a/src/tag.c ++++ b/src/tag.c +@@ -4113,8 +4113,10 @@ expand_tag_fname(char_u *fname, char_u *tag_fname, int expand) + + /* + * Expand file name (for environment variables) when needed. ++ * Disallow backticks, they could execute arbitrary shell ++ * commands. This is not needed for tag filenames. + */ +- if (expand && mch_has_wildcard(fname)) ++ if (expand && mch_has_wildcard(fname) && vim_strchr(fname, '`') == NULL) + { + ExpandInit(&xpc); + xpc.xp_context = EXPAND_FILES; +diff --git a/src/testdir/test_tagjump.vim b/src/testdir/test_tagjump.vim +index e4b258c..17b015c 100644 +--- a/src/testdir/test_tagjump.vim ++++ b/src/testdir/test_tagjump.vim +@@ -1650,4 +1650,26 @@ func Test_tag_excmd_with_nostartofline() + set startofline& + endfunc + ++" Test that backtick expressions in tag filenames are not expanded. ++" This prevents command injection via malicious tags files. ++func Test_tag_backtick_filename_not_expanded() ++ let pwned_file = 'Xtags_pwnd' ++ call assert_false(filereadable(pwned_file)) ++ ++ let tagline = "main\t`touch " .. pwned_file .. "`\t/^int main/;\"\tf" ++ call writefile([tagline], 'Xbt_tags', 'D') ++ call writefile(['int main(int argc, char **argv) {', '}'], 'Xbt_main.c', 'D') ++ ++ set tags=Xbt_tags ++ sp Xbt_main.c ++ ++ " The :tag command should fail to find the file, but must NOT execute ++ " the backtick shell command. ++ call assert_fails('tag main', 'E429:') ++ call assert_false(filereadable(pwned_file)) ++ ++ set tags& ++ bwipe! ++endfunc ++ + " vim: shiftwidth=2 sts=2 expandtab diff -Nru vim-9.1.1230/debian/patches/CVE-2026-42307.patch vim-9.1.1230/debian/patches/CVE-2026-42307.patch --- vim-9.1.1230/debian/patches/CVE-2026-42307.patch 1970-01-01 00:00:00.000000000 +0000 +++ vim-9.1.1230/debian/patches/CVE-2026-42307.patch 2026-09-25 20:25:55.000000000 +0000 @@ -0,0 +1,219 @@ +From: Lee Garrett +Date: Fri, 25 Sep 2026 17:26:06 +0200 +Subject: Fix CVE-2026-42307 + +Origin: backport, https://github.com/vim/vim/commit/405e2fb6d54d5653523809e2853d99d1c000a5fc +Bug: https://github.com/vim/vim/security/advisories/GHSA-85ch-p2qr-m5gx +Forwarded: not-needed +Reviewed-By: Lee Garrett +Last-Update: 2026-09-25 + +patch 9.2.0383: [security]: runtime(netrw): shell-injection via sftp: and file: URLs +Problem: runtime(netrw): shell-injection via sftp: and file: URLs + (Joshua Rogers) +Solution: Escape temporary file names, harden filename suffix regex, + drop unused g:netrw_tmpfile_escape variable + +Github Advisory: +https://github.com/vim/vim/security/advisories/GHSA-85ch-p2qr-m5gx + +This fixes CVE-2026-42307. + +Backport of upstream commit 405e2fb6d54d5653523809e2853d99d1c000a5fc. +- src/version.c bump omitted as not relevant to this branch. +- netrw.vim header changelog lines omitted +- Additionally dropped the now-dangling g:netrw_tmpfile_escape + references in netrwSettings.vim (already removed upstream) to keep + :NetrwSettings working. +--- + runtime/doc/pi_netrw.txt | 4 --- + runtime/doc/tags | 1 - + runtime/pack/dist/opt/netrw/autoload/netrw.vim | 16 +++++---- + .../pack/dist/opt/netrw/autoload/netrwSettings.vim | 3 -- + runtime/pack/dist/opt/netrw/doc/netrw.txt | 4 --- + src/testdir/Make_all.mak | 2 ++ + src/testdir/test_plugin_netrw.vim | 39 ++++++++++++++++++++++ + 7 files changed, 50 insertions(+), 19 deletions(-) + create mode 100644 src/testdir/test_plugin_netrw.vim + +diff --git a/runtime/doc/pi_netrw.txt b/runtime/doc/pi_netrw.txt +index f6cc0dd..038c7c6 100644 +--- a/runtime/doc/pi_netrw.txt ++++ b/runtime/doc/pi_netrw.txt +@@ -2898,10 +2898,6 @@ your browsing preferences. (see also: |netrw-settings|) + such as listing, file removal, etc. + default: ssh + +- *g:netrw_tmpfile_escape* =' &;' +- escape() is applied to all temporary files +- to escape these characters. +- + *g:netrw_timefmt* specify format string to vim's strftime(). + The default, "%c", is "the preferred date + and time representation for the current +diff --git a/runtime/doc/tags b/runtime/doc/tags +index f752533..147fb36 100644 +--- a/runtime/doc/tags ++++ b/runtime/doc/tags +@@ -7729,7 +7729,6 @@ g:netrw_ssh_browse_reject pi_netrw.txt /*g:netrw_ssh_browse_reject* + g:netrw_ssh_cmd pi_netrw.txt /*g:netrw_ssh_cmd* + g:netrw_sshport pi_netrw.txt /*g:netrw_sshport* + g:netrw_timefmt pi_netrw.txt /*g:netrw_timefmt* +-g:netrw_tmpfile_escape pi_netrw.txt /*g:netrw_tmpfile_escape* + g:netrw_uid pi_netrw.txt /*g:netrw_uid* + g:netrw_use_errorwindow pi_netrw.txt /*g:netrw_use_errorwindow* + g:netrw_use_noswf pi_netrw.txt /*g:netrw_use_noswf* +diff --git a/runtime/pack/dist/opt/netrw/autoload/netrw.vim b/runtime/pack/dist/opt/netrw/autoload/netrw.vim +index 15e986e..49123f2 100644 +--- a/runtime/pack/dist/opt/netrw/autoload/netrw.vim ++++ b/runtime/pack/dist/opt/netrw/autoload/netrw.vim +@@ -552,7 +552,6 @@ else + call s:NetrwInit("g:netrw_glob_escape",'*[]?`{~$\') + endif + call s:NetrwInit("g:netrw_menu_escape",'.&? \') +-call s:NetrwInit("g:netrw_tmpfile_escape",' &;') + call s:NetrwInit("s:netrw_map_escape","<|\n\r\\\\"") + if has("gui_running") && (&enc == 'utf-8' || &enc == 'utf-16' || &enc == 'ucs-4') + let s:treedepthstring= "│ " +@@ -2196,7 +2195,7 @@ fun! netrw#NetRead(mode,...) + " NetRead: (sftp) NetRead Method #9 {{{3 + elseif b:netrw_method == 9 + " call Decho("read via sftp (method #9)",'~'.expand("")) +- call netrw#os#Execute(s:netrw_silentxfer."!".g:netrw_sftp_cmd." ".netrw#os#Escape(g:netrw_machine.":".b:netrw_fname,1)." ".tmpfile) ++ call netrw#os#Execute(s:netrw_silentxfer."!".g:netrw_sftp_cmd." ".netrw#os#Escape(g:netrw_machine.":".b:netrw_fname,1)." ".netrw#os#Escape(tmpfile,1)) + let result = s:NetrwGetFile(readcmd, tmpfile, b:netrw_method) + let b:netrw_lastfile = choice + +@@ -2204,7 +2203,7 @@ fun! netrw#NetRead(mode,...) + " NetRead: (file) NetRead Method #10 {{{3 + elseif b:netrw_method == 10 && exists("g:netrw_file_cmd") + " " call Decho("read via ".b:netrw_file_cmd." (method #10)",'~'.expand("")) +- call netrw#os#Execute(s:netrw_silentxfer."!".g:netrw_file_cmd." ".netrw#os#Escape(b:netrw_fname,1)." ".tmpfile) ++ call netrw#os#Execute(s:netrw_silentxfer."!".g:netrw_file_cmd." ".netrw#os#Escape(b:netrw_fname,1)." ".netrw#os#Escape(tmpfile,1)) + let result = s:NetrwGetFile(readcmd, tmpfile, b:netrw_method) + let b:netrw_lastfile = choice + +@@ -10555,15 +10554,18 @@ fun! s:GetTempfile(fname) + endif + + " use fname's suffix for the temporary file ++ " Restrict the suffix to word characters so shell metacharacters in a ++ " remote filename (e.g. sftp://host/foo.txt;id) cannot ride along into ++ " the tempfile name and out into a downstream shell command. + if a:fname != "" +- if a:fname =~ '\.[^./]\+$' ++ if a:fname =~ '\.\w\+$' + " call Decho("using fname<".a:fname.">'s suffix",'~'.expand("")) + if a:fname =~ '\.tar\.gz$' || a:fname =~ '\.tar\.bz2$' || a:fname =~ '\.tar\.xz$' +- let suffix = ".tar".substitute(a:fname,'^.*\(\.[^./]\+\)$','\1','e') ++ let suffix = ".tar".substitute(a:fname,'^.*\(\.\w\+\)$','\1','e') + elseif a:fname =~ '.txz$' +- let suffix = ".txz".substitute(a:fname,'^.*\(\.[^./]\+\)$','\1','e') ++ let suffix = ".txz".substitute(a:fname,'^.*\(\.\w\+\)$','\1','e') + else +- let suffix = substitute(a:fname,'^.*\(\.[^./]\+\)$','\1','e') ++ let suffix = substitute(a:fname,'^.*\(\.\w\+\)$','\1','e') + endif + " call Decho("suffix<".suffix.">",'~'.expand("")) + let tmpfile= substitute(tmpfile,'\.tmp$','','e') +diff --git a/runtime/pack/dist/opt/netrw/autoload/netrwSettings.vim b/runtime/pack/dist/opt/netrw/autoload/netrwSettings.vim +index c32051c..a74a275 100644 +--- a/runtime/pack/dist/opt/netrw/autoload/netrwSettings.vim ++++ b/runtime/pack/dist/opt/netrw/autoload/netrwSettings.vim +@@ -178,8 +178,6 @@ function! netrwSettings#NetrwSettings() + put = 'let g:netrw_sepchr = '.g:netrw_sepchr + put = 'let g:netrw_sshport = '.g:netrw_sshport + put = 'let g:netrw_timefmt = '.g:netrw_timefmt +- let tmpfileescline= line("$") +- put ='let g:netrw_tmpfile_escape...' + put = 'let g:netrw_use_noswf = '.g:netrw_use_noswf + put = 'let g:netrw_xstrlen = '.g:netrw_xstrlen + put = 'let g:netrw_winsize = '.g:netrw_winsize +@@ -197,7 +195,6 @@ function! netrwSettings#NetrwSettings() + call setline(decompressline, "let g:netrw_decompress = ".substitute(string(g:netrw_decompress),"^'\\(.*\\)'$",'\1','')) + call setline(fnameescline, "let g:netrw_fname_escape = '".escape(g:netrw_fname_escape,"'")."'") + call setline(globescline, "let g:netrw_glob_escape = '".escape(g:netrw_glob_escape,"'")."'") +- call setline(tmpfileescline, "let g:netrw_tmpfile_escape = '".escape(g:netrw_tmpfile_escape,"'")."'") + + set nomod + +diff --git a/runtime/pack/dist/opt/netrw/doc/netrw.txt b/runtime/pack/dist/opt/netrw/doc/netrw.txt +index f6cc0dd..038c7c6 100644 +--- a/runtime/pack/dist/opt/netrw/doc/netrw.txt ++++ b/runtime/pack/dist/opt/netrw/doc/netrw.txt +@@ -2898,10 +2898,6 @@ your browsing preferences. (see also: |netrw-settings|) + such as listing, file removal, etc. + default: ssh + +- *g:netrw_tmpfile_escape* =' &;' +- escape() is applied to all temporary files +- to escape these characters. +- + *g:netrw_timefmt* specify format string to vim's strftime(). + The default, "%c", is "the preferred date + and time representation for the current +diff --git a/src/testdir/Make_all.mak b/src/testdir/Make_all.mak +index 5ceacd7..0c22f02 100644 +--- a/src/testdir/Make_all.mak ++++ b/src/testdir/Make_all.mak +@@ -243,6 +243,7 @@ NEW_TESTS = \ + test_plugin_glvs \ + test_plugin_man \ + test_plugin_matchparen \ ++ test_plugin_netrw \ + test_plugin_tar \ + test_plugin_termdebug \ + test_plugin_tohtml \ +@@ -508,6 +509,7 @@ NEW_TESTS_RES = \ + test_plugin_glvs.res \ + test_plugin_man.res \ + test_plugin_matchparen.res \ ++ test_plugin_netrw.res \ + test_plugin_tar.res \ + test_plugin_termdebug.res \ + test_plugin_tohtml.res \ +diff --git a/src/testdir/test_plugin_netrw.vim b/src/testdir/test_plugin_netrw.vim +new file mode 100644 +index 0000000..1e0bf94 +--- /dev/null ++++ b/src/testdir/test_plugin_netrw.vim +@@ -0,0 +1,39 @@ ++" Tests for the netrw plugin. ++ ++source check.vim ++ ++packadd netrw ++" source the netrw autoload script so that its option variables are set ++runtime autoload/netrw.vim ++ ++" The remote filename after '.' was allowed to contain shell metacharacters ++" and rode unescaped into the tempfile name passed to sftp/file_cmd, giving a ++" shell injection on :e sftp://host/foo.txt;. ++func Test_netrw_tempfile_suffix_injection() ++ CheckUnix ++ CheckExecutable id ++ let save_sftp = g:netrw_sftp_cmd ++ let save_file = exists('g:netrw_file_cmd') ? g:netrw_file_cmd : v:null ++ let g:netrw_sftp_cmd = 'true' ++ let g:netrw_file_cmd = 'true' ++ let fname = 'Xrce_marker' ++ try ++ call delete(fname) ++ sil! call netrw#NetRead(2, 'sftp://localhost/foo.txt;id>'..fname) ++ call assert_false(filereadable(fname), 'Command injection via sftp:// tempfile suffix') ++ ++ call delete(fname) ++ sil! call netrw#NetRead(2, 'file://localhost/foo.txt;id>'..fname) ++ call assert_false(filereadable(fname), 'Command injection via file:// tempfile suffix') ++ finally ++ call delete(fname) ++ let g:netrw_sftp_cmd = save_sftp ++ if save_file is v:null ++ unlet! g:netrw_file_cmd ++ else ++ let g:netrw_file_cmd = save_file ++ endif ++ endtry ++endfunc ++ ++" vim:ts=8 sts=2 sw=2 et diff -Nru vim-9.1.1230/debian/patches/CVE-2026-43961.patch vim-9.1.1230/debian/patches/CVE-2026-43961.patch --- vim-9.1.1230/debian/patches/CVE-2026-43961.patch 1970-01-01 00:00:00.000000000 +0000 +++ vim-9.1.1230/debian/patches/CVE-2026-43961.patch 2026-09-25 20:25:55.000000000 +0000 @@ -0,0 +1,97 @@ +From: Lee Garrett +Date: Fri, 25 Sep 2026 17:26:06 +0200 +Subject: Fix CVE-2026-43961 + +Origin: backport, https://github.com/vim/vim/commit/8af0f098c3a42a28661d0295364e6e0fd7dbc92c +Bug: https://github.com/vim/vim/security/advisories/GHSA-66hr-7p6x-x5j3 +Forwarded: not-needed +Reviewed-By: Lee Garrett +Last-Update: 2026-09-25 + +patch 9.2.0480: [security]: runtime(netrw): code injection via mf command +Problem: [security]: runtime(netrw): code injection via mf command + (Christopher Lusk, Zdenek Dohnal) +Solution: Do not use string concatenation inside the filter() commands + (Zdenek Dohnal) + +Github Security Advisory: +https://github.com/vim/vim/security/advisories/GHSA-66hr-7p6x-x5j3 + +This fixes CVE-2026-43961. + +Backport of upstream commit 8af0f098c3a42a28661d0295364e6e0fd7dbc92c. +- src/version.c bump omitted as not relevant to this branch. +- netrw.vim header changelog line omitted. +- Additionally preload autoload/netrw/fs.vim in test_plugin_netrw.vim: + the test :cd's away before netrw#fs# autoloads, and the test suite's + relative 'runtimepath' then breaks autoloading. Upstream only passes + because earlier tests (not present here) load it first. +--- + runtime/pack/dist/opt/netrw/autoload/netrw.vim | 6 +++--- + src/testdir/test_plugin_netrw.vim | 17 +++++++++++++++++ + 2 files changed, 20 insertions(+), 3 deletions(-) + +diff --git a/runtime/pack/dist/opt/netrw/autoload/netrw.vim b/runtime/pack/dist/opt/netrw/autoload/netrw.vim +index 49123f2..0ad1b7e 100644 +--- a/runtime/pack/dist/opt/netrw/autoload/netrw.vim ++++ b/runtime/pack/dist/opt/netrw/autoload/netrw.vim +@@ -6091,7 +6091,7 @@ fun! s:NetrwMarkFile(islocal,fname) + else + " remove filename from buffer's markfilelist + " call Decho("remove filename<".a:fname."> from local markfilelist_".curbufnr."<".string(s:netrwmarkfilelist_{curbufnr}).">",'~'.expand("")) +- call filter(s:netrwmarkfilelist_{curbufnr},'v:val != a:fname') ++ call filter(s:netrwmarkfilelist_{curbufnr}, {_, v -> v !=# a:fname}) + if s:netrwmarkfilelist_{curbufnr} == [] + " local markfilelist is empty; remove it entirely + " call Decho("markfile list now empty",'~'.expand("")) +@@ -6142,7 +6142,7 @@ fun! s:NetrwMarkFile(islocal,fname) + " remove new filename from global markfilelist + " call Decho("remove new filename from global s:markfilelist",'~'.expand("")) + " call Decho("..filter(".string(s:netrwmarkfilelist).",'v:val != '.".dname.")",'~'.expand("")) +- call filter(s:netrwmarkfilelist,'v:val != "'.dname.'"') ++ call filter(s:netrwmarkfilelist, {_, v -> v !=# dname}) + " call Decho("..ending s:netrwmarkfilelist <".string(s:netrwmarkfilelist).">",'~'.expand("")) + if s:netrwmarkfilelist == [] + " call Decho("s:netrwmarkfilelist is empty; unlet it",'~'.expand("")) +@@ -8522,7 +8522,7 @@ fun! s:NetrwTreeDisplay(dir,depth) + " hide given patterns + let listhide= split(g:netrw_list_hide,',') + for pat in listhide +- call filter(w:netrw_treedict[dir],'v:val !~ "'.escape(pat,'\\').'"') ++ call filter(w:netrw_treedict[dir], {_, v -> v !~# pat}) + endfor + + elseif g:netrw_hide == 2 +diff --git a/src/testdir/test_plugin_netrw.vim b/src/testdir/test_plugin_netrw.vim +index 1e0bf94..901d4c4 100644 +--- a/src/testdir/test_plugin_netrw.vim ++++ b/src/testdir/test_plugin_netrw.vim +@@ -5,6 +5,8 @@ source check.vim + packadd netrw + " source the netrw autoload script so that its option variables are set + runtime autoload/netrw.vim ++" preload netrw#fs# functions; 'runtimepath' is relative and tests may :cd ++runtime autoload/netrw/fs.vim + + " The remote filename after '.' was allowed to contain shell metacharacters + " and rode unescaped into the tempfile name passed to sftp/file_cmd, giving a +@@ -36,4 +38,19 @@ func Test_netrw_tempfile_suffix_injection() + endtry + endfunc + ++func Test_netrw_mf_command_injection() ++ CheckUnix ++ CheckExecutable touch ++ let path = tempname() ++ let fname = 'x" . execute("silent! !touch poc") . "' ++ call mkdir(path, 'R') ++ exe "cd " path ++ call writefile([], fname) ++ Explore . ++ call search('^x') ++ :norm mf ++ :norm mf ++ call assert_false(filereadable('poc'), 'Command injection via mf command') ++endfunc ++ + " vim:ts=8 sts=2 sw=2 et diff -Nru vim-9.1.1230/debian/patches/CVE-2026-44656.patch vim-9.1.1230/debian/patches/CVE-2026-44656.patch --- vim-9.1.1230/debian/patches/CVE-2026-44656.patch 1970-01-01 00:00:00.000000000 +0000 +++ vim-9.1.1230/debian/patches/CVE-2026-44656.patch 2026-09-25 20:25:55.000000000 +0000 @@ -0,0 +1,135 @@ +From: Lee Garrett +Date: Fri, 25 Sep 2026 17:26:07 +0200 +Subject: Fix CVE-2026-44656 + +Origin: backport, https://github.com/vim/vim/commit/190cb3c2b9c769a3972bcfd991a7b5b6cb771ef0 +Bug: https://github.com/vim/vim/security/advisories/GHSA-hwg5-3cxw-wvvg +Forwarded: not-needed +Reviewed-By: Lee Garrett +Last-Update: 2026-09-25 + +patch 9.2.0435: [security]: backticks in 'path' may cause shell execution on completion +Problem: [security]: Backticks enclosed shell commands in the 'path' + option value are executed during completion (q1uf3ng). +Solution: Skip path entries containing backticks, add P_SECURE to 'path' + option, so that it cannot be set from a modeline (for symmetry with + the 'cdpath' option) + +Github Advisory: +https://github.com/vim/vim/security/advisories/GHSA-hwg5-3cxw-wvvg + +This fixes CVE-2026-44656. + +Backport of upstream commit 190cb3c2b9c769a3972bcfd991a7b5b6cb771ef0. +- src/version.c bump omitted as not relevant to this branch. +- test_find_complete.vim: added "source check.vim"; upstream's runtest.vim + sources check.vim globally, this branch's does not. +- Test_path_modeline: dropped the 'modelinestrict' references, that option + does not exist on this branch. +--- + runtime/doc/options.txt | 3 +++ + src/findfile.c | 4 ++++ + src/optiondefs.h | 2 +- + src/testdir/test_find_complete.vim | 19 +++++++++++++++++++ + src/testdir/test_modeline.vim | 14 ++++++++++++++ + 5 files changed, 41 insertions(+), 1 deletion(-) + +diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt +index 6d790bf..0152410 100644 +--- a/runtime/doc/options.txt ++++ b/runtime/doc/options.txt +@@ -6325,6 +6325,9 @@ A jump table for the options with a short description can be found at |Q_op|. + < Replace the ';' with a ':' or whatever separator is used. Note that + this doesn't work when $INCL contains a comma or white space. + ++ This option cannot be set from a |modeline| or in the |sandbox|, for ++ security reasons. ++ + *'perldll'* + 'perldll' string (default depends on the build) + global +diff --git a/src/findfile.c b/src/findfile.c +index 2bd1e7e..3e5094a 100644 +--- a/src/findfile.c ++++ b/src/findfile.c +@@ -2424,6 +2424,10 @@ expand_path_option( + { + buflen = copy_option_part(&path_option, buf, MAXPATHL, " ,"); + ++ // do not expand backticks, could have been set via a modeline ++ if (vim_strchr(buf, '`') != NULL) ++ continue; ++ + if (buf[0] == '.' && (buf[1] == NUL || vim_ispathsep(buf[1]))) + { + size_t plen; +diff --git a/src/optiondefs.h b/src/optiondefs.h +index b01591e..3194104 100644 +--- a/src/optiondefs.h ++++ b/src/optiondefs.h +@@ -1890,7 +1890,7 @@ static struct vimoption options[] = + (char_u *)&p_pm, PV_NONE, + did_set_backupext_or_patchmode, NULL, + {(char_u *)"", (char_u *)0L} SCTX_INIT}, +- {"path", "pa", P_STRING|P_EXPAND|P_VI_DEF|P_COMMA|P_NODUP, ++ {"path", "pa", P_STRING|P_EXPAND|P_VI_DEF|P_SECURE|P_COMMA|P_NODUP, + (char_u *)&p_path, PV_PATH, NULL, NULL, + { + #if defined(AMIGA) || defined(MSWIN) +diff --git a/src/testdir/test_find_complete.vim b/src/testdir/test_find_complete.vim +index 079fb78..38da49c 100644 +--- a/src/testdir/test_find_complete.vim ++++ b/src/testdir/test_find_complete.vim +@@ -1,5 +1,7 @@ + " Tests for the 'find' command completion. + ++source check.vim ++ + " Do all the tests in a separate window to avoid E211 when we recursively + " delete the Xfind directory during cleanup + func Test_find_complete() +@@ -161,4 +163,21 @@ func Test_find_complete() + set path& + endfunc + ++" Verify that backticks in 'path' are not executed ++func Test_find_completion_backtick_in_path() ++ CheckUnix ++ CheckExecutable id ++ ++ new Xpoc.c ++ setl path+=`id>Xrce_marker` ++ " Triggering completion must not execute the backtick command. ++ call getcompletion('', 'file_in_path') ++ call assert_false(filereadable('Xrce_marker')) ++ call feedkeys(":find \t\n", "xt") ++ call assert_false(filereadable('Xrce_marker')) ++ ++ bwipe! ++ call delete('Xrce_marker') ++endfunc ++ + " vim: shiftwidth=2 sts=2 expandtab +diff --git a/src/testdir/test_modeline.vim b/src/testdir/test_modeline.vim +index e6baf3a..e4ae0e4 100644 +--- a/src/testdir/test_modeline.vim ++++ b/src/testdir/test_modeline.vim +@@ -366,4 +366,18 @@ func Test_modeline_disable() + call assert_equal(2, &sw) + endfunc + ++" Verify that backticks in 'path' set from a modeline are not executed ++func Test_path_modeline() ++ let lines =<< trim END ++ // vim: set path+=foobar : ++ END ++ call writefile(lines, 'Xpoc.c', 'D') ++ ++ set modeline ++ call assert_fails('split Xpoc.c', 'E520:') ++ ++ bwipe! ++ set modeline& ++endfunc ++ + " vim: shiftwidth=2 sts=2 expandtab diff -Nru vim-9.1.1230/debian/patches/CVE-2026-45130.patch vim-9.1.1230/debian/patches/CVE-2026-45130.patch --- vim-9.1.1230/debian/patches/CVE-2026-45130.patch 1970-01-01 00:00:00.000000000 +0000 +++ vim-9.1.1230/debian/patches/CVE-2026-45130.patch 2026-09-25 20:25:55.000000000 +0000 @@ -0,0 +1,115 @@ +From: Lee Garrett +Date: Fri, 25 Sep 2026 17:26:07 +0200 +Subject: Fix CVE-2026-45130 + +Origin: backport, https://github.com/vim/vim/commit/92993329178cb1f72d700fff45ca86e1c2d369f8 +Bug: https://github.com/vim/vim/security/advisories/GHSA-q4jv-r9gj-6cwv +Forwarded: not-needed +Reviewed-By: Lee Garrett +Last-Update: 2026-09-25 + +patch 9.2.0450: [security]: heap buffer overflow in spellfile.c read_compound() +Problem: read_compound() in spellfile.c computes the size of the regex + pattern buffer using signed-int arithmetic on the attacker + controlled SN_COMPOUND sectionlen. With sectionlen=0x40000008 + and UTF-8 encoding active the multiplication wraps to 27 while + the per-byte loop writes up to ~1B bytes, overflowing the heap. + Reachable when loading a crafted .spl file (e.g. via 'set spell' + after a modeline sets 'spelllang'). The cp/ap/crp allocations + have the same int + 1 overflow class (Daniel Cervera) +Solution: Use type size_t as buffer size and reject values larger than + COMPOUND_MAX_LEN (100000). Apply the same size_t treatment to + the cp/ap/crp allocations. + +Github Advisory: +https://github.com/vim/vim/security/advisories/GHSA-q4jv-r9gj-6cwv + +This fixes CVE-2026-45130. + +Backport of upstream commit 92993329178cb1f72d700fff45ca86e1c2d369f8. +- src/version.c bump omitted as not relevant to this branch. +--- + src/spellfile.c | 20 ++++++++++++++------ + src/testdir/test_spellfile.vim | 4 ++++ + 2 files changed, 18 insertions(+), 6 deletions(-) + +diff --git a/src/spellfile.c b/src/spellfile.c +index 0b9536d..768e9fd 100644 +--- a/src/spellfile.c ++++ b/src/spellfile.c +@@ -296,6 +296,9 @@ + #define CF_WORD 0x01 + #define CF_UPPER 0x02 + ++// Max allowed length for COMPOUND section ++#define COMPOUND_MAX_LEN 100000 ++ + /* + * Loop through all the siblings of a node (including the node) + */ +@@ -1225,6 +1228,8 @@ read_compound(FILE *fd, slang_T *slang, int len) + char_u *crp; + int cnt; + garray_T *gap; ++ size_t patsize; ++ size_t flagsize; + + if (todo < 2) + return SP_FORMERROR; // need at least two bytes +@@ -1281,16 +1286,19 @@ read_compound(FILE *fd, slang_T *slang, int len) + // "a[bc]/a*b+" -> "^\(a[bc]\|a*b\+\)$". + // Inserting backslashes may double the length, "^\(\)$" is 7 bytes. + // Conversion to utf-8 may double the size. +- c = todo * 2 + 7; ++ if ((size_t)todo > COMPOUND_MAX_LEN) ++ return SP_FORMERROR; ++ patsize = (size_t)todo * 2 + 7; + if (enc_utf8) +- c += todo * 2; +- pat = alloc(c); ++ patsize += (size_t)todo * 2; ++ flagsize = (size_t)todo + 1; ++ pat = alloc(patsize); + if (pat == NULL) + return SP_OTHERERROR; + + // We also need a list of all flags that can appear at the start and one + // for all flags. +- cp = alloc(todo + 1); ++ cp = alloc(flagsize); + if (cp == NULL) + { + vim_free(pat); +@@ -1299,7 +1307,7 @@ read_compound(FILE *fd, slang_T *slang, int len) + slang->sl_compstartflags = cp; + *cp = NUL; + +- ap = alloc(todo + 1); ++ ap = alloc(flagsize); + if (ap == NULL) + { + vim_free(pat); +@@ -1311,7 +1319,7 @@ read_compound(FILE *fd, slang_T *slang, int len) + // And a list of all patterns in their original form, for checking whether + // compounding may work in match_compoundrule(). This is freed when we + // encounter a wildcard, the check doesn't work then. +- crp = alloc(todo + 1); ++ crp = alloc(flagsize); + slang->sl_comprules = crp; + + pp = pat; +diff --git a/src/testdir/test_spellfile.vim b/src/testdir/test_spellfile.vim +index dd12698..b3f6e98 100644 +--- a/src/testdir/test_spellfile.vim ++++ b/src/testdir/test_spellfile.vim +@@ -337,6 +337,10 @@ func Test_spellfile_format_error() + " SN_COMPOUND: incorrect comppatlen + call Spellfile_Test(0z080000000007040101000000020165, 'E758:') + ++ " SN_COMPOUND: oversized sectionlen ++ let v = eval('0z08004000000803010161' .. repeat('61', 50) .. 'FF') ++ call Spellfile_Test(v, 'E759:') ++ + " SN_INFO: missing info + call Spellfile_Test(0z0F0000000005040101, '') + diff -Nru vim-9.1.1230/debian/patches/CVE-2026-46483.patch vim-9.1.1230/debian/patches/CVE-2026-46483.patch --- vim-9.1.1230/debian/patches/CVE-2026-46483.patch 1970-01-01 00:00:00.000000000 +0000 +++ vim-9.1.1230/debian/patches/CVE-2026-46483.patch 2026-09-25 20:25:55.000000000 +0000 @@ -0,0 +1,82 @@ +From: Lee Garrett +Date: Fri, 25 Sep 2026 17:26:07 +0200 +Subject: Fix CVE-2026-46483 + +Origin: backport, https://github.com/vim/vim/commit/3fb5e58fbc63d86a3e65f1a141b0d67af2aa38a1 +Bug: https://github.com/vim/vim/security/advisories/GHSA-2fpv-9ff7-xg5w +Forwarded: not-needed +Reviewed-By: Lee Garrett +Last-Update: 2026-09-25 + +patch 9.2.0479: [security]: runtime(tar): command injection in tar plugin +Problem: [security]: runtime(tar): command injection in tar plugin + (Christopher Lusk) +Solution: Use the correct shellescape(args, 1) form for a :! command + +Github Advisory: +https://github.com/vim/vim/security/advisories/GHSA-2fpv-9ff7-xg5w + +This fixes CVE-2026-46483. + +Backport of upstream commit 3fb5e58fbc63d86a3e65f1a141b0d67af2aa38a1. +- src/version.c bump omitted as not relevant to this branch. +- tar.vim: kept only the new changelog entry; the intermediate upstream + changelog entries are from patches not present on this branch. +- test_plugin_tar.vim: added only Test_extract_command_injection; the + surrounding upstream tests belong to patches not backported here. +--- + runtime/autoload/tar.vim | 5 +++-- + src/testdir/test_plugin_tar.vim | 19 +++++++++++++++++++ + 2 files changed, 22 insertions(+), 2 deletions(-) + +diff --git a/runtime/autoload/tar.vim b/runtime/autoload/tar.vim +index 1a0d4f8..d694853 100644 +--- a/runtime/autoload/tar.vim ++++ b/runtime/autoload/tar.vim +@@ -17,6 +17,7 @@ + " 2025 Apr 16 by Vim Project: decouple from netrw by adding s:WinPath() + " 2025 May 19 by Vim Project: restore working directory after read/write + " 2025 Jul 13 by Vim Project: warn with path traversal attacks ++" 2026 May 14 by Vim Project: use correct shellescape() call in Vimuntar() + " + " Contains many ideas from Michael Toren's + " +@@ -799,9 +800,9 @@ fun! tar#Vimuntar(...) + " if necessary, decompress the tarball; then, extract it + if tartail =~ '\.tgz' + if executable("gunzip") +- silent exe "!gunzip ".shellescape(tartail) ++ silent exe "!gunzip ".shellescape(tartail, 1) + elseif executable("gzip") +- silent exe "!gzip -d ".shellescape(tartail) ++ silent exe "!gzip -d ".shellescape(tartail, 1) + else + echoerr "unable to decompress<".tartail."> on this system" + if simplify(curdir) != simplify(tarhome) +diff --git a/src/testdir/test_plugin_tar.vim b/src/testdir/test_plugin_tar.vim +index ea84ffe..e6a9d0c 100644 +--- a/src/testdir/test_plugin_tar.vim ++++ b/src/testdir/test_plugin_tar.vim +@@ -128,3 +128,22 @@ def g:Test_tar_evil() + + bw! + enddef ++ ++def g:Test_extract_command_injection() ++ CheckExecutable gunzip ++ CheckExecutable touch ++ var tgz = eval('0z1F8B08087795056A000364756D6D792E74617200EDCE2B12C2300004D01C254' .. ++ '7480269CE534080A8495BD1DBF3996106C3A08A7ACFACD8157B59A7690BFB4A0FC3707C666E357D' .. ++ 'E65BC8B5A47CC8A5D61A522EA5B510D3CEBF5ED679197B8CE17CEDB7F9D4C76FBB5F3D000000000' .. ++ '000000000FCD11D32415E2C00280000') ++ var dirname = tempname() ++ ++ mkdir(dirname, 'R') ++ var tar = dirname .. "/';%$(touch pwned)'.tgz" ++ writefile(tgz, tar) ++ new ++ exe "e " .. fnameescape(tar) ++ exe ":Vimuntar " .. dirname ++ assert_false(filereadable(dirname .. "/pwned")) ++ bw! ++enddef diff -Nru vim-9.1.1230/debian/patches/CVE-2026-47162.patch vim-9.1.1230/debian/patches/CVE-2026-47162.patch --- vim-9.1.1230/debian/patches/CVE-2026-47162.patch 1970-01-01 00:00:00.000000000 +0000 +++ vim-9.1.1230/debian/patches/CVE-2026-47162.patch 2026-09-25 20:25:55.000000000 +0000 @@ -0,0 +1,71 @@ +From: Lee Garrett +Date: Fri, 25 Sep 2026 17:26:07 +0200 +Subject: Fix CVE-2026-47162 + +Origin: backport, https://github.com/vim/vim/commit/f08ab2f4d7d2947c8dd6c179ae08ee6146a2694b +Bug: https://github.com/vim/vim/security/advisories/GHSA-crm5-rh6j-2c7c +Forwarded: not-needed +Reviewed-By: Lee Garrett +Last-Update: 2026-09-25 + +patch 9.2.0495: [security]: runtime(netrw): code injection via NetrwBookHistSave() +Problem: [security]: runtime(netrw): code injection via + NetrwBookHistSave() +Solution: Properly quote the directory name using string() function + (Srinivas Piskala Ganesh Babu) + +Github Security Advisory: +https://github.com/vim/vim/security/advisories/GHSA-crm5-rh6j-2c7c + +This fixes CVE-2026-47162. + +Backport of upstream commit f08ab2f4d7d2947c8dd6c179ae08ee6146a2694b. +- src/version.c bump omitted as not relevant to this branch. +- netrw.vim header changelog line omitted. +--- + runtime/pack/dist/opt/netrw/autoload/netrw.vim | 2 +- + src/testdir/test_plugin_netrw.vim | 20 ++++++++++++++++++++ + 2 files changed, 21 insertions(+), 1 deletion(-) + +diff --git a/runtime/pack/dist/opt/netrw/autoload/netrw.vim b/runtime/pack/dist/opt/netrw/autoload/netrw.vim +index 0ad1b7e..1cfa0af 100644 +--- a/runtime/pack/dist/opt/netrw/autoload/netrw.vim ++++ b/runtime/pack/dist/opt/netrw/autoload/netrw.vim +@@ -3526,7 +3526,7 @@ fun! s:NetrwBookHistSave() + while ( first || cnt != g:netrw_dirhistcnt ) + let lastline= lastline + 1 + if exists("g:netrw_dirhist_{cnt}") +- call setline(lastline,'let g:netrw_dirhist_'.cnt."='".g:netrw_dirhist_{cnt}."'") ++ call setline(lastline,'let g:netrw_dirhist_'.cnt.'='.string(g:netrw_dirhist_{cnt})) + " call Decho("..".lastline.'let g:netrw_dirhist_'.cnt."='".g:netrw_dirhist_{cnt}."'",'~'.expand("")) + endif + let first = 0 +diff --git a/src/testdir/test_plugin_netrw.vim b/src/testdir/test_plugin_netrw.vim +index 901d4c4..ecfc35f 100644 +--- a/src/testdir/test_plugin_netrw.vim ++++ b/src/testdir/test_plugin_netrw.vim +@@ -53,4 +53,24 @@ func Test_netrw_mf_command_injection() + call assert_false(filereadable('poc'), 'Command injection via mf command') + endfunc + ++func Test_netrw_injection() ++ let g:netrw_home = getcwd() ++ let savefile = g:netrw_home . '/.netrwhist' ++ let g:netrw_dirhistmax = 10 ++ let g:netrw_dirhistcnt = 1 ++ let g:netrw_dirhist_1 = "x'|let g:injected = 1|let y='z" ++ call delete(savefile) ++ try ++ call netrw#Call('NetrwBookHistSave') ++ call assert_true(filereadable(savefile), savefile . ' must be written') ++ unlet g:netrw_dirhist_1 ++ execute 'source ' . fnameescape(savefile) ++ call assert_false(exists("g:injected"), 'injected statement must not execute') ++ call assert_equal("x'|let g:injected = 1|let y='z", g:netrw_dirhist_1, 'dirname must round-trip') ++ finally ++ call delete(savefile) ++ unlet! g:netrw_home g:netrw_dirhistmax g:netrw_dirhistcnt g:netrw_dirhist_1 g:injected ++ endtry ++endfunc ++ + " vim:ts=8 sts=2 sw=2 et diff -Nru vim-9.1.1230/debian/patches/CVE-2026-47167.patch vim-9.1.1230/debian/patches/CVE-2026-47167.patch --- vim-9.1.1230/debian/patches/CVE-2026-47167.patch 1970-01-01 00:00:00.000000000 +0000 +++ vim-9.1.1230/debian/patches/CVE-2026-47167.patch 2026-09-25 20:25:55.000000000 +0000 @@ -0,0 +1,99 @@ +From: Lee Garrett +Date: Fri, 25 Sep 2026 17:26:07 +0200 +Subject: Fix CVE-2026-47167 + +Origin: backport, https://github.com/vim/vim/commit/a65a52d684bc58535ad28a4ae824d22e76399934 +Bug: https://github.com/vim/vim/security/advisories/GHSA-4473-94jm-w5x9 +Forwarded: not-needed +Reviewed-By: Lee Garrett +Last-Update: 2026-09-25 + +patch 9.2.0496: [security]: Code Injection in cucumber filetype plugin +Problem: [security]: Code Injection in cucumber filetype plugin + (Christopher Lusk) +Solution: Use rubys Regexp.new() with the untrusted pattern + +Github Security Advisory: +https://github.com/vim/vim/security/advisories/GHSA-4473-94jm-w5x9 + +This fixes CVE-2026-47167. + +Backport of upstream commit a65a52d684bc58535ad28a4ae824d22e76399934. +- src/version.c bump omitted as not relevant to this branch. +- test_filetype.vim: added "source check.vim" so the CheckFeature command + used by the new test is available on this branch. +--- + runtime/ftplugin/cucumber.vim | 5 ++++- + src/testdir/test_filetype.vim | 32 ++++++++++++++++++++++++++++++++ + 2 files changed, 36 insertions(+), 1 deletion(-) + +diff --git a/runtime/ftplugin/cucumber.vim b/runtime/ftplugin/cucumber.vim +index f4848d1..9723898 100644 +--- a/runtime/ftplugin/cucumber.vim ++++ b/runtime/ftplugin/cucumber.vim +@@ -2,6 +2,8 @@ + " Language: Cucumber + " Maintainer: Tim Pope + " Last Change: 2016 Aug 29 ++" 2026 May 26 by Vim Project: prevent Code Injection ++" https://github.com/vim/vim/security/advisories/GHSA-4473-94jm-w5x9 + + " Only do this when not done yet for this buffer + if (exists("b:did_ftplugin")) +@@ -96,7 +98,8 @@ function! s:stepmatch(receiver,target) + catch + endtry + if has("ruby") && pattern !~ '\\\@ s:steps -> s:stepmatch on every discovered step, ++ " including the malicious one. Suppress preview and error messages. ++ silent! normal [d ++ call assert_false(filereadable(marker), 'Ruby injection executed') ++ bwipe! ++ filetype plugin off ++endfunc ++ + " vim: shiftwidth=2 sts=2 expandtab diff -Nru vim-9.1.1230/debian/patches/CVE-2026-52858+regression-fix.patch vim-9.1.1230/debian/patches/CVE-2026-52858+regression-fix.patch --- vim-9.1.1230/debian/patches/CVE-2026-52858+regression-fix.patch 1970-01-01 00:00:00.000000000 +0000 +++ vim-9.1.1230/debian/patches/CVE-2026-52858+regression-fix.patch 2026-09-25 20:25:55.000000000 +0000 @@ -0,0 +1,236 @@ +From: Lee Garrett +Date: Fri, 25 Sep 2026 17:26:07 +0200 +Subject: Fix CVE-2026-52858 + fix regression + +Origin: backport, https://github.com/vim/vim/commit/4b850457e12e1a678dd209f2868154f7553cbf8d + https://github.com/vim/vim/commit/868ad62cb8bf8038322eab2badd31bd98b02b9df +Bug: https://github.com/vim/vim/security/advisories/GHSA-52mc-rq6p-rc7c +Forwarded: not-needed +Reviewed-By: Lee Garrett +Last-Update: 2026-09-25 + +patch 9.2.0561: [security]: possible code execution with python3complete + +Problem: [security]: possible code execution with python3complete +Solution: Disable execution of import/from statements + +Github Security Advisory: +https://github.com/vim/vim/security/advisories/GHSA-52mc-rq6p-rc7c + +This fixes CVE-2026-52858. + +Backport of upstream commit 4b850457e12e1a678dd209f2868154f7553cbf8d. +- runtime/autoload/python3complete.vim, runtime/autoload/pythoncomplete.vim, + runtime/autoload/README.txt and runtime/doc/filetype.txt fixes taken + verbatim from upstream. +- src/version.c bump omitted as not relevant to the runtime fix. + +####################################################################### + +patch 9.2.0568: pythoncomplete: g:pythoncomplete_allow_import had no effect + +Problem: The security patch 9.2.0561 added a vim.eval() call inside + Completer.evalsource() to honor g:pythoncomplete_allow_import. + But the 'vim' module is only imported inside the outer + vimcomplete() / vimpy3complete() function, not at the script's + top level, so referring to it from a Completer method raises + NameError. The surrounding bare 'except' silently swallows + the error and leaves allow_imports at 0, meaning the opt-in + never takes effect -- 'import os' (and any other + buffer-level import) is always skipped, no candidates are + produced for 'os.<...>' and + Test_popup_and_preview_autocommand() fails on the Windows + CI matrix (Linux skips the test because Python 2 is absent). +Solution: Re-import 'vim' at the top of evalsource() in both + pythoncomplete.vim and python3complete.vim so the eval reads + the global, and set g:pythoncomplete_allow_import = 1 in the + test (it is the opt-in intended for callers that trust the + buffer contents) (thinca). + +closes: #20386 + +Backport of upstream commit 868ad62cb8bf8038322eab2badd31bd98b02b9df. +- runtime/autoload/python3complete.vim and runtime/autoload/pythoncomplete.vim + fixes taken verbatim from upstream. Required for the opt-in added by + 9.2.0561 to work at all. +- src/testdir/test_popup.vim: only the g:pythoncomplete_allow_import lines + were applied. +- src/version.c bump omitted as not relevant to the runtime fix. +--- + runtime/autoload/README.txt | 1 + + runtime/autoload/python3complete.vim | 20 +++++++++++++++++--- + runtime/autoload/pythoncomplete.vim | 20 +++++++++++++++++--- + runtime/doc/filetype.txt | 15 ++++++++++++++- + src/testdir/test_popup.vim | 4 ++++ + 5 files changed, 53 insertions(+), 7 deletions(-) + +diff --git a/runtime/autoload/README.txt b/runtime/autoload/README.txt +index 3b18d3d..b225819 100644 +--- a/runtime/autoload/README.txt ++++ b/runtime/autoload/README.txt +@@ -17,6 +17,7 @@ htmlcomplete.vim HTML + javascriptcomplete.vim Javascript + phpcomplete.vim PHP + pythoncomplete.vim Python ++python3complete.vim Python + rubycomplete.vim Ruby + syntaxcomplete.vim from syntax highlighting + xmlcomplete.vim XML (uses files in the xml directory) +diff --git a/runtime/autoload/python3complete.vim b/runtime/autoload/python3complete.vim +index ea0a331..1c432f3 100644 +--- a/runtime/autoload/python3complete.vim ++++ b/runtime/autoload/python3complete.vim +@@ -14,6 +14,10 @@ + " i.e. "import url" + " Continue parsing on invalid line?? + " ++" v 0.10 by Vim project ++" * disables importing local modules, unless the global Vim variable ++" g:pythoncomplete_allow_import is set to non-zero ++" + " v 0.9 + " * Fixed docstring parsing for classes and functions + " * Fixed parsing of *args and **kwargs type arguments +@@ -131,12 +135,24 @@ class Completer(object): + self.parser = PyParser() + + def evalsource(self,text,line=0): ++ # vim is imported locally in vimpy3complete(); re-import here so the ++ # vim.eval() below works (otherwise NameError, silently caught). ++ import vim + sc = self.parser.parse(text,line) ++ try: allow_imports = int( ++ vim.eval("get(g:, 'pythoncomplete_allow_import', 0)")) ++ except Exception: ++ allow_imports = 0 + src = sc.get_code() + dbg("source: %s" % src) + try: exec(src,self.compldict) + except: dbg("parser: %s, %s" % (sys.exc_info()[0],sys.exc_info()[1])) + for l in sc.locals: ++ # Executing import/from statements harvested from the buffer runs ++ # arbitrary package code; only do so when the user opted in. ++ if not allow_imports and (l.startswith('import') ++ or l.startswith('from ')): ++ continue + try: exec(l,self.compldict) + except: dbg("locals: %s, %s [%s]" % (sys.exc_info()[0],sys.exc_info()[1],l)) + +@@ -300,13 +316,11 @@ class Scope(object): + def get_code(self): + str = "" + if len(self.docstr) > 0: str += '"""'+self.docstr+'"""\n' +- for l in self.locals: +- if l.startswith('import'): str += l+'\n' + str += 'class _PyCmplNoType:\n def __getattr__(self,name):\n return None\n' + for sub in self.subscopes: + str += sub.get_code() + for l in self.locals: +- if not l.startswith('import'): str += l+'\n' ++ if not l.startswith('import') and not l.startswith('from '): str += l+'\n' + + return str + +diff --git a/runtime/autoload/pythoncomplete.vim b/runtime/autoload/pythoncomplete.vim +index aa28bb7..b4340f7 100644 +--- a/runtime/autoload/pythoncomplete.vim ++++ b/runtime/autoload/pythoncomplete.vim +@@ -12,6 +12,10 @@ + " i.e. "import url" + " Continue parsing on invalid line?? + " ++" v 0.10 by Vim project ++" * disables importing local modules, unless the global Vim variable ++" g:pythoncomplete_allow_import is set to non-zero ++" + " v 0.9 + " * Fixed docstring parsing for classes and functions + " * Fixed parsing of *args and **kwargs type arguments +@@ -145,12 +149,24 @@ class Completer(object): + self.parser = PyParser() + + def evalsource(self,text,line=0): ++ # vim is imported locally in vimcomplete(); re-import here so the ++ # vim.eval() below works (otherwise NameError, silently caught). ++ import vim + sc = self.parser.parse(text,line) ++ try: allow_imports = int( ++ vim.eval("get(g:, 'pythoncomplete_allow_import', 0)")) ++ except Exception: ++ allow_imports = 0 + src = sc.get_code() + dbg("source: %s" % src) + try: exec(src) in self.compldict + except: dbg("parser: %s, %s" % (sys.exc_info()[0],sys.exc_info()[1])) + for l in sc.locals: ++ # Executing import/from statements harvested from the buffer runs ++ # arbitrary package code; only do so when the user opted in. ++ if not allow_imports and (l.startswith('import') ++ or l.startswith('from ')): ++ continue + try: exec(l) in self.compldict + except: dbg("locals: %s, %s [%s]" % (sys.exc_info()[0],sys.exc_info()[1],l)) + +@@ -315,13 +331,11 @@ class Scope(object): + def get_code(self): + str = "" + if len(self.docstr) > 0: str += '"""'+self.docstr+'"""\n' +- for l in self.locals: +- if l.startswith('import'): str += l+'\n' + str += 'class _PyCmplNoType:\n def __getattr__(self,name):\n return None\n' + for sub in self.subscopes: + str += sub.get_code() + for l in self.locals: +- if not l.startswith('import'): str += l+'\n' ++ if not l.startswith('import') and not l.startswith('from '): str += l+'\n' + + return str + +diff --git a/runtime/doc/filetype.txt b/runtime/doc/filetype.txt +index 20bf395..ba32522 100644 +--- a/runtime/doc/filetype.txt ++++ b/runtime/doc/filetype.txt +@@ -920,7 +920,20 @@ By default the following options are set, in accordance with PEP8: > + To disable this behavior, set the following variable in your vimrc: > + + let g:python_recommended_style = 0 +- ++< ++Python omni-completion |compl-omni| is provided by python3complete.vim (or ++pythoncomplete.vim) for Vim builds with the |+python|/|+python3| interpreter. ++By default it does not inspect the import / from statements found in the ++buffer. This means completion of names defined in the buffer itself (classes, ++functions, variables) works, but completion of members of imported modules is ++not offered. ++ ++To enable completion of imported module members, set: > ++ let g:pythoncomplete_allow_import = 1 ++< ++WARNING: enabling this causes omni-completion to execute the import statements ++found in the buffer through Python's import machinery, which runs the imported ++modules' top-level code. Only enable this for code you trust. + + QF QUICKFIX *qf.vim* *ft-qf-plugin* + +diff --git a/src/testdir/test_popup.vim b/src/testdir/test_popup.vim +index e599a8d..0f3a4ca 100644 +--- a/src/testdir/test_popup.vim ++++ b/src/testdir/test_popup.vim +@@ -724,6 +724,9 @@ func Test_popup_and_preview_autocommand() + au! + au BufAdd * nested tab sball + augroup END ++ " Let pythoncomplete follow the buffer's 'import os' (off by default ++ " since v9.2.0561) so 'os.' can be completed. ++ let g:pythoncomplete_allow_import = 1 + set omnifunc=pythoncomplete#Complete + call setline(1, 'import os') + " make the line long +@@ -746,6 +749,7 @@ func Test_popup_and_preview_autocommand() + augroup END + augroup! MyBufAdd + bw! ++ unlet g:pythoncomplete_allow_import + endfunc + + func s:run_popup_and_previewwindow_dump(lines, dumpfile) diff -Nru vim-9.1.1230/debian/patches/CVE-2026-52859.patch vim-9.1.1230/debian/patches/CVE-2026-52859.patch --- vim-9.1.1230/debian/patches/CVE-2026-52859.patch 1970-01-01 00:00:00.000000000 +0000 +++ vim-9.1.1230/debian/patches/CVE-2026-52859.patch 2026-09-25 20:25:55.000000000 +0000 @@ -0,0 +1,274 @@ +From: Lee Garrett +Date: Fri, 25 Sep 2026 17:26:07 +0200 +Subject: Fix CVE-2026-52859 + +Origin: backport, https://github.com/vim/vim/commit/63680c6d3d52477817b49cd1a66e7aabe8a7aa19 +Forwarded: not-needed +Reviewed-By: Lee Garrett +Last-Update: 2026-09-25 + +patch 9.2.0565: [security]: out-of-bounds read in update_snapshot() +Problem: Out-of-bounds read in update_snapshot() when a terminal cell + fills all VTERM_MAX_CHARS_PER_CELL slots (a base character + plus five combining marks): the loop over cell.chars[] has no + upper bound and libvterm leaves the array unterminated when full, so + it reads past the array and appends out-of-bounds values to a + buffer sized for only VTERM_MAX_CHARS_PER_CELL characters. +Solution: Bound the loop with i < VTERM_MAX_CHARS_PER_CELL, mirroring + the loop in handle_pushline() (Christian Brabandt). + +This fixes CVE-2026-52859. + +Backport of upstream commit 63680c6d3d52477817b49cd1a66e7aabe8a7aa19. +- src/version.c bump omitted as not relevant to this branch. +--- + src/terminal.c | 3 +- + src/testdir/samples/combining_chars.txt | 200 ++++++++++++++++++++++++++++++++ + src/testdir/test_terminal3.vim | 15 +++ + 3 files changed, 217 insertions(+), 1 deletion(-) + create mode 100644 src/testdir/samples/combining_chars.txt + +diff --git a/src/terminal.c b/src/terminal.c +index 27e4c47..3cf7bc9 100644 +--- a/src/terminal.c ++++ b/src/terminal.c +@@ -2076,7 +2076,8 @@ update_snapshot(term_T *term) + int i; + int c; + +- for (i = 0; (c = cell.chars[i]) > 0 || i == 0; ++i) ++ for (i = 0; i < VTERM_MAX_CHARS_PER_CELL && ++ ((c = cell.chars[i]) > 0 || i == 0); ++i) + ga.ga_len += utf_char2bytes(c == NUL ? ' ' : c, + (char_u *)ga.ga_data + ga.ga_len); + } +diff --git a/src/testdir/samples/combining_chars.txt b/src/testdir/samples/combining_chars.txt +new file mode 100644 +index 0000000..d9a3c17 +--- /dev/null ++++ b/src/testdir/samples/combining_chars.txt +@@ -0,0 +1,200 @@ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ ++á́́́́ጁ ++á́́́́ጁ ++á́́́́ጁ ++á́́́́ጁ ++á́́́́ጁ ++á́́́́ጁ ++á́́́́ጁ ++á́́́́ጁ ++á́́́́ጁ ++á́́́́ጁ +diff --git a/src/testdir/test_terminal3.vim b/src/testdir/test_terminal3.vim +index 03130e6..095851e 100644 +--- a/src/testdir/test_terminal3.vim ++++ b/src/testdir/test_terminal3.vim +@@ -1027,4 +1027,19 @@ func Test_terminal_max_combining_chars() + exe buf . "bwipe!" + endfunc + ++func Test_terminal_output_combining_chars() ++ CheckUnix ++ new ++ let cmd = "cat samples/combining_chars.txt" ++ let buf = term_start(cmd, {'curwin': 1, 'term_finish': 'open', 'term_rows': 10, 'term_cols': 30}) ++ call WaitForAssert({-> assert_match('finished', term_getstatus(buf))}) ++ call TermWait(buf) ++ let lines = getbufline(buf, 1, '$') ++ " get byte lengths to confirm combining chars present ++ let lens = map(copy(lines), 'len(v:val)') ++ let expected = repeat([11], 190) + repeat([14], 10) ++ call assert_equal(expected, lens) ++ bw! ++endfunc ++ + " vim: shiftwidth=2 sts=2 expandtab diff -Nru vim-9.1.1230/debian/patches/CVE-2026-52860.patch vim-9.1.1230/debian/patches/CVE-2026-52860.patch --- vim-9.1.1230/debian/patches/CVE-2026-52860.patch 1970-01-01 00:00:00.000000000 +0000 +++ vim-9.1.1230/debian/patches/CVE-2026-52860.patch 2026-09-25 20:25:55.000000000 +0000 @@ -0,0 +1,451 @@ +From: Lee Garrett +Date: Fri, 25 Sep 2026 17:26:07 +0200 +Subject: Fix CVE-2026-52860 + +Origin: backport, https://github.com/vim/vim/commit/c8c63673bc4253212820626aeeb75999d9a539d2 +Bug: https://github.com/vim/vim/security/advisories/GHSA-65p9-mwwx-7468 +Forwarded: not-needed +Reviewed-By: Lee Garrett +Last-Update: 2026-09-25 + +patch 9.2.0597: [security]: possible code execution with python complete +Problem: [security]: another possible code execution with python complete + (David Carliez) +Solution: Strip default expressions and annotations from generated + source for pythoncomplete and python3complete. + +Github Security Advisory: +https://github.com/vim/vim/security/advisories/GHSA-65p9-mwwx-7468 + +This fixes CVE-2026-52860. + +Backport of upstream commit c8c63673bc4253212820626aeeb75999d9a539d2. +- src/version.c bump omitted as not relevant to this branch. +- test_plugin_python3complete.vim: added "source check.vim" so the + CheckFeature command used by the new test is available on this branch. +--- + runtime/autoload/python3complete.vim | 43 +++++- + runtime/autoload/pythoncomplete.vim | 43 +++++- + src/testdir/Make_all.mak | 2 + + src/testdir/test_plugin_python3complete.vim | 226 ++++++++++++++++++++++++++++ + 4 files changed, 306 insertions(+), 8 deletions(-) + create mode 100644 src/testdir/test_plugin_python3complete.vim + +diff --git a/runtime/autoload/python3complete.vim b/runtime/autoload/python3complete.vim +index 1c432f3..bdabf62 100644 +--- a/runtime/autoload/python3complete.vim ++++ b/runtime/autoload/python3complete.vim +@@ -1,8 +1,8 @@ + "python3complete.vim - Omni Completion for python + " Maintainer: + " Previous Maintainer: Aaron Griffin +-" Version: 0.9 +-" Last Updated: 2022 Mar 30 ++" Version: 0.10 ++" Last Updated: 2026 Jun 04 + " + " Roland Puntaier: this file contains adaptations for python3 and is parallel to pythoncomplete.vim + " +@@ -17,6 +17,11 @@ + " v 0.10 by Vim project + " * disables importing local modules, unless the global Vim variable + " g:pythoncomplete_allow_import is set to non-zero ++" * strip default values and annotations from function parameter lists ++" before exec(), and whitelist class base lists to dotted names: the ++" previous code passed buffer-supplied expressions to exec() which ++" Python evaluates at definition time, allowing arbitrary code ++" execution via crafted def/class headers + " + " v 0.9 + " * Fixed docstring parsing for classes and functions +@@ -100,6 +105,24 @@ warnings.simplefilter(action='ignore', category=FutureWarning) + + import sys, tokenize, io, types + from token import NAME, DEDENT, NEWLINE, STRING ++import re ++ ++# Used by Class.get_code(): a base class expression is only included in the ++# code passed to exec() if it is a pure dotted name (e.g. "Base", "mod.Base", ++# "pkg.sub.Cls"). Anything containing calls, subscripts, "=", ":" or other ++# operators is dropped, since exec()-ing it would evaluate buffer-supplied ++# expressions. See the security note in the file header. ++_DOTTED_NAME_RE = re.compile(r'^[A-Za-z_]\w*(\s*\.\s*[A-Za-z_]\w*)*$') ++ ++def _strip_param(p): ++ # Return the bare parameter name from a parameter spec harvested by ++ # _parenparse(), discarding any default value or annotation. Default ++ # values and annotations would otherwise be evaluated by exec() at ++ # function-definition time. Star prefixes ("*args", "**kw") and bare ++ # "*" / "/" are preserved as written. ++ p = p.split('=', 1)[0] ++ p = p.split(':', 1)[0] ++ return p.strip() + + debugstmts=[] + def dbg(s): debugstmts.append(s) +@@ -350,7 +373,13 @@ class Class(Scope): + return c + def get_code(self): + str = '%sclass %s' % (self.currentindent(),self.name) +- if len(self.supers) > 0: str += '(%s)' % ','.join(self.supers) ++ # Only include base class expressions that are pure dotted names. ++ # Anything else (calls, subscripts, conditionals, ...) is dropped ++ # because exec() would evaluate it at class-definition time. See ++ # the security note in the file header. ++ safe_supers = [s.strip() for s in self.supers ++ if _DOTTED_NAME_RE.match(s.strip())] ++ if len(safe_supers) > 0: str += '(%s)' % ','.join(safe_supers) + str += ':\n' + if len(self.docstr) > 0: str += self.childindent()+'"""'+self.docstr+'"""\n' + if len(self.subscopes) > 0: +@@ -367,8 +396,14 @@ class Function(Scope): + def copy_decl(self,indent=0): + return Function(self.name,self.params,indent, self.docstr) + def get_code(self): ++ # Strip default values and annotations from each parameter before ++ # joining: exec() evaluates these at definition time and a hostile ++ # buffer could otherwise execute arbitrary code via crafted def ++ # headers. See file header for details. ++ safe_params = [_strip_param(p) for p in self.params] ++ safe_params = [p for p in safe_params if p] + str = "%sdef %s(%s):\n" % \ +- (self.currentindent(),self.name,','.join(self.params)) ++ (self.currentindent(),self.name,','.join(safe_params)) + if len(self.docstr) > 0: str += self.childindent()+'"""'+self.docstr+'"""\n' + str += "%spass\n" % self.childindent() + return str +diff --git a/runtime/autoload/pythoncomplete.vim b/runtime/autoload/pythoncomplete.vim +index b4340f7..7614882 100644 +--- a/runtime/autoload/pythoncomplete.vim ++++ b/runtime/autoload/pythoncomplete.vim +@@ -1,8 +1,8 @@ + "pythoncomplete.vim - Omni Completion for python + " Maintainer: + " Previous Maintainer: Aaron Griffin +-" Version: 0.9 +-" Last Updated: 2020 Oct 9 ++" Version: 0.10 ++" Last Updated: 2026 Jun 04 + " + " Changes + " TODO: +@@ -15,6 +15,11 @@ + " v 0.10 by Vim project + " * disables importing local modules, unless the global Vim variable + " g:pythoncomplete_allow_import is set to non-zero ++" * strip default values and annotations from function parameter lists ++" before exec(), and whitelist class base lists to dotted names: the ++" previous code passed buffer-supplied expressions to exec() which ++" Python evaluates at definition time, allowing arbitrary code ++" execution via crafted def/class headers + " + " v 0.9 + " * Fixed docstring parsing for classes and functions +@@ -95,6 +100,24 @@ function! s:DefPython() + python << PYTHONEOF + import sys, tokenize, cStringIO, types + from token import NAME, DEDENT, NEWLINE, STRING ++import re ++ ++# Used by Class.get_code(): a base class expression is only included in the ++# code passed to exec() if it is a pure dotted name (e.g. "Base", "mod.Base", ++# "pkg.sub.Cls"). Anything containing calls, subscripts, "=", ":" or other ++# operators is dropped, since exec()-ing it would evaluate buffer-supplied ++# expressions. See the security note in the file header. ++_DOTTED_NAME_RE = re.compile(r'^[A-Za-z_]\w*(\s*\.\s*[A-Za-z_]\w*)*$') ++ ++def _strip_param(p): ++ # Return the bare parameter name from a parameter spec harvested by ++ # _parenparse(), discarding any default value or annotation. Default ++ # values and annotations would otherwise be evaluated by exec() at ++ # function-definition time. Star prefixes ("*args", "**kw") and bare ++ # "*" / "/" are preserved as written. ++ p = p.split('=', 1)[0] ++ p = p.split(':', 1)[0] ++ return p.strip() + + debugstmts=[] + def dbg(s): debugstmts.append(s) +@@ -365,7 +388,13 @@ class Class(Scope): + return c + def get_code(self): + str = '%sclass %s' % (self.currentindent(),self.name) +- if len(self.supers) > 0: str += '(%s)' % ','.join(self.supers) ++ # Only include base class expressions that are pure dotted names. ++ # Anything else (calls, subscripts, conditionals, ...) is dropped ++ # because exec() would evaluate it at class-definition time. See ++ # the security note in the file header. ++ safe_supers = [s.strip() for s in self.supers ++ if _DOTTED_NAME_RE.match(s.strip())] ++ if len(safe_supers) > 0: str += '(%s)' % ','.join(safe_supers) + str += ':\n' + if len(self.docstr) > 0: str += self.childindent()+'"""'+self.docstr+'"""\n' + if len(self.subscopes) > 0: +@@ -382,8 +411,14 @@ class Function(Scope): + def copy_decl(self,indent=0): + return Function(self.name,self.params,indent, self.docstr) + def get_code(self): ++ # Strip default values and annotations from each parameter before ++ # joining: exec() evaluates these at definition time and a hostile ++ # buffer could otherwise execute arbitrary code via crafted def ++ # headers. See file header for details. ++ safe_params = [_strip_param(p) for p in self.params] ++ safe_params = [p for p in safe_params if p] + str = "%sdef %s(%s):\n" % \ +- (self.currentindent(),self.name,','.join(self.params)) ++ (self.currentindent(),self.name,','.join(safe_params)) + if len(self.docstr) > 0: str += self.childindent()+'"""'+self.docstr+'"""\n' + str += "%spass\n" % self.childindent() + return str +diff --git a/src/testdir/Make_all.mak b/src/testdir/Make_all.mak +index 0c22f02..6932188 100644 +--- a/src/testdir/Make_all.mak ++++ b/src/testdir/Make_all.mak +@@ -244,6 +244,7 @@ NEW_TESTS = \ + test_plugin_man \ + test_plugin_matchparen \ + test_plugin_netrw \ ++ test_plugin_python3complete \ + test_plugin_tar \ + test_plugin_termdebug \ + test_plugin_tohtml \ +@@ -510,6 +511,7 @@ NEW_TESTS_RES = \ + test_plugin_man.res \ + test_plugin_matchparen.res \ + test_plugin_netrw.res \ ++ test_plugin_python3complete.res \ + test_plugin_tar.res \ + test_plugin_termdebug.res \ + test_plugin_tohtml.res \ +diff --git a/src/testdir/test_plugin_python3complete.vim b/src/testdir/test_plugin_python3complete.vim +new file mode 100644 +index 0000000..9a237d0 +--- /dev/null ++++ b/src/testdir/test_plugin_python3complete.vim +@@ -0,0 +1,226 @@ ++" Tests for the Python omni-completion plugin (runtime/autoload/python3complete.vim). ++" ++source check.vim ++ ++CheckFeature python3 ++ ++" Run omni-completion against the given buffer contents and assert that the ++" marker file was not created. Pre-patch behaviour exec()s reconstructed ++" def/class headers, which evaluates the buffer-supplied expression and ++" creates the marker file. Post-patch, the expressions are stripped. ++func s:CompleteAndExpectNoMarker(buffer_lines, marker_path, msg) ++ call delete(a:marker_path) ++ defer delete(a:marker_path) ++ let g:pythoncomplete_allow_import = 0 ++ new ++ setfiletype python ++ call setline(1, a:buffer_lines) ++ call cursor(line('$'), col([line('$'), '$'])) ++ ++ " The PoC trigger -- direct invocation of the omnifunc with an empty base. ++ " This is the same path Vim takes for CTRL-X CTRL-O. ++ silent! call python3complete#Complete(0, '') ++ ++ call assert_false(filereadable(a:marker_path), ++ \ a:msg . ' (marker ' . a:marker_path . ' was created)') ++ ++ bwipe! ++ unlet! g:pythoncomplete_allow_import ++endfunc ++ ++func Test_python3complete_no_exec_via_function_default() ++ let marker = tempname() ++ call s:CompleteAndExpectNoMarker([ ++ \ 'def f(x=open(' . string(marker) . ', "w").close()):', ++ \ ' pass', ++ \ 'f.', ++ \ ], marker, ++ \ 'function default expression was evaluated during omni-completion') ++endfunc ++ ++func Test_python3complete_no_exec_via_function_annotation() ++ let marker = tempname() ++ call s:CompleteAndExpectNoMarker([ ++ \ 'def f(x: open(' . string(marker) . ', "w").close()):', ++ \ ' pass', ++ \ 'f.', ++ \ ], marker, ++ \ 'function annotation expression was evaluated during omni-completion') ++endfunc ++ ++func Test_python3complete_no_exec_via_class_base() ++ let marker = tempname() ++ " "or object" gives the class a valid base after the side-effecting ++ " open().close() expression returns None. Without "or object" the ++ " exec would raise TypeError, but the file would still be created ++ " before the exception -- the assertion would still hold. Using ++ " "or object" keeps the buffer parseable as valid Python. ++ call s:CompleteAndExpectNoMarker([ ++ \ 'class Foo(open(' . string(marker) . ', "w").close() or object):', ++ \ ' pass', ++ \ 'Foo.', ++ \ ], marker, ++ \ 'class base expression was evaluated during omni-completion') ++endfunc ++ ++func Test_python3complete_no_exec_with_multiple_params() ++ " The strip must apply to every parameter, not just the first. ++ let marker = tempname() ++ call s:CompleteAndExpectNoMarker([ ++ \ 'def f(a, b=1, c=open(' . string(marker) . ', "w").close(), d=2):', ++ \ ' pass', ++ \ 'f.', ++ \ ], marker, ++ \ 'non-first parameter default was evaluated during omni-completion') ++endfunc ++ ++func Test_python3complete_no_exec_via_starargs_default() ++ " "*args" and "**kw" must still be preserved after stripping; ensure a ++ " default following them is also stripped. ++ let marker = tempname() ++ call s:CompleteAndExpectNoMarker([ ++ \ 'def f(*args, key=open(' . string(marker) . ', "w").close(), **kw):', ++ \ ' pass', ++ \ 'f.', ++ \ ], marker, ++ \ 'keyword-only default after *args was evaluated during omni-completion') ++endfunc ++ ++func Test_python3complete_normal_completion_still_works() ++ " Positive control: completion against a buffer with a legitimate class ++ " must still produce completion items. The stripping logic should not ++ " break the normal completion path. ++ let g:pythoncomplete_allow_import = 0 ++ ++ new ++ setfiletype python ++ call setline(1, [ ++ \ 'class MyHelper:', ++ \ ' def alpha(self): pass', ++ \ ' def beta(self): pass', ++ \ 'h = MyHelper()', ++ \ 'h.', ++ \ ]) ++ call cursor(5, 3) ++ ++ " First call returns the column to start completion at; second returns ++ " the list of completion items. ++ let start = python3complete#Complete(1, '') ++ call assert_true(start >= 0, ++ \ 'python3complete#Complete(1, "") returned ' . start) ++ ++ let items = python3complete#Complete(0, '') ++ " Items should be a list (possibly empty if the parser can't resolve "h", ++ " but should not be a parse error from our stripping changes). ++ call assert_equal(type([]), type(items), ++ \ 'python3complete#Complete(0, "") did not return a list') ++ ++ bwipe! ++ unlet! g:pythoncomplete_allow_import ++endfunc ++ ++func Test_python3complete_inherited_completion_via_dotted_base() ++ " Positive control for the class-base whitelist: a dotted-name base class ++ " (the common, safe case) must still be carried into the reconstructed ++ " source so that completion on a subclass can resolve inherited members. ++ let g:pythoncomplete_allow_import = 0 ++ ++ new ++ setfiletype python ++ call setline(1, [ ++ \ 'class Base:', ++ \ ' def shared(self): pass', ++ \ 'class Derived(Base):', ++ \ ' def own(self): pass', ++ \ 'd = Derived()', ++ \ 'd.', ++ \ ]) ++ call cursor(6, 3) ++ ++ let items = python3complete#Complete(0, '') ++ call assert_equal(type([]), type(items), ++ \ 'completion against a subclass with a dotted base did not return a list') ++ ++ bwipe! ++ unlet! g:pythoncomplete_allow_import ++endfunc ++ ++" Build a tiny Python module that creates a marker file as a side effect of ++" being imported, add its directory to sys.path, run omni-completion against ++" a buffer containing `import vimtest_marker_mod`, and report whether the ++" marker file was created. Used by the two allow_import tests below. ++func s:RunImportCompletion(allow_import_value) ++ let g:pythoncomplete_allow_import = a:allow_import_value ++ let marker = tempname() ++ let module_dir = tempname() ++ call mkdir(module_dir, 'R') ++ ++ call writefile([ ++ \ 'open(' . string(marker) . ', "w").close()', ++ \ ], module_dir . '/vimtest_marker_mod.py') ++ ++ defer delete(marker) ++ ++ " Pass module_dir to Python via a g: variable so vim.eval() can read it. ++ let g:pythoncomplete_test_module_dir = module_dir ++ py3 << EOF ++import sys, vim ++_p = vim.eval('g:pythoncomplete_test_module_dir') ++if _p not in sys.path: ++ sys.path.insert(0, _p) ++# Drop any cached copy so the module body re-runs and the marker side ++# effect fires on import. ++sys.modules.pop('vimtest_marker_mod', None) ++EOF ++ ++ new ++ setfiletype python ++ call setline(1, [ ++ \ 'import vimtest_marker_mod', ++ \ 'vimtest_marker_mod.', ++ \ ]) ++ call cursor(2, 2) ++ ++ silent! call python3complete#Complete(0, '') ++ ++ let ran = filereadable(marker) ++ ++ bwipe! ++ unlet g:pythoncomplete_allow_import ++ ++ " Teardown: restore sys.path, drop the cached module so a subsequent ++ " test run starts clean, clean up the temp module dir. ++ py3 << EOF ++import sys, vim ++_p = vim.eval('g:pythoncomplete_test_module_dir') ++if _p in sys.path: ++ sys.path.remove(_p) ++sys.modules.pop('vimtest_marker_mod', None) ++EOF ++ unlet g:pythoncomplete_test_module_dir ++ call delete(module_dir, 'rf') ++ call delete(marker) ++ unlet! g:pythoncomplete_allow_import ++ ++ return ran ++endfunc ++ ++func Test_python3complete_allow_import_off_blocks_imports() ++ " GHSA-52mc-rq6p-rc7c mitigation: with the default flag value (0), an ++ " `import` line harvested from the buffer must NOT be exec()'d. The ++ " marker module's side effect (creating a file when its body runs) is ++ " the observable proof that the exec did or did not happen. ++ call assert_false(s:RunImportCompletion(0), ++ \ 'g:pythoncomplete_allow_import=0 did not block the buffer import') ++endfunc ++ ++func Test_python3complete_allow_import_on_runs_imports() ++ " Symmetric positive control: with the flag set to non-zero, the harvested ++ " import IS exec()'d and the module loads. Without this control the ++ " negative test above could pass for unrelated reasons (e.g. completion ++ " failing to parse the buffer at all). ++ call assert_true(s:RunImportCompletion(1), ++ \ 'g:pythoncomplete_allow_import=1 did not run the buffer import') ++endfunc ++ ++" vim: shiftwidth=2 sts=2 expandtab diff -Nru vim-9.1.1230/debian/patches/CVE-2026-55693.patch vim-9.1.1230/debian/patches/CVE-2026-55693.patch --- vim-9.1.1230/debian/patches/CVE-2026-55693.patch 1970-01-01 00:00:00.000000000 +0000 +++ vim-9.1.1230/debian/patches/CVE-2026-55693.patch 2026-09-25 20:25:55.000000000 +0000 @@ -0,0 +1,89 @@ +From: Lee Garrett +Date: Fri, 25 Sep 2026 17:26:07 +0200 +Subject: Fix CVE-2026-55693 + +Origin: backport, https://github.com/vim/vim/commit/a80874d9b84a01040e3d1aef2d4a59e1934dafb7 +Bug: https://github.com/vim/vim/security/advisories/GHSA-wgh4-64f7-q3jq +Forwarded: not-needed +Reviewed-By: Lee Garrett +Last-Update: 2026-09-25 + +patch 9.2.0653: [security]: out-of-bounds write in tree_count_words() +Problem: [security]: a crafted spell file can drive tree_count_words() + past the end of its MAXWLEN-sized depth arrays; the descent + loop has no depth bound. +Solution: only descend while depth < MAXWLEN - 1, as the sibling trie + walkers already do; apply the same guard to sug_filltree(). + +Github Security Advisory: +https://github.com/vim/vim/security/advisories/GHSA-wgh4-64f7-q3jq + +Supported by AI. + +This fixes CVE-2026-55693. + +Backport of upstream commit a80874d9b84a01040e3d1aef2d4a59e1934dafb7. +- src/version.c bump omitted as not relevant to this branch. +--- + src/spellfile.c | 4 ++-- + src/testdir/test_spellfile.vim | 27 +++++++++++++++++++++++++++ + 2 files changed, 29 insertions(+), 2 deletions(-) + +diff --git a/src/spellfile.c b/src/spellfile.c +index 768e9fd..2f6e59e 100644 +--- a/src/spellfile.c ++++ b/src/spellfile.c +@@ -648,7 +648,7 @@ tree_count_words(char_u *byts, idx_T *idxs) + ++curi[depth]; + } + } +- else ++ else if (depth < MAXWLEN - 1) + { + // Normal char, go one level deeper to count the words. + ++depth; +@@ -5656,7 +5656,7 @@ sug_filltree(spellinfo_T *spin, slang_T *slang) + ++curi[depth]; + } + } +- else ++ else if (depth < MAXWLEN - 1) + { + // Normal char, go one level deeper. + tword[depth++] = c; +diff --git a/src/testdir/test_spellfile.vim b/src/testdir/test_spellfile.vim +index b3f6e98..e4b16cf 100644 +--- a/src/testdir/test_spellfile.vim ++++ b/src/testdir/test_spellfile.vim +@@ -1173,4 +1173,31 @@ func Test_mkspell_empty_dic() + endfunc + + ++func Test_spell_sug_tree_count_words_overflow() ++ " A crafted .spl/.sug pair with a BY_INDEX self-cycle in the fold word tree ++ " parses cleanly (shared refs aren't recursed, so read_tree_node()'s depth ++ " cap never trips), but drove tree_count_words() past its MAXWLEN-sized depth ++ " arrays -> stack out-of-bounds write. The walk only happens when ++ " spellsuggest() loads the matching .sug. Reaching the assert == no OOB. ++ call mkdir('Xrtp/spell', 'pR') ++ " VIMspell + v50, SN_SUGFILE(ts), SN_END, LWORDTREE{node:1,BY_INDEX->0,'A'}, ++ " empty KWORDTREE/PREFIXTREE ++ let spl = eval('0z56494D7370656C6C320B0000000008000000001234' ++ \ .. '5678FF000000020101000000410000000000000000') ++ " VIMsug + v1, matching ts, SUGWORDTREE word "a", empty SUGTABLE ++ let sug = 0z56494D737567010000000012345678000000040161010000000000 ++ call writefile(spl, 'Xrtp/spell/xx.utf-8.spl', 'b') ++ call writefile(sug, 'Xrtp/spell/xx.utf-8.sug', 'b') ++ ++ new ++ set runtimepath+=./Xrtp ++ set spelllang=xx ++ set spell ++ " Unpatched: OOB write here (ASan abort, or crash). Patched: returns a list. ++ call assert_equal(v:t_list, type(spellsuggest('helloo'))) ++ ++ set spell& spelllang& runtimepath& ++ bwipe! ++endfunc ++ + " vim: shiftwidth=2 sts=2 expandtab diff -Nru vim-9.1.1230/debian/patches/CVE-2026-55892.patch vim-9.1.1230/debian/patches/CVE-2026-55892.patch --- vim-9.1.1230/debian/patches/CVE-2026-55892.patch 1970-01-01 00:00:00.000000000 +0000 +++ vim-9.1.1230/debian/patches/CVE-2026-55892.patch 2026-09-25 20:25:55.000000000 +0000 @@ -0,0 +1,81 @@ +From: Lee Garrett +Date: Fri, 25 Sep 2026 17:26:07 +0200 +Subject: Fix CVE-2026-55892 + +Origin: backport, https://github.com/vim/vim/commit/8325b193bba5f01e7a7d8241fc8633d93dff996b +Bug: https://github.com/vim/vim/security/advisories/GHSA-qm9w-fmpj-879h +Forwarded: not-needed +Reviewed-By: Lee Garrett +Last-Update: 2026-09-25 + +patch 9.2.0662: [security]: Stack out-of-bounds write in dump_prefixes() +Problem: [security]: a crafted spell file with a self-referential + BY_INDEX node in the prefix tree can drive dump_prefixes() + past the end of its MAXWLEN-sized depth arrays on :spelldump + (cipher-creator) +Solution: only descend while depth < MAXWLEN - 1, as the sibling trie + walkers already do (Yasuhiro Matsumoto) + +Github Security Advisory: +https://github.com/vim/vim/security/advisories/GHSA-qm9w-fmpj-879h + +Supported by AI + +This fixes CVE-2026-55892. + +Backport of upstream commit 8325b193bba5f01e7a7d8241fc8633d93dff996b. +- src/version.c bump omitted as not relevant to this branch. +--- + src/spell.c | 2 +- + src/testdir/test_spell.vim | 27 +++++++++++++++++++++++++++ + 2 files changed, 28 insertions(+), 1 deletion(-) + +diff --git a/src/spell.c b/src/spell.c +index 87256f9..8ab197d 100644 +--- a/src/spell.c ++++ b/src/spell.c +@@ -4328,7 +4328,7 @@ dump_prefixes( + } + } + } +- else ++ else if (depth < MAXWLEN - 1) + { + // Normal char, go one level deeper. + prefix[depth++] = c; +diff --git a/src/testdir/test_spell.vim b/src/testdir/test_spell.vim +index 692e191..e9c7cca 100644 +--- a/src/testdir/test_spell.vim ++++ b/src/testdir/test_spell.vim +@@ -1569,4 +1569,31 @@ let g:test_data_aff_sal = [ + \"SAL Z S", + \ ] + ++" A crafted .spl with a self-referential BY_INDEX node in the PREFIXTREE drove ++" dump_prefixes() past its MAXWLEN-sized depth arrays (stack out-of-bounds ++" write). The tree parses cleanly (shared refs aren't recursed); the walk ++" happens on :spelldump. Reaching the assert means no OOB. Same class as the ++" tree_count_words() fix (9.2.0653). ++func Test_spelldump_prefixtree_overflow() ++ CheckUnix ++ call mkdir('Xrtp/spell', 'pR') ++ " VIMspell + v50, SN_PREFCOND(prefixcnt=1), SN_END, ++ " LWORDTREE word "a" with affixID=1 (so dump_prefixes runs), ++ " empty KWORDTREE, PREFIXTREE child BY_INDEX -> nodeidx 0 (self-cycle), 'A' ++ let spl = eval('0z56494D7370656C6C32030000000003000100FF00000004' ++ \ .. '0161010220010000000000000002010100000041') ++ call writefile(spl, 'Xrtp/spell/xx.utf-8.spl', 'b') ++ ++ new ++ set runtimepath+=./Xrtp ++ set spelllang=xx ++ set spell ++ spelldump ++ call assert_true(line('$') > 1) ++ ++ set spell& spelllang& runtimepath& ++ bwipe! ++ bwipe! ++endfunc ++ + " vim: shiftwidth=2 sts=2 expandtab diff -Nru vim-9.1.1230/debian/patches/CVE-2026-55895.patch vim-9.1.1230/debian/patches/CVE-2026-55895.patch --- vim-9.1.1230/debian/patches/CVE-2026-55895.patch 1970-01-01 00:00:00.000000000 +0000 +++ vim-9.1.1230/debian/patches/CVE-2026-55895.patch 2026-09-25 20:25:55.000000000 +0000 @@ -0,0 +1,85 @@ +From: Lee Garrett +Date: Fri, 25 Sep 2026 17:26:07 +0200 +Subject: Fix CVE-2026-55895 + +Origin: backport, https://github.com/vim/vim/commit/55bc757a5d436e59d50fe43f7cda94b118f86cb2 +Bug: https://github.com/vim/vim/security/advisories/GHSA-vhh8-v6wx-hjjh +Forwarded: not-needed +Reviewed-By: Lee Garrett +Last-Update: 2026-09-25 + +patch 9.2.0663: [security]: runtime(netrw): code injection in local file deletion +Problem: [security]: s:NetrwLocalRmFile() escapes only the backslash in + the file name before passing it to :execute, so a name + containing "|" injects arbitrary Ex commands when the file is + deleted (cipher-creator) +Solution: Use fnameescape() to correctly escape the file name + (Yasuhiro Matsumoto). + +Github Security Advisory: +https://github.com/vim/vim/security/advisories/GHSA-vhh8-v6wx-hjjh + +Supported by AI + +This fixes CVE-2026-55895. + +Backport of upstream commit 55bc757a5d436e59d50fe43f7cda94b118f86cb2. +- src/version.c bump omitted as not relevant to this branch. +- "Last Change" header hunk omitted; this branch's netrw.vim header has + no such line. +--- + runtime/pack/dist/opt/netrw/autoload/netrw.vim | 4 ++-- + src/testdir/test_plugin_netrw.vim | 20 ++++++++++++++++++++ + 2 files changed, 22 insertions(+), 2 deletions(-) + +diff --git a/runtime/pack/dist/opt/netrw/autoload/netrw.vim b/runtime/pack/dist/opt/netrw/autoload/netrw.vim +index 1cfa0af..4412a01 100644 +--- a/runtime/pack/dist/opt/netrw/autoload/netrw.vim ++++ b/runtime/pack/dist/opt/netrw/autoload/netrw.vim +@@ -3636,7 +3636,7 @@ fun! s:NetrwBrowse(islocal,dirname) + elseif !a:islocal && dirname !~ '[\/]$' && dirname !~ '^"' + " s:NetrwBrowse : remote regular file handler {{{3 + if bufname(dirname) != "" +- exe "NetrwKeepj b ".bufname(dirname) ++ exe "NetrwKeepj b ".fnameescape(bufname(dirname)) + else + " attempt transfer of remote regular file + +@@ -10310,7 +10310,7 @@ function! s:NetrwLocalRmFile(path, fname, all) + call netrw#ErrorMsg(s:ERROR, printf("unable to delete <%s>!", rmfile), 103) + else + " Remove file only if there are no pending changes +- execute printf('silent! bwipeout %s', rmfile) ++ execute printf('silent! bwipeout %s', fnameescape(rmfile)) + endif + + elseif dir && (all || empty(ok)) +diff --git a/src/testdir/test_plugin_netrw.vim b/src/testdir/test_plugin_netrw.vim +index ecfc35f..cb729b5 100644 +--- a/src/testdir/test_plugin_netrw.vim ++++ b/src/testdir/test_plugin_netrw.vim +@@ -73,4 +73,24 @@ func Test_netrw_injection() + endtry + endfunc + ++" Deleting a file whose name contains an Ex command separator must not let the ++" name inject commands into the :execute in s:NetrwLocalRmFile(). ++func Test_netrw_local_rm_injection() ++ CheckUnix ++ let dir = getcwd() . '/Xnetrwrm' ++ let fname = "x|let g:injected = 1" ++ call mkdir(dir, 'pR') ++ call writefile([], dir . '/' . fname) ++ try ++ call netrw#Call('NetrwLocalRmFile', dir, fname, 1) ++ call assert_false(exists('g:injected'), 'filename must not inject Ex commands') ++ " The file is removed before the sink, so its absence also confirms the ++ " vulnerable code path was actually exercised (not skipped on an error). ++ call assert_false(filereadable(dir . '/' . fname), 'crafted file must be deleted') ++ finally ++ call delete(dir . '/' . fname) ++ unlet! g:injected ++ endtry ++endfunc ++ + " vim:ts=8 sts=2 sw=2 et diff -Nru vim-9.1.1230/debian/patches/CVE-2026-57452.patch vim-9.1.1230/debian/patches/CVE-2026-57452.patch --- vim-9.1.1230/debian/patches/CVE-2026-57452.patch 1970-01-01 00:00:00.000000000 +0000 +++ vim-9.1.1230/debian/patches/CVE-2026-57452.patch 2026-09-25 20:25:55.000000000 +0000 @@ -0,0 +1,77 @@ +From: Lee Garrett +Date: Fri, 25 Sep 2026 17:26:07 +0200 +Subject: Fix CVE-2026-57452 + +Origin: backport, https://github.com/vim/vim/commit/c8777cec25dcfae89c42e9aff51af61f71c5745f +Bug: https://github.com/vim/vim/security/advisories/GHSA-c4j9-wr9j-4486 +Forwarded: not-needed +Reviewed-By: Lee Garrett +Last-Update: 2026-09-25 + +patch 9.2.0671: [security]: possible out-of-bounds read with sodium encrypted files +Problem: [security]: possible out-of-bounds read with sodium encrypted + files (cipher-creator) +Solution: Verify that there is enough space before calling + crypto_secretstream_xchacha20poly1305_init_pull() + +Github Security Advisory: +https://github.com/vim/vim/security/advisories/GHSA-c4j9-wr9j-4486 + +Supported by AI + +This fixes CVE-2026-57452. + +Backport of upstream commit c8777cec25dcfae89c42e9aff51af61f71c5745f. +- src/version.c bump omitted as not relevant to this branch. +--- + src/crypt.c | 3 ++- + src/testdir/test_crypt.vim | 24 ++++++++++++++++++++++++ + 2 files changed, 26 insertions(+), 1 deletion(-) + +diff --git a/src/crypt.c b/src/crypt.c +index 09e9011..af7d4ba 100644 +--- a/src/crypt.c ++++ b/src/crypt.c +@@ -1257,7 +1257,8 @@ crypt_sodium_buffer_decode( + + if (sod_st->count == 0) + { +- if (crypto_secretstream_xchacha20poly1305_init_pull(&sod_st->state, ++ if (len < crypto_secretstream_xchacha20poly1305_HEADERBYTES || ++ crypto_secretstream_xchacha20poly1305_init_pull(&sod_st->state, + from, sod_st->key) != 0) + { + emsg(_(e_libsodium_decryption_failed_header_incomplete)); +diff --git a/src/testdir/test_crypt.vim b/src/testdir/test_crypt.vim +index 85ed574..39ad2e4 100644 +--- a/src/testdir/test_crypt.vim ++++ b/src/testdir/test_crypt.vim +@@ -461,4 +461,28 @@ func Test_crypt_set_key_disallow_append_subtract() + bwipe! + endfunc + ++func Test_crypt_sodium_short_body() ++ CheckFeature sodium ++ " A VimCrypt~04! file with a complete 36-byte header (12 magic + 16 salt + ++ " 8 seed) but a body shorter than one secretstream header (24 bytes) used to ++ " underflow the body length and crash with a wild out-of-bounds read in ++ " crypto_secretstream_xchacha20poly1305_pull(). It must now fail cleanly. ++ " Bytes: "VimCrypt~04!" + 16 salt + 8 seed + 8-byte body = 44 bytes. ++ call writefile(0z56696D43727970747E303421 ++ \ + 0zA0A1A2A3A4A5A6A7A8A9AAABACADAEAF ++ \ + 0zB0B1B2B3B4B5B6B7 ++ \ + 0z0000000000000000, 'Xtest_sodium_short') ++ ++ let v:errmsg = '' ++ try ++ call feedkeys(":split Xtest_sodium_short\foobar\", "xt") ++ catch /^Vim\%((\S\+)\)\=:E1198:/ ++ " no-op ++ endtry ++ ++ bwipe! ++ call delete('Xtest_sodium_short') ++ set key= ++endfunc ++ + " vim: shiftwidth=2 sts=2 expandtab diff -Nru vim-9.1.1230/debian/patches/CVE-2026-57455.patch vim-9.1.1230/debian/patches/CVE-2026-57455.patch --- vim-9.1.1230/debian/patches/CVE-2026-57455.patch 1970-01-01 00:00:00.000000000 +0000 +++ vim-9.1.1230/debian/patches/CVE-2026-57455.patch 2026-09-25 20:25:55.000000000 +0000 @@ -0,0 +1,73 @@ +From: Lee Garrett +Date: Fri, 25 Sep 2026 17:26:07 +0200 +Subject: Fix CVE-2026-57455 + +Origin: backport, https://github.com/vim/vim/commit/497f931f85339d175d7f69588dd249e8ccfed41b +Bug: https://github.com/vim/vim/security/advisories/GHSA-q8mh-6qm3-25g4 +Forwarded: not-needed +Reviewed-By: Lee Garrett +Last-Update: 2026-09-25 + +patch 9.2.0698: [security]: Out-of-bounds write with soundfold() +Problem: [security]: Out-of-bounds write with soundfold() + (cipher-creator) +Solution: Add an abort condition to the for loop to validate the buffer + size. + +Github Security Advisory: +https://github.com/vim/vim/security/advisories/GHSA-q8mh-6qm3-25g4 + +Supported by AI + +This fixes CVE-2026-57455. + +Backport of upstream commit 497f931f85339d175d7f69588dd249e8ccfed41b. +- src/version.c bump omitted as not relevant to this branch. +--- + src/spell.c | 2 +- + src/testdir/test_spellfile.vim | 21 +++++++++++++++++++++ + 2 files changed, 22 insertions(+), 1 deletion(-) + +diff --git a/src/spell.c b/src/spell.c +index 8ab197d..f41272b 100644 +--- a/src/spell.c ++++ b/src/spell.c +@@ -3273,7 +3273,7 @@ spell_soundfold_sofo(slang_T *slang, char_u *inword, char_u *res) + else + { + // The sl_sal_first[] table contains the translation. +- for (s = inword; (c = *s) != NUL; ++s) ++ for (s = inword; (c = *s) != NUL && ri < MAXWLEN - 1; ++s) + { + if (VIM_ISWHITE(c)) + c = ' '; +diff --git a/src/testdir/test_spellfile.vim b/src/testdir/test_spellfile.vim +index e4b16cf..49ce9b3 100644 +--- a/src/testdir/test_spellfile.vim ++++ b/src/testdir/test_spellfile.vim +@@ -1200,4 +1200,25 @@ func Test_spell_sug_tree_count_words_overflow() + bwipe! + endfunc + ++" A word longer than MAXWLEN must not overflow the soundfold result buffer in ++" the single-byte SOFO branch of spell_soundfold_sofo(). ++func Test_soundfold_overflow() ++ let _enc=&enc ++ set enc=latin1 ++ call writefile(['SOFOFROM ab', 'SOFOTO xy'], 'Xtest.aff', 'D') ++ call writefile(['1', 'foo'], 'Xtest.dic', 'D') ++ mkspell! Xtest Xtest ++ defer delete('Xtest.latin1.spl') ++ defer delete('Xtest.latin1.sug') ++ setl spelllang=Xtest.latin1.spl spell ++ ++ " Before the fix the copy loop wrote one byte per input byte into a ++ " MAXWLEN (254) stack buffer with no upper bound, smashing the stack. ++ let sound = soundfold(repeat('ab', 300)) ++ call assert_true(strlen(sound) < 254, 'soundfold result exceeds MAXWLEN') ++ ++ set spell& spelllang& ++ let &enc = _enc ++endfunc ++ + " vim: shiftwidth=2 sts=2 expandtab diff -Nru vim-9.1.1230/debian/patches/CVE-2026-57456.patch vim-9.1.1230/debian/patches/CVE-2026-57456.patch --- vim-9.1.1230/debian/patches/CVE-2026-57456.patch 1970-01-01 00:00:00.000000000 +0000 +++ vim-9.1.1230/debian/patches/CVE-2026-57456.patch 2026-09-25 20:25:55.000000000 +0000 @@ -0,0 +1,150 @@ +From: Lee Garrett +Date: Fri, 25 Sep 2026 17:26:07 +0200 +Subject: Fix CVE-2026-57456 + +Origin: backport, https://github.com/vim/vim/commit/cce141c42740f122dd8486ae04e21c2a81016ba8 +Bug: https://github.com/vim/vim/security/advisories/GHSA-ppj8-wqjf-6fp3 +Forwarded: not-needed +Reviewed-By: Lee Garrett +Last-Update: 2026-09-25 + +patch 9.2.0699: [security]: possible code execution with python complete +Problem: [security]: possible code execution with python complete + (morningbread) +Solution: Use repr() to quote the doc strings correctly + +Github Security Advisory: +https://github.com/vim/vim/security/advisories/GHSA-ppj8-wqjf-6fp3 + +Supported by AI + +This fixes CVE-2026-57456. + +Backport of upstream commit cce141c42740f122dd8486ae04e21c2a81016ba8. +- src/version.c bump omitted as not relevant to this branch. +--- + runtime/autoload/python3complete.vim | 9 +++++---- + runtime/autoload/pythoncomplete.vim | 9 +++++---- + src/testdir/test_plugin_python3complete.vim | 15 +++++++++++++++ + 3 files changed, 25 insertions(+), 8 deletions(-) + +diff --git a/runtime/autoload/python3complete.vim b/runtime/autoload/python3complete.vim +index bdabf62..6df10d9 100644 +--- a/runtime/autoload/python3complete.vim ++++ b/runtime/autoload/python3complete.vim +@@ -2,7 +2,7 @@ + " Maintainer: + " Previous Maintainer: Aaron Griffin + " Version: 0.10 +-" Last Updated: 2026 Jun 04 ++" Last Updated: 2026 Jun 21 + " + " Roland Puntaier: this file contains adaptations for python3 and is parallel to pythoncomplete.vim + " +@@ -22,6 +22,7 @@ + " previous code passed buffer-supplied expressions to exec() which + " Python evaluates at definition time, allowing arbitrary code + " execution via crafted def/class headers ++" * use repr() on doc strings to prevent code execution + " + " v 0.9 + " * Fixed docstring parsing for classes and functions +@@ -338,7 +339,7 @@ class Scope(object): + + def get_code(self): + str = "" +- if len(self.docstr) > 0: str += '"""'+self.docstr+'"""\n' ++ if len(self.docstr) > 0: str += repr(self.docstr)+'\n' + str += 'class _PyCmplNoType:\n def __getattr__(self,name):\n return None\n' + for sub in self.subscopes: + str += sub.get_code() +@@ -381,7 +382,7 @@ class Class(Scope): + if _DOTTED_NAME_RE.match(s.strip())] + if len(safe_supers) > 0: str += '(%s)' % ','.join(safe_supers) + str += ':\n' +- if len(self.docstr) > 0: str += self.childindent()+'"""'+self.docstr+'"""\n' ++ if len(self.docstr) > 0: str += self.childindent()+repr(self.docstr)+'\n' + if len(self.subscopes) > 0: + for s in self.subscopes: str += s.get_code() + else: +@@ -404,7 +405,7 @@ class Function(Scope): + safe_params = [p for p in safe_params if p] + str = "%sdef %s(%s):\n" % \ + (self.currentindent(),self.name,','.join(safe_params)) +- if len(self.docstr) > 0: str += self.childindent()+'"""'+self.docstr+'"""\n' ++ if len(self.docstr) > 0: str += self.childindent()+repr(self.docstr)+'\n' + str += "%spass\n" % self.childindent() + return str + +diff --git a/runtime/autoload/pythoncomplete.vim b/runtime/autoload/pythoncomplete.vim +index 7614882..5dd6c94 100644 +--- a/runtime/autoload/pythoncomplete.vim ++++ b/runtime/autoload/pythoncomplete.vim +@@ -2,7 +2,7 @@ + " Maintainer: + " Previous Maintainer: Aaron Griffin + " Version: 0.10 +-" Last Updated: 2026 Jun 04 ++" Last Updated: 2026 Jun 21 + " + " Changes + " TODO: +@@ -20,6 +20,7 @@ + " previous code passed buffer-supplied expressions to exec() which + " Python evaluates at definition time, allowing arbitrary code + " execution via crafted def/class headers ++" * use repr() on doc strings to prevent code execution + " + " v 0.9 + " * Fixed docstring parsing for classes and functions +@@ -353,7 +354,7 @@ class Scope(object): + + def get_code(self): + str = "" +- if len(self.docstr) > 0: str += '"""'+self.docstr+'"""\n' ++ if len(self.docstr) > 0: str += repr(self.docstr)+'\n' + str += 'class _PyCmplNoType:\n def __getattr__(self,name):\n return None\n' + for sub in self.subscopes: + str += sub.get_code() +@@ -396,7 +397,7 @@ class Class(Scope): + if _DOTTED_NAME_RE.match(s.strip())] + if len(safe_supers) > 0: str += '(%s)' % ','.join(safe_supers) + str += ':\n' +- if len(self.docstr) > 0: str += self.childindent()+'"""'+self.docstr+'"""\n' ++ if len(self.docstr) > 0: str += self.childindent()+repr(self.docstr)+'\n' + if len(self.subscopes) > 0: + for s in self.subscopes: str += s.get_code() + else: +@@ -419,7 +420,7 @@ class Function(Scope): + safe_params = [p for p in safe_params if p] + str = "%sdef %s(%s):\n" % \ + (self.currentindent(),self.name,','.join(safe_params)) +- if len(self.docstr) > 0: str += self.childindent()+'"""'+self.docstr+'"""\n' ++ if len(self.docstr) > 0: str += self.childindent()+repr(self.docstr)+'\n' + str += "%spass\n" % self.childindent() + return str + +diff --git a/src/testdir/test_plugin_python3complete.vim b/src/testdir/test_plugin_python3complete.vim +index 9a237d0..0500f23 100644 +--- a/src/testdir/test_plugin_python3complete.vim ++++ b/src/testdir/test_plugin_python3complete.vim +@@ -223,4 +223,19 @@ func Test_python3complete_allow_import_on_runs_imports() + \ 'g:pythoncomplete_allow_import=1 did not run the buffer import') + endfunc + ++func Test_python3complete_no_exec_via_class_docstring() ++ " A class-body docstring is emitted verbatim between triple quotes by ++ " get_code() and runs at class-definition time during exec(). A single- ++ " quoted source docstring lets an embedded """ survive doc()'s leading/ ++ " trailing quote strip and break out of the generated literal. ++ let marker = tempname() ++ call s:CompleteAndExpectNoMarker([ ++ \ 'class Foo:', ++ \ ' ''x"""+open("' . marker . '", "w").close()+"""y''', ++ \ ' pass', ++ \ 'Foo.', ++ \ ], marker, ++ \ 'class docstring expression was evaluated during omni-completion') ++endfunc ++ + " vim: shiftwidth=2 sts=2 expandtab diff -Nru vim-9.1.1230/debian/patches/CVE-2026-59856.patch vim-9.1.1230/debian/patches/CVE-2026-59856.patch --- vim-9.1.1230/debian/patches/CVE-2026-59856.patch 1970-01-01 00:00:00.000000000 +0000 +++ vim-9.1.1230/debian/patches/CVE-2026-59856.patch 2026-09-25 20:25:55.000000000 +0000 @@ -0,0 +1,101 @@ +From: Lee Garrett +Date: Fri, 25 Sep 2026 17:26:07 +0200 +Subject: Fix CVE-2026-59856 + +Origin: backport, https://github.com/vim/vim/commit/43afc581a37a35762dd0ef292f038b9dc5680a24 +Forwarded: not-needed +Reviewed-By: Lee Garrett +Last-Update: 2026-09-25 + +patch 9.2.0736: potential command execution in PHP omni-completion +Problem: With PHP omni-completion, a crafted file can potentially + execute arbitrary commands when completing a class member. +Solution: Quote the class name before inserting it into the search() + pattern run via win_execute(). + +This fixes CVE-2026-59856. + +Backport of upstream commit 43afc581a37a35762dd0ef292f038b9dc5680a24. +- src/version.c bump omitted as not relevant to this branch. +--- + runtime/autoload/phpcomplete.vim | 3 ++- + src/testdir/Make_all.mak | 2 ++ + src/testdir/test_plugin_phpcomplete.vim | 35 +++++++++++++++++++++++++++++++++ + 3 files changed, 39 insertions(+), 1 deletion(-) + create mode 100644 src/testdir/test_plugin_phpcomplete.vim + +diff --git a/runtime/autoload/phpcomplete.vim b/runtime/autoload/phpcomplete.vim +index 5b4263a..93f7d8b 100644 +--- a/runtime/autoload/phpcomplete.vim ++++ b/runtime/autoload/phpcomplete.vim +@@ -2082,7 +2082,8 @@ function! phpcomplete#GetClassContentsStructure(file_path, file_lines, class_nam + let result = [] + let popup_id = popup_create(a:file_lines, {'hidden': v:true}) + +- call win_execute(popup_id, 'call search(''\c\(class\|interface\|trait\)\_s\+'.a:class_name.'\(\>\|$\)'')') ++ call win_execute(popup_id, 'call search(' ++ \ . string('\c\(class\|interface\|trait\)\_s\+' . a:class_name . '\(\>\|$\)') . ')') + call win_execute(popup_id, "let cfline = line('.')") + call win_execute(popup_id, "call search('{')") + call win_execute(popup_id, "let endline = line('.')") +diff --git a/src/testdir/Make_all.mak b/src/testdir/Make_all.mak +index 6932188..aa0f093 100644 +--- a/src/testdir/Make_all.mak ++++ b/src/testdir/Make_all.mak +@@ -244,6 +244,7 @@ NEW_TESTS = \ + test_plugin_man \ + test_plugin_matchparen \ + test_plugin_netrw \ ++ test_plugin_phpcomplete \ + test_plugin_python3complete \ + test_plugin_tar \ + test_plugin_termdebug \ +@@ -511,6 +512,7 @@ NEW_TESTS_RES = \ + test_plugin_man.res \ + test_plugin_matchparen.res \ + test_plugin_netrw.res \ ++ test_plugin_phpcomplete.res \ + test_plugin_python3complete.res \ + test_plugin_tar.res \ + test_plugin_termdebug.res \ +diff --git a/src/testdir/test_plugin_phpcomplete.vim b/src/testdir/test_plugin_phpcomplete.vim +new file mode 100644 +index 0000000..7f66be4 +--- /dev/null ++++ b/src/testdir/test_plugin_phpcomplete.vim +@@ -0,0 +1,35 @@ ++" Tests for the PHP omni-completion plugin (runtime/autoload/phpcomplete.vim). ++ ++" A buffer class name is interpolated into a search() pattern run via ++" win_execute(). Without escaping, "'" closes the string and "|" starts a new ++" Ex command, so the name runs as an Ex command during completion. ++func Test_phpcomplete_no_exec_via_class_name() ++ unlet! g:phpcomplete_injected ++ let lines = [' 0, 'no class structure returned') ++ call assert_match('class Foo', result[0].content, ++ \ 'class body missing from returned content') ++ call assert_match('bar', result[0].content, ++ \ 'class member missing from returned content') ++endfunc ++ ++" vim: shiftwidth=2 sts=2 expandtab diff -Nru vim-9.1.1230/debian/patches/CVE-2026-59857.patch vim-9.1.1230/debian/patches/CVE-2026-59857.patch --- vim-9.1.1230/debian/patches/CVE-2026-59857.patch 1970-01-01 00:00:00.000000000 +0000 +++ vim-9.1.1230/debian/patches/CVE-2026-59857.patch 2026-09-25 20:25:55.000000000 +0000 @@ -0,0 +1,109 @@ +From: Lee Garrett +Date: Fri, 25 Sep 2026 17:26:07 +0200 +Subject: Fix CVE-2026-59857 + +Origin: backport, https://github.com/vim/vim/commit/d22ff1c955ff87e8273210eae125aab0e85b6c30 +Bug: https://github.com/vim/vim/security/advisories/GHSA-m3hf-xcm3-xhm2 +Forwarded: not-needed +Reviewed-By: Lee Garrett +Last-Update: 2026-09-25 + +patch 9.2.0725: [security]: Stack out-of-bounds write in spell_soundfold_sal() +Problem: [security]: A crafted spell file with non-collapsing SAL rules + can make soundfold() write one byte past the end of the + MAXWLEN result buffer. This is the same class of + out-of-bounds write as GHSA-q8mh-6qm3-25g4 (fixed in 9.2.0698 + for the SOFO branch), found while auditing the surrounding + code. +Solution: Bound the single-byte SAL result writes and the terminating + NUL to MAXWLEN - 1, matching the SOFO branch. + +The single-byte branch of spell_soundfold_sal() guarded its writes with +"reslen < MAXWLEN", allowing reslen to reach MAXWLEN (254). The trailing +"res[reslen] = NUL" then wrote at index 254 of the 254-byte stack buffer +res[MAXWLEN], an off-by-one out-of-bounds write. Input is case-folded to +about 253 characters, so a 253-character argument together with a SAL map +that does not collapse (collapse_result false) reaches the boundary. + +Related to previous issue +[GHSA-q8mh-6qm3-25g4](https://github.com/vim/vim/security/advisories/GHSA-q8mh-6qm3-25g4) +(9.2.0698) + +Github Security Advisory: +https://github.com/vim/vim/security/advisories/GHSA-m3hf-xcm3-xhm2 + +This fixes CVE-2026-59857. + +Backport of upstream commit d22ff1c955ff87e8273210eae125aab0e85b6c30. +- src/version.c bump omitted as not relevant to this branch. +--- + src/spell.c | 6 +++--- + src/testdir/test_spellfile.vim | 24 ++++++++++++++++++++++++ + 2 files changed, 27 insertions(+), 3 deletions(-) + +diff --git a/src/spell.c b/src/spell.c +index f41272b..ec8c3b9 100644 +--- a/src/spell.c ++++ b/src/spell.c +@@ -3516,7 +3516,7 @@ spell_soundfold_sal(slang_T *slang, char_u *inword, char_u *res) + // no '<' rule used + i += k - 1; + z = 0; +- while (*s != NUL && s[1] != NUL && reslen < MAXWLEN) ++ while (*s != NUL && s[1] != NUL && reslen < MAXWLEN - 1) + { + if (reslen == 0 || res[reslen - 1] != *s) + res[reslen++] = *s; +@@ -3526,7 +3526,7 @@ spell_soundfold_sal(slang_T *slang, char_u *inword, char_u *res) + c = *s; + if (strstr((char *)pf, "^^") != NULL) + { +- if (c != NUL) ++ if (c != NUL && reslen < MAXWLEN - 1) + res[reslen++] = c; + STRMOVE(word, word + i + 1); + i = 0; +@@ -3545,7 +3545,7 @@ spell_soundfold_sal(slang_T *slang, char_u *inword, char_u *res) + + if (z0 == 0) + { +- if (k && !p0 && reslen < MAXWLEN && c != NUL ++ if (k && !p0 && reslen < MAXWLEN - 1 && c != NUL + && (!slang->sl_collapse || reslen == 0 + || res[reslen - 1] != c)) + // condense only double letters +diff --git a/src/testdir/test_spellfile.vim b/src/testdir/test_spellfile.vim +index 49ce9b3..af84e77 100644 +--- a/src/testdir/test_spellfile.vim ++++ b/src/testdir/test_spellfile.vim +@@ -390,6 +390,30 @@ func Test_spellfile_format_error() + let &rtp = save_rtp + endfunc + ++" An over-length soundfold() argument must not overflow the MAXWLEN result ++" buffer in the single-byte branch of spell_soundfold_sal(). ++func Test_spellfile_soundfold_sal_overflow() ++ let save_enc = &encoding ++ set encoding=latin1 ++ " A SAL map that appends without collapsing, so the result is not shorter ++ " than the input. ++ call writefile(['SET ISO8859-1', 'SAL collapse_result false', ++ \ 'SAL a aaaa', 'SAL b bbbb'], 'Xsal.aff') ++ call writefile(['2', 'hello', 'world'], 'Xsal.dic') ++ mkspell! Xsal Xsal ++ set spl=Xsal.latin1.spl spell ++ ++ " 253 input characters hit the buffer boundary; the result must not exceed ++ " MAXWLEN - 1. ++ call assert_true(strlen(soundfold(repeat('a', 253))) <= 253) ++ ++ set nospell spl& spelllang& ++ call delete('Xsal.aff') ++ call delete('Xsal.dic') ++ call delete('Xsal.latin1.spl') ++ let &encoding = save_enc ++endfunc ++ + " Test for format errors in suggest file + func Test_sugfile_format_error() + let save_rtp = &rtp diff -Nru vim-9.1.1230/debian/patches/CVE-2026-59858.patch vim-9.1.1230/debian/patches/CVE-2026-59858.patch --- vim-9.1.1230/debian/patches/CVE-2026-59858.patch 1970-01-01 00:00:00.000000000 +0000 +++ vim-9.1.1230/debian/patches/CVE-2026-59858.patch 2026-09-25 20:25:55.000000000 +0000 @@ -0,0 +1,133 @@ +From: Lee Garrett +Date: Fri, 25 Sep 2026 17:26:07 +0200 +Subject: Fix CVE-2026-59858 + +Origin: backport, https://github.com/vim/vim/commit/6b611b0d15603c52ebdad17172b0232b4f65704e +Bug: https://github.com/vim/vim/security/advisories/GHSA-mf92-v4xw-j45x +Forwarded: not-needed +Reviewed-By: Lee Garrett +Last-Update: 2026-09-25 + +patch 9.2.0735: [security]: arbitrary Ex command execution during C omni-completion +Problem: [security]: With C omni-completion, a crafted tags file can execute + arbitrary Ex commands when completing a struct/union member + (cipher-creator) +Solution: Escape the type field before inserting it into the :vimgrep + pattern so it cannot close the pattern and start a new command + (Hirohito Higashi). + +Github Security Advisory: +https://github.com/vim/vim/security/advisories/GHSA-mf92-v4xw-j45x + +This fixes CVE-2026-59858. + +Backport of upstream commit 6b611b0d15603c52ebdad17172b0232b4f65704e. +- src/version.c bump omitted as not relevant to this branch. +--- + runtime/autoload/ccomplete.vim | 2 +- + src/testdir/Make_all.mak | 2 ++ + src/testdir/test_plugin_ccomplete.vim | 62 +++++++++++++++++++++++++++++++++++ + 3 files changed, 65 insertions(+), 1 deletion(-) + create mode 100644 src/testdir/test_plugin_ccomplete.vim + +diff --git a/runtime/autoload/ccomplete.vim b/runtime/autoload/ccomplete.vim +index 355f724..eda2185 100644 +--- a/runtime/autoload/ccomplete.vim ++++ b/runtime/autoload/ccomplete.vim +@@ -560,7 +560,7 @@ def StructMembers( # {{{1 + if !cached + while 1 + execute 'silent! keepjumps noautocmd ' +- .. n .. 'vimgrep ' .. '/\t' .. typename .. '\(\t\|$\)/j ' ++ .. n .. 'vimgrep ' .. '/\t' .. escape(typename, '/\') .. '\(\t\|$\)/j ' + .. fnames + + qflist = getqflist() +diff --git a/src/testdir/Make_all.mak b/src/testdir/Make_all.mak +index aa0f093..5f7fd2f 100644 +--- a/src/testdir/Make_all.mak ++++ b/src/testdir/Make_all.mak +@@ -239,6 +239,7 @@ NEW_TESTS = \ + test_partial \ + test_paste \ + test_perl \ ++ test_plugin_ccomplete \ + test_plugin_comment \ + test_plugin_glvs \ + test_plugin_man \ +@@ -507,6 +508,7 @@ NEW_TESTS_RES = \ + test_partial.res \ + test_paste.res \ + test_perl.res \ ++ test_plugin_ccomplete.res \ + test_plugin_comment.res \ + test_plugin_glvs.res \ + test_plugin_man.res \ +diff --git a/src/testdir/test_plugin_ccomplete.vim b/src/testdir/test_plugin_ccomplete.vim +new file mode 100644 +index 0000000..a635bd5 +--- /dev/null ++++ b/src/testdir/test_plugin_ccomplete.vim +@@ -0,0 +1,62 @@ ++" Tests for the C omni-completion plugin (runtime/autoload/ccomplete.vim). ++ ++func s:WriteTags(lines) ++ " Mark unsorted so lookup is a linear scan regardless of entry order. ++ let tagsfile = tempname() ++ call writefile(["!_TAG_FILE_SORTED\t0\t/0/"] + a:lines, tagsfile) ++ return tagsfile ++endfunc ++ ++" A crafted typeref field is interpolated into the :vimgrep pattern in ++" StructMembers(). Without escaping, "/" closes the pattern and "|" starts a ++" new Ex command, so the field runs as an Ex command during completion. ++func Test_ccomplete_no_exec_via_typeref() ++ unlet! g:ccomplete_injected ++ let tagsfile = s:WriteTags([ ++ \ "myvar\tmain.c\t/^x$/;\"\tv\ttyperef:x/|let g:ccomplete_injected = 1|\"", ++ \ ]) ++ ++ let save_tags = &tags ++ let &tags = tagsfile ++ ++ new ++ call ccomplete#Complete(1, '') ++ call ccomplete#Complete(0, 'myvar.x') ++ ++ call assert_false(exists('g:ccomplete_injected'), ++ \ 'typeref field was executed as an Ex command during omni-completion') ++ ++ bwipe! ++ let &tags = save_tags ++ unlet! g:ccomplete_injected ++endfunc ++ ++" A legitimate typeref must still drive struct-member completion: escaping the ++" field value must not break the normal path. ++func Test_ccomplete_typeref_completion_still_works() ++ let tagsfile = s:WriteTags([ ++ \ "myvar\tmain.c\t/^x$/;\"\tv\ttyperef:struct:mystruct", ++ \ "alpha\tmain.c\t/^x$/;\"\tm\tstruct:mystruct", ++ \ "beta\tmain.c\t/^x$/;\"\tm\tstruct:mystruct", ++ \ ]) ++ ++ let save_tags = &tags ++ let &tags = tagsfile ++ ++ new ++ call ccomplete#Complete(1, '') ++ let items = ccomplete#Complete(0, 'myvar.') ++ ++ call assert_equal(type([]), type(items), ++ \ 'ccomplete#Complete did not return a list') ++ let names = map(copy(items), 'v:val.word') ++ call assert_true(index(names, 'alpha') >= 0, ++ \ 'struct member "alpha" missing from completion: ' . string(names)) ++ call assert_true(index(names, 'beta') >= 0, ++ \ 'struct member "beta" missing from completion: ' . string(names)) ++ ++ bwipe! ++ let &tags = save_tags ++endfunc ++ ++" vim: shiftwidth=2 sts=2 expandtab diff -Nru vim-9.1.1230/debian/patches/CVE-2026-73072+regression-fix.patch vim-9.1.1230/debian/patches/CVE-2026-73072+regression-fix.patch --- vim-9.1.1230/debian/patches/CVE-2026-73072+regression-fix.patch 1970-01-01 00:00:00.000000000 +0000 +++ vim-9.1.1230/debian/patches/CVE-2026-73072+regression-fix.patch 2026-09-25 20:25:55.000000000 +0000 @@ -0,0 +1,165 @@ +From: Lee Garrett +Date: Fri, 25 Sep 2026 17:56:29 +0200 +Subject: Fix CVE-2026-73072 + fix regression + +Origin: backport, https://github.com/vim/vim/commit/05c41c922309c7a11b6ec2f124be66551c90d66a + https://github.com/vim/vim/commit/5a7ce2733a2d9d9fccad4dd48047546000d9994d +Bug: https://github.com/vim/vim/security/advisories/GHSA-9jqx-hgpr-6v64 +Forwarded: not-needed +Reviewed-By: Lee Garrett +Last-Update: 2026-09-25 + +Changed from upstream: +- src/version.c bumps omitted as not relevant to this branch. + +patch 9.2.0846: [security]: heap buffer overflow in set_sofo() + +Problem: [security]: heap buffer overflow in set_sofo() + (Yazan Balawneh) +Solution: Reset sl_sal_first (Yasuhiro Matsumoto). + +A crafted spell file with an empty SN_SAL section before an SN_SOFO +section reaches set_sofo() with sl_sal_first[] already set to -1 by +set_sal_first(). The counting loop then under-counts colliding +multi-byte "from" characters, allocates an undersized list and writes +past its end. + +Github Security Advisory: +https://github.com/vim/vim/security/advisories/GHSA-9jqx-hgpr-6v64 + +patch 9.2.0854: memory leak when reading a spell file with SN_SAL and SN_SOFO + +Problem: Memory leak when a spell file has an SN_SAL section before an + SN_SOFO section: set_sofo() reuses sl_sal without freeing the + salitem_T entries left by read_sal_section() (after v9.2.0846). +Solution: Factor the SAL free loop into free_sal_items() and call it + before set_sofo() reuses sl_sal. + +closes: #20836 + +Supported by AI. +--- + src/proto/spell.pro | 1 + + src/spell.c | 39 ++++++++++++++++++++++++++------------- + src/spellfile.c | 6 +++++- + src/testdir/test_spellfile.vim | 5 +++++ + 4 files changed, 37 insertions(+), 14 deletions(-) + +diff --git a/src/proto/spell.pro b/src/proto/spell.pro +index a68de8f..a7e547b 100644 +--- a/src/proto/spell.pro ++++ b/src/proto/spell.pro +@@ -11,6 +11,7 @@ void spell_cat_line(char_u *buf, char_u *line, int maxlen); + char_u *spell_enc(void); + slang_T *slang_alloc(char_u *lang); + void slang_free(slang_T *lp); ++void free_sal_items(garray_T *gap); + void slang_clear(slang_T *lp); + void slang_clear_sug(slang_T *lp); + void count_common_word(slang_T *lp, char_u *word, int len, int count); +diff --git a/src/spell.c b/src/spell.c +index ec8c3b9..ebf2231 100644 +--- a/src/spell.c ++++ b/src/spell.c +@@ -1754,6 +1754,30 @@ slang_free(slang_T *lp) + vim_free(lp); + } + ++ ++/* ++ * Free the salitem_T entries in a "sl_sal" garray (the SN_SAL form) and ++ * clear the garray. Used by slang_clear() and when set_sofo() reuses ++ * sl_sal for the SN_SOFO form. ++ */ ++ void ++free_sal_items(garray_T *gap) ++{ ++ salitem_T *smp; ++ ++ while (gap->ga_len > 0) ++ { ++ smp = &((salitem_T *)gap->ga_data)[--gap->ga_len]; ++ vim_free(smp->sm_lead); ++ // Don't free sm_oneof and sm_rules, they point into sm_lead. ++ vim_free(smp->sm_to); ++ vim_free(smp->sm_lead_w); ++ vim_free(smp->sm_oneof_w); ++ vim_free(smp->sm_to_w); ++ } ++ ga_clear(gap); ++} ++ + /* + * Clear an slang_T so that the file can be reloaded. + */ +@@ -1762,7 +1786,6 @@ slang_clear(slang_T *lp) + { + garray_T *gap; + fromto_T *ftp; +- salitem_T *smp; + int i; + int round; + +@@ -1794,20 +1817,10 @@ slang_clear(slang_T *lp) + // SOFOFROM and SOFOTO items: free lists of wide characters. + for (i = 0; i < gap->ga_len; ++i) + vim_free(((int **)gap->ga_data)[i]); ++ ga_clear(gap); + } + else +- // SAL items: free salitem_T items +- while (gap->ga_len > 0) +- { +- smp = &((salitem_T *)gap->ga_data)[--gap->ga_len]; +- vim_free(smp->sm_lead); +- // Don't free sm_oneof and sm_rules, they point into sm_lead. +- vim_free(smp->sm_to); +- vim_free(smp->sm_lead_w); +- vim_free(smp->sm_oneof_w); +- vim_free(smp->sm_to_w); +- } +- ga_clear(gap); ++ free_sal_items(gap); + + for (i = 0; i < lp->sl_prefixcnt; ++i) + vim_regfree(lp->sl_prefprog[i]); +diff --git a/src/spellfile.c b/src/spellfile.c +index 2f6e59e..96f3b9b 100644 +--- a/src/spellfile.c ++++ b/src/spellfile.c +@@ -1427,6 +1427,8 @@ set_sofo(slang_T *lp, char_u *from, char_u *to) + + if (has_mbyte) + { ++ free_sal_items(&lp->sl_sal); ++ + // Use "sl_sal" as an array with 256 pointers to a list of wide + // characters. The index is the low byte of the character. + // The list contains from-to pairs with a terminating NUL. +@@ -1439,7 +1441,9 @@ set_sofo(slang_T *lp, char_u *from, char_u *to) + gap->ga_len = 256; + + // First count the number of items for each list. Temporarily use +- // sl_sal_first[] for this. ++ // sl_sal_first[] for this. Reset it first: a preceding SN_SAL section ++ // may have set the entries to -1 via set_sal_first(). ++ vim_memset(lp->sl_sal_first, 0, sizeof(salfirst_T) * 256); + for (p = from, s = to; *p != NUL && *s != NUL; ) + { + c = mb_cptr2char_adv(&p); +diff --git a/src/testdir/test_spellfile.vim b/src/testdir/test_spellfile.vim +index af84e77..91273e7 100644 +--- a/src/testdir/test_spellfile.vim ++++ b/src/testdir/test_spellfile.vim +@@ -322,6 +322,11 @@ func Test_spellfile_format_error() + " SN_SOFO: multi-byte characters in sofofrom and sofoto + call Spellfile_Test(0z0600000000080002CF810002CF82FF000000000000000000000000, '') + ++ " SN_SAL (empty) followed by SN_SOFO with two multi-byte 'from' characters ++ " sharing the same low byte. A preceding SN_SAL poisons sl_sal_first[], so ++ " without a reset set_sofo() under-counts and writes out of bounds. ++ call Spellfile_Test(0z05000000000300000006000000000A0004CAABCEAB00024142FF000000000000000000000000, '') ++ + " SN_COMPOUND: compmax is less than 2 + call Spellfile_Test(0z08000000000101, 'E759:') + diff -Nru vim-9.1.1230/debian/patches/CVE-2026-73073.patch vim-9.1.1230/debian/patches/CVE-2026-73073.patch --- vim-9.1.1230/debian/patches/CVE-2026-73073.patch 1970-01-01 00:00:00.000000000 +0000 +++ vim-9.1.1230/debian/patches/CVE-2026-73073.patch 2026-09-25 20:25:55.000000000 +0000 @@ -0,0 +1,99 @@ +From: Lee Garrett +Date: Fri, 25 Sep 2026 18:19:18 +0200 +Subject: Fix CVE-2026-73073 + +Origin: backport, https://github.com/vim/vim/commit/2f628d8104958fa7421664f792ca6d4f7a39a10f +Bug: https://github.com/vim/vim/security/advisories/GHSA-cx73-phcg-3j5g +Forwarded: not-needed +Reviewed-By: Lee Garrett +Last-Update: 2026-09-25 + +Changed from upstream: +- src/testdir/test_plugin_ccomplete.vim: added "source check.vim", as this + branch's runtest.vim does not source it automatically (needed for + CheckUnix). +- src/version.c bump omitted as not relevant to this branch. + +patch 9.2.0845: [security]: arbitrary Ex command execution during C omni-completion + +Problem: [security]: arbitrary Ex command execution during C + omni-completion (Threonine) +Solution: Match tags typeref literally to block Ex command injection + (Yasuhiro Matsumoto). + +Escaping only "/" and "\" left the typeref able to break out of the +:vimgrep pattern without a "/": an unclosed "[" makes vimgrep's pattern +skipping fail, and the parser then treats a following "|" as a command +separator, so the tag value runs as Ex commands during C omni-completion. +Match the field literally with \V so no regex metacharacter can affect +pattern parsing. + +Github Security Advisory: +https://github.com/vim/vim/security/advisories/GHSA-cx73-phcg-3j5g +--- + runtime/autoload/ccomplete.vim | 5 ++++- + src/testdir/test_plugin_ccomplete.vim | 28 ++++++++++++++++++++++++++++ + 2 files changed, 32 insertions(+), 1 deletion(-) + +diff --git a/runtime/autoload/ccomplete.vim b/runtime/autoload/ccomplete.vim +index eda2185..5ab75a3 100644 +--- a/runtime/autoload/ccomplete.vim ++++ b/runtime/autoload/ccomplete.vim +@@ -559,8 +559,11 @@ def StructMembers( # {{{1 + endif + if !cached + while 1 ++ # Match "typename" literally (\V): escaping alone is not enough, as e.g. ++ # an unclosed "[" makes vimgrep's pattern skipping fail and the rest of ++ # the tag value is then parsed as Ex commands. + execute 'silent! keepjumps noautocmd ' +- .. n .. 'vimgrep ' .. '/\t' .. escape(typename, '/\') .. '\(\t\|$\)/j ' ++ .. n .. 'vimgrep ' .. '/\t\V' .. escape(typename, '/\') .. '\m\(\t\|$\)/j ' + .. fnames + + qflist = getqflist() +diff --git a/src/testdir/test_plugin_ccomplete.vim b/src/testdir/test_plugin_ccomplete.vim +index a635bd5..71fc6d2 100644 +--- a/src/testdir/test_plugin_ccomplete.vim ++++ b/src/testdir/test_plugin_ccomplete.vim +@@ -1,5 +1,7 @@ + " Tests for the C omni-completion plugin (runtime/autoload/ccomplete.vim). + ++source check.vim ++ + func s:WriteTags(lines) + " Mark unsorted so lookup is a linear scan regardless of entry order. + let tagsfile = tempname() +@@ -31,6 +33,32 @@ func Test_ccomplete_no_exec_via_typeref() + unlet! g:ccomplete_injected + endfunc + ++" Escaping "/" and "\" is not enough: with no "/" in the payload, an unclosed ++" "[" makes vimgrep's pattern skipping fail, and the command parser then treats ++" the first "|" as a command separator. The typeref must be matched literally. ++func Test_ccomplete_no_exec_via_typeref_bracket() ++ CheckUnix ++ let sentinel = tempname() ++ call delete(sentinel) ++ let tagsfile = s:WriteTags([ ++ \ "myvar\tmain.c\t/^x$/;\"\tv\ttyperef:struct:[|call system('touch " .. sentinel .. "')|####", ++ \ ]) ++ ++ let save_tags = &tags ++ let &tags = tagsfile ++ ++ new ++ call ccomplete#Complete(1, '') ++ call ccomplete#Complete(0, 'myvar.x') ++ ++ call assert_false(filereadable(sentinel), ++ \ 'typeref field was executed as an Ex command during omni-completion') ++ ++ bwipe! ++ let &tags = save_tags ++ call delete(sentinel) ++endfunc ++ + " A legitimate typeref must still drive struct-member completion: escaping the + " field value must not break the normal path. + func Test_ccomplete_typeref_completion_still_works() diff -Nru vim-9.1.1230/debian/patches/CVE-2026-73076.patch vim-9.1.1230/debian/patches/CVE-2026-73076.patch --- vim-9.1.1230/debian/patches/CVE-2026-73076.patch 1970-01-01 00:00:00.000000000 +0000 +++ vim-9.1.1230/debian/patches/CVE-2026-73076.patch 2026-09-25 20:25:55.000000000 +0000 @@ -0,0 +1,269 @@ +From: Lee Garrett +Date: Fri, 25 Sep 2026 18:22:11 +0200 +Subject: Fix CVE-2026-73076 + +Origin: backport, https://github.com/vim/vim/commit/581a2f3ac9c6f96a26324f6b2c8c11415fd0d452 +Bug: https://github.com/vim/vim/security/advisories/GHSA-r22p-fhw4-84p2 +Forwarded: not-needed +Reviewed-By: Lee Garrett +Last-Update: 2026-09-25 + +Changed from upstream: +- runtime/autoload/vimball.vim: the .VimballRecord filename check is added + as a standalone if block, since the earlier path traversal and strange + filename checks it chains onto upstream (9.2.0358, 9.2.0421) are not + present on this branch. The "Date:" header hunk is omitted. +- src/testdir/test_plugin_vimball.vim: added, as it does not exist on this + branch. Only Test_vimball_basic() and the new + Test_vimball_VimballRecord_filenames() are included; the path traversal and + evil filename tests depend on the fixes named above. +- src/testdir/Make_all.mak: register test_plugin_vimball. +- src/version.c bump omitted as not relevant to this branch. + +patch 9.2.0847: [security]: vimball: code execution via .VimballRecord file + +Problem: [security]: vimball: code execution via .VimballRecord file + (tdjackey) +Solution: Forbid arbitrary commands, fix broken directory deletion code, + refactor code + +Github Security Advisory: +https://github.com/vim/vim/security/advisories/GHSA-r22p-fhw4-84p2 +--- + runtime/autoload/vimball.vim | 64 ++++++++++++++++------------ + src/testdir/Make_all.mak | 2 + + src/testdir/test_plugin_vimball.vim | 84 +++++++++++++++++++++++++++++++++++++ + 3 files changed, 124 insertions(+), 26 deletions(-) + create mode 100644 src/testdir/test_plugin_vimball.vim + +diff --git a/runtime/autoload/vimball.vim b/runtime/autoload/vimball.vim +index 6456984..6783f2e 100644 +--- a/runtime/autoload/vimball.vim ++++ b/runtime/autoload/vimball.vim +@@ -18,9 +18,9 @@ if &cp || exists("g:loaded_vimball") + finish + endif + let g:loaded_vimball = "v37" +-if v:version < 702 ++if v:version < 900 + echohl WarningMsg +- echo "***warning*** this version of vimball needs vim 7.2" ++ echo "***warning*** this version of vimball needs vim 9.0" + echohl Normal + finish + endif +@@ -270,6 +270,13 @@ fun! vimball#Vimball(really,...) + let fenc = substitute(getline(linenr+1),'^\d\+\s*\(\S\{-}\)$','\1','') + let filecnt = filecnt + 1 + " call Decho("fname<".fname."> fsize=".fsize." filecnt=".filecnt. " fenc=".fenc) ++ if fname =~? '\%(^\|/\)\.VimballRecord$' ++ echomsg "(Vimball) Forbidding .VimballRecord filename, aborting..." ++ exe "tabn ".curtabnr ++ bw! Vimball ++ call s:ChgDir(curdir) ++ return ++ endif + + if a:really + echomsg "extracted <".fname.">: ".fsize." lines" +@@ -309,7 +316,7 @@ fun! vimball#Vimball(really,...) + else + call mkdir(dirname) + endif +- call s:RecordInVar(home,"rmdir('".dirname."')") ++ call s:RecordDirInVar(dirname) + endif + endwhile + endif +@@ -343,7 +350,7 @@ fun! vimball#Vimball(really,...) + exe "silent w! ".fnameescape(fnamepath) + endif + echo "wrote ".fnameescape(fnamepath) +- call s:RecordInVar(home,"call delete('".fnamepath."')") ++ call s:RecordInVar(fnamepath) + endif + + " return to tab with vimball +@@ -464,11 +471,17 @@ fun! vimball#RmVimball(...) + let s:VBRstring= substitute(exestring,'call delete(','','g') + let s:VBRstring= substitute(s:VBRstring,"[')]",'','g') + " call Decho("exe ".exestring) +- sil! keepalt keepjumps exe exestring +- sil! keepalt keepjumps d +- let exestring= strlen(substitute(exestring,'call delete(.\{-})|\=',"D","g")) +-" call Decho("exestring<".exestring.">") +- echomsg "removed ".exestring." files" ++ let nr_files= 0 ++ for line in split(exestring, '|') ++ if line !~ '^call delete(''[^'']\{-}''\(,"d"\)\?)$' ++ echomsg "ignoring .VimballRecord entry: " line ++ else ++ sil! keepalt keepjumps exe line ++ let nr_files+= 1 ++ endif ++ endfor ++ sil! keepalt keepjumps d ++ echomsg "removed ".nr_files." files" + else + let s:VBRstring= '' + let curfile = substitute(curfile,'\.vmb','','') +@@ -622,21 +635,20 @@ fun! s:ChgDir(newdir) + endfun + + " --------------------------------------------------------------------- +-" s:RecordInVar: record a un-vimball command in the .VimballRecord file {{{2 +-fun! s:RecordInVar(home,cmd) +-" call Dfunc("RecordInVar(home<".a:home."> cmd<".a:cmd.">)") +- if a:cmd =~ '^rmdir' +-" if !exists("s:recorddir") +-" let s:recorddir= substitute(a:cmd,'^rmdir',"call s:Rmdir",'') +-" else +-" let s:recorddir= s:recorddir."|".substitute(a:cmd,'^rmdir',"call s:Rmdir",'') +-" endif +- elseif !exists("s:recordfile") +- let s:recordfile= a:cmd +- else +- let s:recordfile= s:recordfile."|".a:cmd ++" s:RecordInVar: record a un-vimball file deletion in the .VimballRecord file {{{2 ++fun! s:RecordInVar(file) ++ if !exists("s:recordfile") ++ let s:recordfile=[] ++ endif ++ call add(s:recordfile, $'call delete({string(a:file)})') ++endfun ++ ++" s:RecordDirInVar: record a un-vimball dir deletion in the .VimballRecord file {{{2 ++fun! s:RecordDirInVar(dir) ++ if !exists("s:recorddir") ++ let s:recorddir = [] + endif +-" call Dret("RecordInVar : s:recordfile<".(exists("s:recordfile")? s:recordfile : "")."> s:recorddir<".(exists("s:recorddir")? s:recorddir : "").">") ++ call add(s:recorddir, $'call delete({string(a:dir)},"d")') + endfun + + " --------------------------------------------------------------------- +@@ -660,11 +672,11 @@ fun! s:RecordInFile(home) + setlocal ma + $ + if exists("s:recordfile") && exists("s:recorddir") +- let cmd= cmd.s:recordfile."|".s:recorddir ++ let cmd= cmd.join(s:recordfile, '|')."|".join(s:recorddir, '|') + elseif exists("s:recorddir") +- let cmd= cmd.s:recorddir ++ let cmd= cmd.join(s:recorddir, '|') + elseif exists("s:recordfile") +- let cmd= cmd.s:recordfile ++ let cmd= cmd.join(s:recordfile, '|') + else + " call Dret("s:RecordInFile : neither recordfile nor recorddir exist") + return +diff --git a/src/testdir/Make_all.mak b/src/testdir/Make_all.mak +index 5f7fd2f..e53b90b 100644 +--- a/src/testdir/Make_all.mak ++++ b/src/testdir/Make_all.mak +@@ -250,6 +250,7 @@ NEW_TESTS = \ + test_plugin_tar \ + test_plugin_termdebug \ + test_plugin_tohtml \ ++ test_plugin_vimball \ + test_plugin_zip \ + test_plus_arg_edit \ + test_popup \ +@@ -519,6 +520,7 @@ NEW_TESTS_RES = \ + test_plugin_tar.res \ + test_plugin_termdebug.res \ + test_plugin_tohtml.res \ ++ test_plugin_vimball.res \ + test_plugin_zip.res \ + test_plus_arg_edit.res \ + test_popup.res \ +diff --git a/src/testdir/test_plugin_vimball.vim b/src/testdir/test_plugin_vimball.vim +new file mode 100644 +index 0000000..1c68bf2 +--- /dev/null ++++ b/src/testdir/test_plugin_vimball.vim +@@ -0,0 +1,84 @@ ++" Test for the vimball plugin ++ ++let s:testdir = expand("