patch 9.2.0725: [security]: Stack out-of-bounds write in spell_soundfold_sal()
Commit:
https://github.com/vim/vim/commit/d22ff1c955ff87e8273210eae125aab0e85b6c30
Author: Hirohito Higashi <
h.eas...@gmail.com>
Date: Mon Jun 22 13:00:36 2026 +0900
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) <
nor...@anthropic.com>
Signed-off-by: Hirohito Higashi <
h.eas...@gmail.com>
Signed-off-by: Christian Brabandt <
c...@256bit.org>
diff --git a/src/spell.c b/src/spell.c
index 96700782e..127660425 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 9ec728a76..951538d51 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 010537fa1..ed23c9f59 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,
/**/