From: Hirohito Higashi Date: Mon, 22 Jun 2026 04:00:36 +0000 (+0900) Subject: patch 9.2.0725: [security]: Stack out-of-bounds write in spell_soundfold_sal() X-Git-Tag: v9.2.0725^0 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=d22ff1c955ff87e8273210eae125aab0e85b6c30;p=thirdparty%2Fvim.git patch 9.2.0725: [security]: Stack out-of-bounds write in spell_soundfold_sal() Problem: [security]: A crafted spell file with non-collapsing SAL rules can make soundfold() write one byte past the end of the MAXWLEN result buffer. This is the same class of out-of-bounds write as GHSA-q8mh-6qm3-25g4 (fixed in 9.2.0698 for the SOFO branch), found while auditing the surrounding code. Solution: Bound the single-byte SAL result writes and the terminating NUL to MAXWLEN - 1, matching the SOFO branch. The single-byte branch of spell_soundfold_sal() guarded its writes with "reslen < MAXWLEN", allowing reslen to reach MAXWLEN (254). The trailing "res[reslen] = NUL" then wrote at index 254 of the 254-byte stack buffer res[MAXWLEN], an off-by-one out-of-bounds write. Input is case-folded to about 253 characters, so a 253-character argument together with a SAL map that does not collapse (collapse_result false) reaches the boundary. Related to previous issue [GHSA-q8mh-6qm3-25g4](https://github.com/vim/vim/security/advisories/GHSA-q8mh-6qm3-25g4) (9.2.0698) Github Security Advisory: https://github.com/vim/vim/security/advisories/GHSA-m3hf-xcm3-xhm2 Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Hirohito Higashi Signed-off-by: Christian Brabandt --- diff --git a/src/spell.c b/src/spell.c index 96700782ef..1276604251 100644 --- a/src/spell.c +++ b/src/spell.c @@ -3513,7 +3513,7 @@ spell_soundfold_sal(slang_T *slang, char_u *inword, char_u *res) // no '<' rule used i += k - 1; z = 0; - while (*s != NUL && s[1] != NUL && reslen < MAXWLEN) + while (*s != NUL && s[1] != NUL && reslen < MAXWLEN - 1) { if (reslen == 0 || res[reslen - 1] != *s) res[reslen++] = *s; @@ -3523,7 +3523,7 @@ spell_soundfold_sal(slang_T *slang, char_u *inword, char_u *res) c = *s; if (strstr((char *)pf, "^^") != NULL) { - if (c != NUL) + if (c != NUL && reslen < MAXWLEN - 1) res[reslen++] = c; STRMOVE(word, word + i + 1); i = 0; @@ -3542,7 +3542,7 @@ spell_soundfold_sal(slang_T *slang, char_u *inword, char_u *res) if (z0 == 0) { - if (k && !p0 && reslen < MAXWLEN && c != NUL + if (k && !p0 && reslen < MAXWLEN - 1 && c != NUL && (!slang->sl_collapse || reslen == 0 || res[reslen - 1] != c)) // condense only double letters diff --git a/src/testdir/test_spellfile.vim b/src/testdir/test_spellfile.vim index 9ec728a768..951538d514 100644 --- a/src/testdir/test_spellfile.vim +++ b/src/testdir/test_spellfile.vim @@ -405,6 +405,30 @@ func Test_spellfile_format_error() let &rtp = save_rtp endfunc +" An over-length soundfold() argument must not overflow the MAXWLEN result +" buffer in the single-byte branch of spell_soundfold_sal(). +func Test_spellfile_soundfold_sal_overflow() + let save_enc = &encoding + set encoding=latin1 + " A SAL map that appends without collapsing, so the result is not shorter + " than the input. + call writefile(['SET ISO8859-1', 'SAL collapse_result false', + \ 'SAL a aaaa', 'SAL b bbbb'], 'Xsal.aff') + call writefile(['2', 'hello', 'world'], 'Xsal.dic') + mkspell! Xsal Xsal + set spl=Xsal.latin1.spl spell + + " 253 input characters hit the buffer boundary; the result must not exceed + " MAXWLEN - 1. + call assert_true(strlen(soundfold(repeat('a', 253))) <= 253) + + set nospell spl& spelllang& + call delete('Xsal.aff') + call delete('Xsal.dic') + call delete('Xsal.latin1.spl') + let &encoding = save_enc +endfunc + " Test for format errors in suggest file func Test_sugfile_format_error() let save_rtp = &rtp diff --git a/src/version.c b/src/version.c index 010537fa1e..ed23c9f597 100644 --- a/src/version.c +++ b/src/version.c @@ -759,6 +759,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 725, /**/ 724, /**/