commit 7e8bdd348c3bb6074ab795b97addb818705991bf
parent a416494e648b18a41fd5d1cdcf04ef0e05c15926
Author: zeertzjq <zeertzjq@outlook.com>
Date: Sat, 28 Feb 2026 12:33:47 +0800
vim-patch:9.2.0077: [security]: Crash when recovering a corrupted swap file (#38104)
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
https://github.com/vim/vim/commit/65c1a143c331c886dc28888dd632708f953b4eb3
Co-authored-by: Christian Brabandt <cb@256bit.org>
Diffstat:
4 files changed, 60 insertions(+), 3 deletions(-)
diff --git a/src/nvim/memline.c b/src/nvim/memline.c
@@ -1041,9 +1041,12 @@ void ml_recover(bool checkext)
// This is slow, but it works.
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, line_count,
- NULL, 0, false) != OK) {
+ 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, false) != OK) {
cannot_open = true;
} else {
lnum += line_count;
@@ -1066,6 +1069,23 @@ void ml_recover(bool checkext)
bnum = pp->pb_pointer[idx].pe_bnum;
line_count = pp->pb_pointer[idx].pe_line_count;
page_count = (unsigned)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++, _("???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/test/old/testdir/samples/recover-crash1.swp b/test/old/testdir/samples/recover-crash1.swp
Binary files differ.
diff --git a/test/old/testdir/samples/recover-crash2.swp b/test/old/testdir/samples/recover-crash2.swp
Binary files differ.
diff --git a/test/old/testdir/test_recover.vim b/test/old/testdir/test_recover.vim
@@ -478,4 +478,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 !has('nvim') && 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' " Xpoc1.swp (non-hidden) doesn't work in Nvim
+ 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' " Xpoc1.swp (non-hidden) doesn't work in Nvim
+ 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