Backport upstream fix for CVE-2026-73076 which addresses code
execution via the .VimballRecord file in the vimball plugin.
The patch adds filename validation to reject .VimballRecord as
a vimball filename, sanitizes stored commands using structured
call delete() calls with safe quoting, and validates entries
in RmVimball before execution. Adapted for Vim 8.0 legacy
script syntax (no Vim9 string interpolation or method syntax).
CVE: CVE-2026-73076
Upstream patches:
- 581a2f3ac9.patch
Resolves: RHEL-242118
This commit was backported by Ymir, a Red Hat Enterprise Linux software maintenance AI agent.
Assisted-by: Ymir
235 lines
7.7 KiB
Diff
235 lines
7.7 KiB
Diff
From 3e8ecbd202b6121055d4cc8d05fea6aa4d05e96f Mon Sep 17 00:00:00 2001
|
|
From: Christian Brabandt <cb@256bit.org>
|
|
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 <cb@256bit.org>
|
|
---
|
|
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("\<cr>", "t")
|
|
+ unlet mess
|
|
+ let mess = split(execute(':mess'), '\n')
|
|
+ call assert_equal('extracted <XVimball/Xtest.txt>: 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("\<cr>", "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
|