patch 9.2.0077: [security]: Crash when recovering a corrupted swap file
Commit:
https://github.com/vim/vim/commit/65c1a143c331c886dc28888dd632708f953b4eb3
Author: Christian Brabandt <
c...@256bit.org>
Date: Mon Feb 23 21:42:39 2026 +0000
patch 9.2.0077: [security]: Crash when recovering a corrupted swap file
Problem: memline: a crafted swap files with bogus pe_page_count/pe_bnum
values could cause a multi-GB allocation via mf_get(), and
invalid pe_old_lnum/pe_line_count values could cause a SEGV
when passed to readfile() (ehdgks0627, un3xploitable)
Solution: Add bounds checks on pe_page_count and pe_bnum against
mf_blocknr_max before descending into the block tree, and
validate pe_old_lnum >= 1 and pe_line_count > 0 before calling
readfile().
Github Advisory:
https://github.com/vim/vim/security/advisories/GHSA-r2gw-2x48-jj5p
Signed-off-by: Christian Brabandt <
c...@256bit.org>
diff --git a/Filelist b/Filelist
index b6414223f..4b71b2aee 100644
--- a/Filelist
+++ b/Filelist
@@ -213,6 +213,7 @@ SRC_ALL = \
src/testdir/ru_RU/LC_MESSAGES/__PACKAGE__.po \
src/testdir/runtest.vim \
src/testdir/samples/*.html \
+ src/testdir/samples/*.swp \
src/testdir/samples/*.txt \
src/testdir/samples/*.vim \
src/testdir/samples/evil.zip \
diff --git a/src/memline.c b/src/memline.c
index 604982a90..2d92aadb6 100644
--- a/src/memline.c
+++ b/src/memline.c
@@ -1595,8 +1595,12 @@ ml_recover(int checkext)
if (!cannot_open)
{
line_count = pp->pb_pointer[idx].pe_line_count;
- if (readfile(curbuf->b_ffname, NULL, lnum,
- pp->pb_pointer[idx].pe_old_lnum - 1,
+ linenr_T pe_old_lnum = pp->pb_pointer[idx].pe_old_lnum;
+ // Validate pe_line_count and pe_old_lnum from the
+ // untrusted swap file before passing to readfile().
+ if (line_count <= 0 || pe_old_lnum < 1 ||
+ readfile(curbuf->b_ffname, NULL, lnum,
+ pe_old_lnum - 1,
line_count, NULL, 0) != OK)
cannot_open = TRUE;
else
@@ -1627,6 +1631,27 @@ ml_recover(int checkext)
bnum = pp->pb_pointer[idx].pe_bnum;
line_count = pp->pb_pointer[idx].pe_line_count;
page_count = pp->pb_pointer[idx].pe_page_count;
+ // Validate pe_bnum and pe_page_count from the untrusted
+ // swap file before passing to mf_get(), which uses
+ // page_count to calculate allocation size. A bogus value
+ // (e.g. 0x40000000) would cause a multi-GB allocation.
+ // pe_page_count must be >= 1 and bnum + page_count must
+ // not exceed the number of pages in the swap file.
+ if (page_count < 1
+ || bnum + page_count > mfp->mf_blocknr_max + 1)
+ {
+ ++error;
+ ml_append(lnum++,
+ (char_u *)_("???ILLEGAL BLOCK NUMBER"),
+ (colnr_T)0, TRUE);
+ // Skip this entry and pop back up the stack to keep
+ // recovering whatever else we can.
+ idx = ip->ip_index + 1;
+ bnum = ip->ip_bnum;
+ page_count = 1;
+ --buf->b_ml.ml_stack_top;
+ continue;
+ }
idx = 0;
continue;
}
diff --git a/src/po/vim.pot b/src/po/vim.pot
index 88bca4ef8..1ea35bab2 100644
--- a/src/po/vim.pot
+++ b/src/po/vim.pot
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Vim
"
"Report-Msgid-Bugs-To:
vim...@vim.org
"
-"POT-Creation-Date: 2026-02-19 17:55+0000
"
+"POT-Creation-Date: 2026-02-27 21:04+0000
"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE
"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>
"
"Language-Team: LANGUAGE <
L...@li.org>
"
@@ -1960,6 +1960,9 @@ msgstr ""
msgid "???LINES MISSING"
msgstr ""
+msgid "???ILLEGAL BLOCK NUMBER"
+msgstr ""
+
msgid "???BLOCK MISSING"
msgstr ""
diff --git a/src/testdir/samples/recover-crash1.swp b/src/testdir/samples/recover-crash1.swp
new file mode 100644
index 0000000000000000000000000000000000000000..5fa9a8169c3a75302500c6a755f9971ac06f9f31
GIT binary patch
literal 9765
zcmYc?2=nw+u+%eR00IFJfdFZxRaFQcZdOiya$-(KesKwIC14r-lH3A)kfCXrIjMTZ
z<pl)Pk4le*z-S0iGXz2zjExKpKqe|HDJlpHg;LY-QL{!vU^E0qLtr!nMni!75cv0>
ze7&POhgt|+8EOt5b?;~h45tt%DPT-xV5kRri3#d0MhMLWr5RxKaPl=3{o+7Hvqp^?
z#32C52hx3m*omV~8w~+q2pl2<j1C2jCY{j`7`Y)JJ951~=zOj_=v*{R95e{KGF-zX
zXldz<23i_EYBtqEz=3KejT$seLco^h{&1kV4Wkx}hQP=R0gu$2oP34w{Gyx`1^_3u
BL#Y4&
literal 0
HcmV?d00001
diff --git a/src/testdir/samples/recover-crash2.swp b/src/testdir/samples/recover-crash2.swp
new file mode 100644
index 0000000000000000000000000000000000000000..01ab0e7cc37aebbf14a3712604c4e804747f78d1
GIT binary patch
literal 9682
zcmYc?2=nw+u+%eR00IFJfdFZxRaFQcZdOiya$-(KesKwIC14r-lH3A)kfCXrIjMTZ
z<pl)Pk4g`f5QrWs4jtj{3}rAjGBf}Mg0hmLg0N8N2oH}@?~jJSAP)hSLGHd$=Z%KI
zXb6mkz-S1JhQMeD47m^}DPT-xV5kSWn+fW6MhMLWr5Ru}jl8B$BXj9ytOF@F*^;6d
zr1n1q6p^ZLRQ+fO;0pn0x*~`2CQ%bUXN<}Xt`Jy0xSTWUn$Zv#4FP;1po>qEIJsux
zl;KeI4u|-tFx5kVg@KFeW)2Ah!IP6i!lk2*91Vfd5Eu;s5<_6f4}y^BU)n0CLYs)T
d4jeU|Y9TN}TSRE#=aHI|ldlk-UzC%=003Dli_ZW6
literal 0
HcmV?d00001
diff --git a/src/testdir/test_recover.vim b/src/testdir/test_recover.vim
index db59223c2..93425f1fb 100644
--- a/src/testdir/test_recover.vim
+++ b/src/testdir/test_recover.vim
@@ -471,4 +471,41 @@ func Test_noname_buffer()
call assert_equal(['one', 'two'], getline(1, '$'))
endfunc
+" Test for recovering a corrupted swap file, those caused a crash
+func Test_recover_corrupted_swap_file1()
+ CheckUnix
+ " only works correctly on 64bit Unix systems:
+ if v:sizeoflong != 8 || !has('unix')
+ throw 'Skipped: Corrupt Swap file sample requires a 64bit Unix build'
+ endif
+ " Test 1: Heap buffer-overflow
+ new
+ let sample = 'samples/recover-crash1.swp'
+ let target = 'Xpoc1.swp'
+ call filecopy(sample, target)
+ try
+ sil recover! Xpoc1
+ catch /^Vim\%((\S\+)\)\=:E1364:/
+ endtry
+ let content = getline(1, '$')->join()
+ call assert_match('???ILLEGAL BLOCK NUMBER', content)
+ call delete(target)
+ bw!
+"
+" " Test 2: Segfault
+ new
+ let sample = 'samples/recover-crash2.swp'
+ let target = 'Xpoc2.swp'
+ call filecopy(sample, target)
+ try
+ sil recover! Xpoc2
+ catch /^Vim\%((\S\+)\)\=:E1364:/
+ endtry
+ let content = getline(1, '$')->join()
+ call assert_match('???ILLEGAL BLOCK NUMBER', content)
+ call assert_match('???LINES MISSING', content)
+ call delete(target)
+ bw!
+endfunc
+
" vim: shiftwidth=2 sts=2 expandtab
diff --git a/src/version.c b/src/version.c
index 21a1f3207..360e31edf 100644
--- a/src/version.c
+++ b/src/version.c
@@ -734,6 +734,8 @@ static char *(features[]) =
static int included_patches[] =
{ /* Add new patch number below this line */
+/**/
+ 77,
/**/
76,
/**/