Fix CVE-2026-73076: vimball code execution via .VimballRecord file

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
This commit is contained in:
RHEL Packaging Agent 2026-08-15 09:48:01 +00:00 committed by Zdenek Dohnal
parent dab3bfc2b3
commit 1a7fc60318
2 changed files with 247 additions and 1 deletions

View File

@ -0,0 +1,234 @@
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

View File

@ -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 <script> in expand()
Patch3068: 0001-patch-9.2.0847-security-vimball-code-execution-via-V.patch
# gcc is no longer in buildroot by default
@ -464,6 +472,7 @@ perl -pi -e "s,bin/nawk,bin/awk,g" runtime/tools/mve.awk
%patch -P 3065 -p1 -b .CVE-2026-57455
%patch -P 3066 -p1 -b .CVE-2026-55693
%patch -P 3067 -p1 -b .CVE-2026-59858
%patch -P 3068 -p1 -b .CVE-2026-73076
%build
@ -983,6 +992,9 @@ touch %{buildroot}/%{_datadir}/%{name}/vimfiles/doc/tags
%{_datadir}/icons/locolor/*/apps/*
%changelog
* Sat Aug 15 2026 RHEL Packaging Agent <redhat-ymir-agent@redhat.com> - 2:8.0.1763-31.1
- CVE-2026-73076 vim: code execution via .VimballRecord file
* Tue Jul 14 2026 RHEL Packaging Agent <redhat-ymir-agent@redhat.com> - 2:8.0.1763-31
- RHEL-203873 CVE-2026-59858 vim: arbitrary Ex command execution in
C omni-completion