diff --git a/0001-patch-9.2.0847-security-vimball-code-execution-via-V.patch b/0001-patch-9.2.0847-security-vimball-code-execution-via-V.patch new file mode 100644 index 00000000..5a148448 --- /dev/null +++ b/0001-patch-9.2.0847-security-vimball-code-execution-via-V.patch @@ -0,0 +1,234 @@ +From 3e8ecbd202b6121055d4cc8d05fea6aa4d05e96f Mon Sep 17 00:00:00 2001 +From: Christian Brabandt +Date: Fri, 24 Jul 2026 17:43:51 +0200 +Subject: [PATCH] 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 + +Signed-off-by: Christian Brabandt +--- + runtime/autoload/vimball.vim | 60 ++++++++++++--------- + src/testdir/Make_all.mak | 1 + + src/testdir/test_plugin_vimball.vim | 84 +++++++++++++++++++++++++++++ + 3 files changed, 121 insertions(+), 24 deletions(-) + create mode 100644 src/testdir/test_plugin_vimball.vim + +diff --git a/runtime/autoload/vimball.vim b/runtime/autoload/vimball.vim +index 9c7dcbd..137c61a 100644 +--- a/runtime/autoload/vimball.vim ++++ b/runtime/autoload/vimball.vim +@@ -268,6 +268,14 @@ fun! vimball#Vimball(really,...) + 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 ++ call s:ChgDir(curdir) ++ call vimball#RestoreSettings() ++ return ++ endif ++ + if a:really + echomsg "extracted <".fname.">: ".fsize." lines" + else +@@ -306,7 +314,7 @@ fun! vimball#Vimball(really,...) + else + call mkdir(dirname) + endif +- call s:RecordInVar(home,"rmdir('".dirname."')") ++ call s:RecordDirInVar(dirname) + endif + endwhile + endif +@@ -340,7 +348,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 +@@ -460,12 +468,17 @@ fun! vimball#RmVimball(...) + endif + let s:VBRstring= substitute(exestring,'call delete(','','g') + let s:VBRstring= substitute(s:VBRstring,"[')]",'','g') +-" call Decho("exe ".exestring) +- sil! keepalt keepjumps exe exestring ++ 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 +- let exestring= strlen(substitute(exestring,'call delete(.\{-})|\=',"D","g")) +-" call Decho("exestring<".exestring.">") +- echomsg "removed ".exestring." files" ++ echomsg "removed ".nr_files." files" + else + let s:VBRstring= '' + let curfile = substitute(curfile,'\.vmb','','') +@@ -599,21 +612,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 + + " --------------------------------------------------------------------- +@@ -637,11 +649,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 52e9c0b..81291fd 100644 +--- a/src/testdir/Make_all.mak ++++ b/src/testdir/Make_all.mak +@@ -148,6 +148,7 @@ NEW_TESTS = test_arabic.res \ + test_plugin_ccomplete.res \ + test_plugin_netrw.res \ + test_plugin_python3complete.res \ ++ test_plugin_vimball.res \ + test_plus_arg_edit.res \ + test_preview.res \ + test_profile.res \ +diff --git a/src/testdir/test_plugin_vimball.vim b/src/testdir/test_plugin_vimball.vim +new file mode 100644 +index 0000000..39f2b0c +--- /dev/null ++++ b/src/testdir/test_plugin_vimball.vim +@@ -0,0 +1,84 @@ ++" Test for the vimball plugin ++ ++let s:testdir = fnamemodify(resolve(expand("%:p")), ":h") ++let s:default_vimball = ['" Vimball Archiver by Charles E. Campbell', ++ \ 'UseVimball', ++ \ 'finish', ++ \ 'XVimball/Xtest.txt [[[1', ++ \ '2', ++ \ 'Hello Vimball', ++ \ '123'] ++ ++func SetUp() ++ ru plugin/vimballPlugin.vim ++ let g:vimball_home = s:testdir ++endfunc ++ ++func TearDown() ++ call delete('Xtest.vmb') ++ call delete('.VimballRecord') ++endfunc ++ ++func s:setup() ++ call mkdir('XVimball', 'p') ++ call writefile(['Hello Vimball', '123'], 'XVimball/Xtest.txt') ++endfunc ++ ++func s:teardown() ++ call delete('XVimball', 'rf') ++ call delete('Xtest.vmb') ++ bw! Xtest.vmb ++ bw! XVimball/Xtest.txt ++ if bufloaded('.VimballRecord') ++ bw! .VimballRecord ++ endif ++endfunc ++ ++func s:Mkvimball() ++ call s:setup() ++ new ++ 0put ='XVimball/Xtest.txt' ++ $d ++ 1,1MkVimball! Xtest ++ bw! ++endfunc ++ ++func Test_vimball_basic() ++ call s:Mkvimball() ++ call assert_true(filereadable('Xtest.vmb'), 'vimball file should be created') ++ call assert_equal(s:default_vimball, readfile('Xtest.vmb')) ++ ++ call delete('XVimball', 'rf') ++ sp Xtest.vmb ++ let mess = execute(':mess') ++ call assert_match('\*\*\*vimball\*\*\* Source this file to extract it!', mess) ++ so % ++ call feedkeys("\", "t") ++ unlet mess ++ let mess = split(execute(':mess'), '\n') ++ call assert_equal('extracted : 2 lines', mess[-2]) ++ ++ call assert_true(filereadable('XVimball/Xtest.txt'), 'extracted file should exist') ++ call assert_equal(['Hello Vimball', '123'], readfile('XVimball/Xtest.txt')) ++ ++ " Vimball extraction has been recorded ++ call assert_true(filereadable('.VimballRecord')) ++ let record = readfile('.VimballRecord') ++ call assert_equal(1, len(record)) ++ call assert_match('^Xtest.vmb: call delete(''.\{-}'')|call delete(''.\{-}'',"d")$', record[0]) ++ call s:teardown() ++endfunc ++ ++func Test_vimball_VimballRecord_filenames() ++ call s:Mkvimball() ++ call delete('XVimball', 'rf') ++ sp Xtest.vmb ++ 4s#.*\ze\t#.VimballRecord# ++ so % ++ call feedkeys("\", "it") ++ ++ let mess = split(execute(':mess'), '\n')[-1] ++ call assert_match('(Vimball) Forbidding .VimballRecord filename.* aborting\.\.\.', mess) ++ call assert_false(filereadable('.VimballRecord')) ++ call s:teardown() ++endfunc diff --git a/vim.spec b/vim.spec index f3d1880f..673414c8 100644 --- a/vim.spec +++ b/vim.spec @@ -24,7 +24,7 @@ Summary: The VIM editor URL: http://www.vim.org/ Name: vim Version: %{baseversion}.%{patchlevel} -Release: 31%{?dist} +Release: 31%{?dist}.1 License: Vim and MIT Source0: ftp://ftp.vim.org/pub/vim/unix/vim-%{baseversion}-%{patchlevel}.tar.bz2 Source1: vim.sh @@ -215,6 +215,14 @@ Patch3066: 0001-patch-9.2.0653-security-out-of-bounds-write-in-tree_.patch # https://github.com/vim/vim/commit/6b611b0d15603c52ebdad17172b0232b4f65704e # stripped src/version.c hunk, adapted ccomplete.vim fix for vim 8.0 legacy script syntax Patch3067: 0001-patch-9.2.0735-security-arbitrary-Ex-command-executi.patch +# RHEL-242118 CVE-2026-73076 vimball: code execution via .VimballRecord file +# https://redhat.atlassian.net/browse/RHEL-242118 +# https://github.com/vim/vim/commit/581a2f3ac9c6f96a26324f6b2c8c11415fd0d452 +# stripped src/version.c hunk, omitted Date header change, kept v:version < 702 check +# (upstream bumped to 900 but RHEL 8 ships Vim 8.0 = version 800), +# rewrote $'...' string interpolation to legacy Vimscript for Vim 8.0 compatibility, +# do not use