Opened 3 weeks ago

Closed 4 days ago

#5956 closed enhancement (fixed)

vim-9.2.0736 (Security Update)

Reported by: Joe Locash Owned by: SecurityAdvisory
Priority: normal Milestone: 13.1
Component: Book Version: git
Severity: normal Keywords:
Cc:

Description

There have been a few security updates since the last book update:

Out-of-bounds Write in Spell File Word Count in Vim < 9.2.0653
==============================================================
Date: 15.06.2026
Severity: Medium
CVE: *requested, not yet assigned*
CWE: Out-of-bounds Write (CWE-787)

## Summary
The `tree_count_words()` function in `src/spellfile.c` fills in the word-count
fields of a spell-file word trie by walking it iteratively with a depth
counter.  The counter is bounded only by the trie structure itself; it is
never checked against the size of the fixed `MAXWLEN`-element stack arrays it
indexes (`arridx[]`, `curi[]`, `wordcount[]`).  A crafted `.spl`/`.sug` file
pair, loaded when the user invokes spell suggestion, can drive the descent
arbitrarily deep, so the function writes past the end of those arrays.  This
is a stack out-of-bounds write that corrupts the call frame and crashes the
editor.

## Description
`tree_count_words()` is called from `suggest_load_files()` when spell
suggestion loads a language's `.sug` file (for example on `z=`,
`:spellsuggest`, or suggestion completion).  For each node it descends one
level with:
```C
    else
    {
        ++depth;
        arridx[depth] = idxs[n];
        curi[depth] = 1;
        wordcount[depth] = 0;
    }
```

The three arrays have `MAXWLEN` (254) elements, so any `depth` of 254 or more
writes out of bounds.  In a well-formed file each trie level corresponds to
one byte of a word, so depth is naturally limited, but the reader does not
enforce this for shared subtrees: `read_tree_node()` caps inline recursion at
`depth > MAXWLEN`, yet a `BY_INDEX` shared reference is accepted (its target
index is range-checked) and is *not* recursed into.  A trie that uses shared
references to form a cycle, or a deep forward-shared chain, therefore parses
cleanly while driving the iterative walker past `MAXWLEN`.

## Impact
A spell file is normally inert data, but Vim resolves `spelllang`,
`spellfile`, and `runtimepath` to load it, so a repository or archive that
ships a malicious `spell/` sidecar can deliver the crafted pair.  The trigger
is user-interaction-gated: spell checking must be enabled and the user must
invoke spell suggestion on a misspelled word.  When that happens, the
out-of-bounds write corrupts the `tree_count_words()` stack frame.  On builds
compiled with stack protection the overwrite is caught at the function epilogue
and the process aborts; cyclic geometries instead walk into the stack guard
page and causes a crash.

## Acknowledgements
The Vim project would like to thank Cipher / Causal Security
(https://causalsecurity.com/) for reporting and analyzing the issue and
suggesting a fix.

## References
The issue has been fixed as of Vim patch [v9.2.0653](https://github.com/vim/vim/releases/tag/v9.2.0653).
- [Commit](https://github.com/vim/vim/commit/a80874d9b84a01040e3d1aef2d4a59e1934dafb7)
- [Github Security Advisory](https://github.com/vim/vim/security/advisories/GHSA-wgh4-64f7-q3jq)
Out-of-bounds Write in Spell File Prefix Dump in Vim < 9.2.0662
===============================================================
Date: 16.06.2026
Severity: Medium
CVE: *requested, not yet assigned*
CWE: Out-of-bounds Write (CWE-787)

## Summary
The `dump_prefixes()` function in `src/spell.c` walks a spell-file prefix trie
iteratively with a depth counter while dumping the prefixes that apply to a
word.  The counter is bounded only by the trie structure itself; it is never
checked against the size of the fixed `MAXWLEN`-element stack arrays it indexes
(`prefix[]`, `arridx[]`, `curi[]`).  A crafted `.spl` file, loaded when the
user dumps the word list, can drive the descent arbitrarily deep, so the
function writes past the end of those arrays.  This is a stack out-of-bounds
write that corrupts the call frame and crashes the editor.

## Description
`dump_prefixes()` is called from `spell_dump_compl()` when the word list is
dumped, for example on `:spelldump` or via spelling completion.  For each node
it descends one level with:
```C
    else
    {
        prefix[depth++] = c;
        arridx[depth] = idxs[n];
        curi[depth] = 1;
    }
```
The arrays have `MAXWLEN` (254) elements, so any `depth` of 254 or more writes
out of bounds; a second sink underflows a `size_t` length passed to
`vim_strncpy()` once `depth` reaches `MAXWLEN`.  In a well-formed file each
trie level corresponds to one byte of a prefix, so depth is naturally limited,
but the reader does not enforce this for shared subtrees: a `BY_INDEX` shared
reference is accepted (its target index is range-checked) and is *not* recursed
into.  A prefix trie that uses a shared reference to point back to an ancestor
or itself therefore parses cleanly while driving the iterative walker past
`MAXWLEN`.  This is the same class of issue as
[GHSA-wgh4-64f7-q3jq](https://github.com/vim/vim/security/advisories/GHSA-wgh4-64f7-q3jq)
(`tree_count_words()`, fixed in 9.2.0653) in a sibling trie walker that was
left unguarded.

## Impact
A spell file is normally inert data, but Vim resolves `spelllang`, `spellfile`,
and `runtimepath` to load it, so a repository or archive that ships a malicious
`spell/` sidecar can deliver the crafted file.  The trigger is
user-interaction-gated: spell checking must be enabled and the user must dump
the word list.  When that happens, the out-of-bounds write corrupts the
`dump_prefixes()` stack frame.

## Acknowledgements
The Vim project would like to thank Cipher / Causal Security
(https://causalsecurity.com/) for reporting and analyzing the issue and
suggesting a fix.

## References
The issue has been fixed as of Vim patch [v9.2.0662](https://github.com/vim/vim/releases/tag/v9.2.0662).
- [Commit](https://github.com/vim/vim/commit/8325b193bba5f01e7a7d8241f)
- [Github Security Advisory](https://github.com/vim/vim/security/advisories/GHSA-qm9w-fmpj-879h)
Vimscript Code Injection in netrw NetrwLocalRmFile() via crafted filename affects Vim < 9.2.0663
================================================================================================
Date: 16.06.2026
Severity: Medium
CVE: *requested, not yet assigned*
CWE: Improper Control of Generation of Code (CWE-94) /
     Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection') (CWE-78)

## Summary
A Vimscript code injection vulnerability exists in `s:NetrwLocalRmFile()` in
the netrw plugin (`runtime/pack/dist/opt/netrw/autoload/netrw.vim`) when
deleting a local file from the browser.  A filename derived from the buffer's
directory listing is interpolated into an Ex command line passed to
`:execute` with only the backslash character escaped, allowing a crafted
filename containing a bar (`|`) to terminate the intended command and execute
arbitrary Vimscript, including shell commands via `:call system()` and `:!`.

## Description
`s:NetrwLocalRmFile()` removes a file and wipes its buffer.  The sibling sinks
in the same file guard the Ex context with `fnameescape()`, for example:

    exe "sil! keepj keepalt file ".fnameescape(a:newname)

The delete handler, however, escapes only the backslash character and passes
the value straight to `:execute`:

    let rmfile = s:NetrwFile(netrw#fs#ComposePath(a:path,
                   escape(a:fname, '\\')))->fnamemodify(':.')
    ...
    execute printf('silent! bwipeout %s', rmfile)

`escape(a:fname, '\\')` neutralizes only `\`; the subsequent `ComposePath`,
`NetrwFile`, and `fnamemodify` calls join and normalize the path but do not
remove `|` or a newline.  When the value reaches `:execute`, a `|` terminates
the `bwipeout` command and begins the next, so the remainder of the filename
is run as Ex commands.  A file named `x|call system('cmd')|y` turns the
deletion into `silent! bwipeout x`, `call system('cmd')`, `y`, and the
injected `:call system(...)` runs an arbitrary shell command.
The filename reaches `s:NetrwLocalRmFile()` through the `D` mapping, which
reads the filename from the current line of the netrw directory listing and,
after confirmation, deletes the entry.

## Impact
The vulnerability allows arbitrary Vimscript execution, and by extension
arbitrary shell command execution, with the privileges of the user running
Vim.  Exploitation requires:
- a Unix-like system on which a filename may contain a bar (`|`),
- a crafted file present in a directory the victim browses with netrw, and
- the victim to delete that specific entry (press `D`, then confirm with `y`).

The severity is rated Medium because exploitation requires a planted file with
an unusual name and a deliberate delete action by the victim on that specific
entry, although the resulting primitive is full command execution as the
victim user.
Note: due to the nature of the issue, it seems unlikely that a user would
delete such a suspicious filename.

## Acknowledgements
The Vim project would like to thank Cipher / Causal Security
(https://causalsecurity.com/) for reporting and analyzing the issue.

## References
The issue has been fixed as of Vim patch [v9.2.0663](https://github.com/vim/vim/releases/tag/v9.2.0663).
- [Commit](https://github.com/vim/vim/commit/55bc757a5d436e59d50fe43f7cda94b118f86cb2)
- [Github Security Advisory](https://github.com/vim/vim/security/advisories/GHSA-vhh8-v6wx-hjjh)

Change History (9)

comment:1 by Joe Locash, 3 weeks ago

Summary: vim-9.2.0663 (Security Update)vim-9.2.0671 (Security Update)

A couple more...

Out-of-bounds Read in Text Property Count in Vim < 9.2.0670
===========================================================
Date: 17.06.2026
Severity: Medium
CVE: *requested, not yet assigned*
CWE: Out-of-bounds Read (CWE-125)

## Summary

`get_text_props()` in `src/textprop.c` reads a `uint16` property count stored
inline after a line's text and returns it as the number of 32-byte
`textprop_T` entries that follow.  The only check is a floor that guarantees
room for a single entry; the count is never checked against the amount of data
actually present.  A line that declares a large count while carrying little
data causes consumers to read far past the end of the line buffer.  Such a
line can be delivered through a crafted undo file, leading to a crash.

## Description

When text properties are present, a line is stored as
`[text][NUL][prop_count (uint16)][textprop_T ...][vtext ...]`.
`get_text_props()` validates only that the property data is large enough for
one entry and then returns `prop_count` unchanged:

```C
    if (propdata_len < PROP_COUNT_SIZE + sizeof(textprop_T))
    {
        iemsg(e_text_property_info_corrupted);
        return 0;
    }
    mch_memmove(&prop_count, text + textlen, PROP_COUNT_SIZE);
    *props = text + textlen + PROP_COUNT_SIZE;
    return (int)prop_count;
```

Consumers use the returned count as a loop bound, reading
`sizeof(textprop_T)` (32) bytes per iteration with no re-check against the real
line length, so a count of 65535 against a single stored entry reads about two
megabytes past the line.  An attacker authors both a source file and its
sibling undo (`.un~`) file; the saved line text in the undo file is patched so
that, on undo, the restored line declares a property count far larger than its
data.  The next consumer, such as `prop_list()` or a screen redraw, then reads
out of bounds.  The same unbounded count is used by the unpacked-memline path
and by the line-deletion code, which are affected in the same way.

## Impact

Undo files are normally trusted data written by Vim itself, but Vim reads a
sibling `.un~` file when `'undofile'` is enabled and the file's content hash
matches, so a repository or archive that ships a malicious source/undo pair can
deliver the crafted line.  The trigger is user-interaction-gated: `'undofile'`
must be enabled, the user must open the file and undo, and a consumer must then
inspect the line (for example by displaying it).  When that happens, the
out-of-bounds read can leak adjacent memory through property metadata returned
by `prop_list()` and crash the editor.

## Acknowledgements

The Vim project would like to thank Cipher / Causal Security
(https://causalsecurity.com/) for reporting and analyzing the issue and
suggesting a fix.

## References

The issue has been fixed as of Vim patch [v9.2.0670](https://github.com/vim/vim/releases/tag/v9.2.0670).
- [Commit](https://github.com/vim/vim/commit/b2338ca90643e2f01ecb6547c1172716aaec4f79)
- [Github Security Advisory](https://github.com/vim/vim/security/advisories/GHSA-f36c-2qcp-7gpw)
Out-of-bounds Read with libsodium-encrypted Files in Vim < 9.2.0671
===================================================================
Date: 17.06.2026
Severity: Medium
CVE: *requested, not yet assigned*
CWE: Integer Underflow (CWE-191), Out-of-bounds Read (CWE-125)

## Summary

When Vim opens a file encrypted with the `VimCrypt~04!` or `VimCrypt~05!`
method (xchacha20poly1305, requires the `+sodium` feature) whose body is shorter
than a single libsodium secretstream header, an unsigned length calculation
underflows and a subsequent decryption call reads far past the end of the input
buffer, crashing Vim.

## Description

`crypt_get_header_len()` validates only the size of the file header (magic,
salt and seed), not the length of the encrypted body that follows.  A file with
a complete header but a body of 1 to 23 bytes therefore passes the size check.
In `crypt_sodium_buffer_decode()` the secretstream header length is then
subtracted from the body length:

```C
    from += crypto_secretstream_xchacha20poly1305_HEADERBYTES;
    len  -= crypto_secretstream_xchacha20poly1305_HEADERBYTES;
```

`len` is a `size_t`, so when the body is shorter than `HEADERBYTES` the
subtraction wraps around to a value close to the maximum.  The wrapped length is
then passed to `crypto_secretstream_xchacha20poly1305_pull()`, which reads that
many bytes from the input buffer.  Because the output buffer was already
allocated using the original (small) body length, the corrupted length widens
the read rather than a write, resulting in an out-of-bounds read and a crash.

## Impact

An attacker who can get a victim to open a crafted, ostensibly encrypted file
and enter any key at the prompt can reliably crash Vim (denial of service).  No
information is disclosed: the decryption call faults on the over-read and never
returns plaintext, and the message authentication would fail in any case.
Exploitation requires the `+sodium` feature, user interaction to open the file
and supply a key, and is limited to a crash.

## Acknowledgements

The Vim project would like to thank Cipher / Causal Security
(https://causalsecurity.com/) for reporting and analyzing the issue and
suggesting a fix.

## References

The issue has been fixed as of Vim patch [v9.2.0671](https://github.com/vim/vim/releases/tag/v9.2.0671).
- [Commit](https://github.com/vim/vim/commit/c8777cec25dcfae89c42e9aff51af61f71c5745f)
- [Github Security Advisory](https://github.com/vim/vim/security/advisories/GHSA-c4j9-wr9j-4486)

comment:2 by Joe Locash, 3 weeks ago

Summary: vim-9.2.0671 (Security Update)vim-9.2.0679 (Security Update)
Out-of-bounds Read with Text Properties in Vim >= 9.2.0320 && Vim < 9.2.0679
============================================================================
Date: 20.06.2026
Severity: Medium
CVE: *requested, not yet assigned*
CWE: Out-of-bounds Read (CWE-125)

## Summary

A crafted undo or swap file can store a virtual-text property whose offset and
length point outside the line's property data. When Vim restores or displays
such a line it converts the offset into a pointer and reads the virtual text
without bounds checking, causing an out-of-bounds read that can crash Vim or
disclose adjacent heap memory.

This completes GHSA-f36c-2qcp-7gpw (patch 9.2.0670): that patch validated only
the property *count*; the per-property virtual-text offset and length were
still taken from the untrusted file unchecked.

## Details

Since patch 9.2.0320, text properties are stored inline after a line:

```
[text][NUL][prop_count (uint16)][textprop_T ...][virtual text ...]
```

For a virtual-text property (`tp_id < 0`), `tp_text_offset` is the byte offset
of its string relative to the `prop_count` position. On load the offset is
turned into a pointer (`count_ptr + tp_text_offset`) and the string is later
read as `tp_len + 1` bytes.

`text_prop_count_valid()` (9.2.0670) only ensures the `textprop_T` array fits
within the property data; it does not constrain `tp_text_offset` or `tp_len`. A
file with `tp_id < 0` and an out-of-range offset therefore yields a wild
pointer, and reading the virtual text reads out of bounds. The unchecked
conversion is reachable when:

- restoring a line from an undo/swap file, and
- displaying or querying a line's properties (`get_text_props()`, used by
  screen drawing, `charset.c`, and `prop_list()`/`prop_find()`).

## Impact

Out-of-bounds read leading to a crash (denial of service) and potentially
disclosure of adjacent heap memory. Exploitation requires the victim to open a
maliciously crafted undo or swap file, so attack complexity is high and user
interaction is required. Severity: Medium.

## Patch

Fixed by validating, for every virtual-text property, that `tp_text_offset` and
`tp_len` lie within the available property data before the offset is converted
to a pointer (`text_prop_vtext_valid()`), applied on both the undo/swap restore
and the display path. Update to patch 9.2.0679.

## Workarounds

Do not load undo files (`:rundo`) or recover swap files from untrusted sources.

## References

The issue has been fixed as of Vim patch [v9.2.0679](https://github.com/vim/vim/releases/tag/v9.2.0679).
- [Commit](https://github.com/vim/vim/commit/b3faeecc976d3031d7c0675623516ec60c30f949)
- [Github Security Advisory](https://github.com/vim/vim/security/advisories/GHSA-ww8h-47xp-hp4w)
- [GHSA-f36c-2qcp-7gpw](https://github.com/vim/vim/security/advisories/GHSA-f36c-2qcp-7gpw) - the count check this completes
- Introduced in patch [v9.2.0320](https://github.com/vim/vim/releases/tag/v9.2.0320) (inline text-property storage format)

comment:3 by Joe Locash, 2 weeks ago

Summary: vim-9.2.0679 (Security Update)vim-9.2.0699 (Security Update)
Out-of-bounds Write in SOFO Soundfolding in Vim < 9.2.0698
==========================================================
Date: 21.06.2026
Severity: Medium
CVE: *requested, not yet assigned*
CWE: Out-of-bounds Write (CWE-787)

## Summary
The single-byte branch of `spell_soundfold_sofo()` in `src/spell.c` translates
a word through a spell file's SOFO (sound-folding) byte map into a caller-owned
result buffer.  Its copy loop advances the output index `ri` with no upper
bound and terminates only on the input NUL, writing one byte per input byte
into the `MAXWLEN`-element stack buffer the caller provides.  A word longer
than `MAXWLEN`, passed to `soundfold()` (or reached via sound-based spell
suggestion) while a SOFO-based spell language is active, therefore writes past
the end of that buffer.  This is a stack out-of-bounds write that corrupts the
call frame and crashes the editor.

## Description
`spell_soundfold_sofo()` has two branches.  The multibyte branch (taken under
multibyte encodings) bounds its output with `if (ri + MB_MAXBYTES > MAXWLEN)
break;`.  The single-byte branch, taken for 8-bit encodings such as `latin1`,
has no equivalent guard:

```C
    else
    {
        // The sl_sal_first[] table contains the translation.
        for (s = inword; (c = *s) != NUL; ++s)      // bound: input NUL only
        {
            if (VIM_ISWHITE(c))
                c = ' ';
            else
                c = slang->sl_sal_first[c];
            if (c != NUL && (ri == 0 || res[ri - 1] != c))
                res[ri++] = c;                      // no ri < MAXWLEN guard
        }
    }
    res[ri] = NUL;                                  // trailing OOB write too
```

The destination `res` is an array of size `[MAXWLEN]`. `f_soundfold()` passes
the user-supplied string straight to `eval_soundfold()` without length-bounding
it, so once the active language carries a SOFO map (`sl_sal_first`), any input
longer than 253 bytes runs `ri` past the end of the buffer.  The two sibling
walkers do bound their output (`spell_soundfold_sal()` truncates its input with
`vim_strncpy(.., MAXWLEN - 1)` and guards `reslen < MAXWLEN`;
`spell_soundfold_wsal()` guards `reslen < MAXWLEN`); the single-byte SOFO
branch was the remaining unguarded walker of this class.

## Impact
This issue is driven by the length of the word handed to the
spell_soundfold_sofo() function, not by the contents of the spell file. any
loaded spell language with a SOFO sound-folding table is enough.  Exploitation
is constrained, however.  The vulnerable single-byte branch is only reached
under a non-multibyte 8-bit encoding (e.g. `set encoding=latin1`); under the
default UTF-8 encoding the multibyte branch, which is already bounded, is taken
instead.  Spell checking must be enabled with such a SOFO-based language, and
`soundfold()`  must be invoked on an over-long word - for instance a script or
plugin that calls `soundfold()` on untrusted input.  When those conditions hold
the out-of-bounds write corrupts the `eval_soundfold()` stack frame and the
process aborts.

## Acknowledgements
The Vim project would like to thank Cipher / Causal Security
(https://causalsecurity.com/) for reporting and analyzing the issue and
suggesting a fix.

## References
The issue has been fixed as of Vim patch [v9.2.0698](https://github.com/vim/vim/releases/tag/v9.2.0698).
- [Commit](https://github.com/vim/vim/commit/497f931f85339d175d7f69588dd249e8ccfed41b)
- [Github Security Advisory](https://github.com/vim/vim/security/advisories/GHSA-q8mh-6qm3-25g4)
Arbitrary Code Execution via Python Omni-Completion Docstrings in Vim < 9.2.0699
================================================================================
Date: 2026-06-21
Severity: Medium
CVE: *requested, not yet assigned*
CWE: Improper Control of Generation of Code (CWE-94)

## Summary
Vim's Python omni-completion executes reconstructed function and class
definitions from the current buffer with `exec()` as part of populating the
completion dictionary. When reconstructing that source, each scope's docstring
is inserted verbatim between triple quotes with no escaping, so a hostile
buffer can break out of the triple-quoted literal and execute attacker-
controlled Python during omni-completion. This is the same class of issue as
GHSA-65p9-mwwx-7468 (patch 9.2.0597), whose fix sanitised parameter
defaults/annotations and class base lists but left the docstring path
untouched.

## Description
In `runtime/autoload/python3complete.vim` (and the legacy
`pythoncomplete.vim`), the `get_code()` methods build the source later passed
to `exec()` and emit each docstring as `'"""' + self.docstr + '"""'`.
`self.docstr` comes straight from buffer content, and the `doc()` helper only
strips leading and trailing quote and whitespace characters, so a `"""`
embedded in the middle of a docstring survives. A class-body docstring written
as a single-quoted source string keeps the embedded `"""` as one string token
through `doc()`, then breaks out of the generated triple-quoted literal: the
reconstructed `class` body becomes string concatenation around an attacker
expression, which Python evaluates at class-definition time when `exec()` runs.

## Impact
An attacker who can convince a user to open or edit a hostile Python
buffer and trigger Python omni-completion (CTRL-X CTRL-O, or a plugin
that invokes the completion function) can execute Python code in the
user's Vim process. The code runs with the user's privileges.

Vim built without `+python3` and `+python` is not affected. Triggering
omni-completion in the hostile buffer is required; opening the file
alone is not sufficient.

## Acknowledgements
The Vim project would like to thank Chenyuan Mi for reporting and analyzing the
issue and suggesting a fix.

## References
The issue has been fixed as of Vim patch [v9.2.0699](https://github.com/vim/vim/releases/tag/v9.2.0699).
- [Commit](https://github.com/vim/vim/commit/cce141c42740f122dd8486ae04e21c2a81016ba8)
- [Github Security Advisory](https://github.com/vim/vim/security/advisories/GHSA-ppj8-wqjf-6fp3)
- [Github Security Advisory GHSA-65p9-mwwx-7468](https://github.com/vim/vim/security/advisories/GHSA-65p9-mwwx-7468) (prior fix for the same surface)

comment:4 by Bruce Dubbs, 2 weeks ago

I'm going to wait until the end of the month to update vim. The number of security issues is disturbing though. Judging from the rate, there will probably be more.

comment:5 by Joe Locash, 2 weeks ago

Summary: vim-9.2.0699 (Security Update)vim-9.2.0725 (Security Update)
Out-of-bounds Write in SAL Soundfolding in Vim < 9.2.0725
=========================================================
Date: 24.06.2026
Severity: Medium
CVE: *requested, not yet assigned*
CWE: Out-of-bounds Write (CWE-787)

## Summary
The single-byte branch of `spell_soundfold_sal()` in `src/spell.c` translates
a word through a spell file's SAL (sound-folding) rules into a caller-owned
result buffer.  Its result writes are guarded with `reslen < MAXWLEN`, which
allows `reslen` to reach `MAXWLEN`.  The function then terminates the result
with `res[reslen] = NUL`, writing one byte past the end of the `MAXWLEN`-element
stack buffer when `reslen == MAXWLEN`.  A word that reaches the buffer boundary,
passed to `soundfold()` (or reached via sound-based spell suggestion) while a
SAL-based spell language is active, therefore writes past the end of that
buffer.  This is a stack out-of-bounds write that corrupts the call frame and
crashes the editor.

## Description
`spell_soundfold_sal()` writes its translated output into the stack buffer
`res`, an array of size `[MAXWLEN]` (valid indices `0..MAXWLEN - 1`).  The
single-byte branch, taken for 8-bit encodings such as `latin1`, guards each
result write with `reslen < MAXWLEN`, so `reslen` can be incremented up to and
including `MAXWLEN`.  After the translation loop the result is NUL-terminated:

```C
    res[reslen] = NUL;          // reslen may equal MAXWLEN -> OOB write
```

With `reslen == MAXWLEN` this stores at index `MAXWLEN` of a `MAXWLEN`-element
buffer, one byte past the end (CWE-787).  The preceding write sites in this
branch — the no-`<`-rule copy loop, the `"^^"` rule append, and the
double-letter condense — are bounded with `< MAXWLEN` rather than `< MAXWLEN - 1`,
leaving no room for the terminating NUL at the boundary.

`f_soundfold()` passes the user-supplied string to `eval_soundfold()` without
length-bounding it; the input is case-folded to roughly `MAXWLEN - 1` bytes, so
an argument at that boundary length drives `reslen` to `MAXWLEN`.  This is the
SAL counterpart to the SOFO issue fixed in 9.2.0698; the single-byte SAL branch
was the remaining walker of this class with an off-by-one at the terminator.

## Impact
This issue is driven by the length of the word handed to the
`spell_soundfold_sal()` function, not by the contents of the spell file; any
loaded spell language with SAL sound-folding rules is enough.  Exploitation is
constrained, however.  The vulnerable single-byte branch is only reached under
a non-multibyte 8-bit encoding (e.g. `set encoding=latin1`); under the default
UTF-8 encoding the multibyte branch `spell_soundfold_wsal()`, which writes one
full character at a time and breaks on `l + MB_MAXBYTES > MAXWLEN`, is taken
instead and is unaffected.  Spell checking must be enabled with such a
SAL-based language, and `soundfold()` must be invoked on a boundary-length word
— for instance a script or plugin that calls `soundfold()` on untrusted input.
When those conditions hold the out-of-bounds write corrupts the
`eval_soundfold()` stack frame and the process aborts.

## Acknowledgements
The Vim project would like to thank Hirohito Higashi (@h-east) for reporting and
analyzing the issue and suggesting a fix.

## References
The issue has been fixed as of Vim patch [v9.2.0725](https://github.com/vim/vim/releases/tag/v9.2.0725).
- [Commit](https://github.com/vim/vim/commit/d22ff1c955ff87e8273210eae125aab0e85b6c30)
- [Github Security Advisory](https://github.com/vim/vim/security/advisories/GHSA-m3hf-xcm3-xhm2)

comment:6 by Joe Locash, 12 days ago

Summary: vim-9.2.0725 (Security Update)vim-9.2.0735 (Security Update)
Arbitrary Code Execution via C Omni-Completion in Vim < 9.2.0735
================================================================
Date: 26.06.2026
Severity: Medium
CVE: *requested, not yet assigned*
CWE: Improper Neutralization of Special Elements (CWE-94),
     Inclusion of Functionality from Untrusted Control Sphere (CWE-829)

## Summary
The C omni-completion script in `runtime/autoload/ccomplete.vim` interpolates
the `typeref:` / `typename:` extension field of a `tags` entry, without
escaping, into a `:vimgrep` pattern that is run through `:execute`.  Because
`:vimgrep` honors the bar (`|`) as a command separator, a crafted tag field
can close the search pattern and append an arbitrary Ex command.  Opening a
hostile `.c` file whose project `tags` file contains such an entry and
invoking C omni-completion runs that command as the editing user.

## Description
`runtime/ftplugin/c.vim` installs `omnifunc=ccomplete#Complete` on every C
buffer when Vim has filetype plugins enabled.  When the user invokes omni-
completion with `CTRL-X CTRL-O` on a structure-member access (for example
`myvar.field`), the completer looks up the variable in the `tags` files,
reads the type from the entry's `typeref:` (or `typename:`) field, and
searches the `tags` files for members of that type.

The member search builds a `:vimgrep` command and runs it with `:execute`.
The type name taken from the tag field is concatenated directly into the
search pattern with no escaping, while the file-name argument is passed
through `escape()`.  The `typeref:` field is parsed verbatim by `taglist()`;
its only constraint is that it contains no internal whitespace, so the
characters `/ | ' " ( )` all survive.

`:vimgrep` carries the `EX_TRLBAR` attribute, which means an unescaped bar in
the constructed command line terminates the `:vimgrep` and begins a new Ex
command.  A tag field of the form `x/|<command>|"` therefore closes the
regular expression with `/`, starts a fresh Ex command after `|`, and comments
out the remainder of the generated line.

## Impact
Arbitrary local code execution as the user running Vim, with that user's full
credential set, file-system access, and network egress.  Realistic delivery
vectors include:

- cloning or checking out a third-party repository that ships a `tags` file
  alongside its C sources,
- extracting a source tarball or archive whose layout places a hostile `tags`
  file next to the `.c` file being inspected,
- auditing a malware sample or untrusted source tree in its own directory.

Exploitation requires:

- Vim with filetype plugins enabled (`filetype plugin on`, the default in
  `runtime/defaults.vim` and most distribution `vimrc` files),
- a `tags` file reachable through the `'tags'` option (the default `./tags`
  resolves to the attacker's file in the conventional ctags workflow, where
  Vim's working directory is the project root),
- the victim opening the hostile `.c` file and invoking C omni-completion on a
  member access.

The severity is rated Medium because the user must manually invoke omni-
completion on a member access after opening the file; the bug does not fire on
file-open alone and the `ccomplete.vim` produces an error message, which makes
the whole attack quite noticeable.

## Acknowledgements
The Vim project would like to thank Cipher / Causal Security
(https://causalsecurity.com/) for reporting and analyzing the issue and
providing a proof of concept.

## References
The issue has been fixed as of Vim patch [v9.2.0735](https://github.com/vim/vim/releases/tag/v9.2.0735).
- [Commit](https://github.com/vim/vim/commit/6b611b0d15603c52ebdad17172b0232b4f65704e)
- [Github Security Advisory](https://github.com/vim/vim/security/advisories/GHSA-mf92-v4xw-j45x)

comment:7 by Joe Locash, 11 days ago

Summary: vim-9.2.0735 (Security Update)vim-9.2.0736 (Security Update)
Arbitrary Code Execution via PHP Omni-Completion in Vim < 9.2.0736
==================================================================
Date: 28.06.2026
Severity: Medium
CVE: *requested, not yet assigned*
CWE: Improper Neutralization of Special Elements (CWE-94),
     Inclusion of Functionality from Untrusted Control Sphere (CWE-829)

## Summary
The PHP omni-completion script in `runtime/autoload/phpcomplete.vim`
interpolates a class or trait name, taken from the contents of the edited
buffer, into a `search()` pattern that is run via `win_execute()` without
escaping.  A name containing a single quote can terminate the `search()`
string argument early; because the bar (`|`) is honored as an Ex command
separator, the remainder of the name is then run as Ex commands.  Via the
`:!` command this allows arbitrary command execution.

## Description
`runtime/ftplugin/php.vim` installs `omnifunc=phpcomplete#Complete` on every
PHP buffer when Vim has filetype plugins enabled.  When the user invokes
omni-completion with `CTRL-X CTRL-O`, `phpcomplete#GetClassContentsStructure()`
locates a class declaration by building a `search()` command and running it
through `win_execute()`:

    call win_execute(popup_id,
      \ 'call search(''\c\(class\|interface\|trait\)\_s\+'
      \ .. a:class_name .. '\(\>\|$\)'')')

The class name is concatenated into the single-quoted `search()` argument
without neutralizing the single quote.  A `'` in the name ends that string
early, and the `:call` command honors the `EX_TRLBAR` attribute, so a bar
following the quote begins a new Ex command that `win_execute()` then runs.

The name passed to this function can originate from the buffer's own contents
(for example a class or trait name parsed from the file), so a crafted PHP
file can place an injecting value where a class name is expected.

## Impact
Arbitrary Ex command execution, and via the `:!` command arbitrary operating-
system command execution, in the context of the user running Vim.

Exploitation requires:

- Vim with filetype plugins enabled (`filetype plugin on`, the default in
  `runtime/defaults.vim` and most distribution `vimrc` files),
- the PHP omni-completion function in use
  (`omnifunc=phpcomplete#Complete`, set by the bundled PHP ftplugin),
- the victim opening a crafted PHP file and invoking omni-completion.

The severity is rated Medium because the user must open the crafted file and
manually invoke omni-completion; the bug does not fire on file-open alone.

## Acknowledgements
The Vim project would like to thank Hirohito Higashi for reporting, analyzing
and fixing the issue.

## References
The issue has been fixed as of Vim patch [v9.2.0736](https://github.com/vim/vim/releases/tag/v9.2.0736).
- [Commit](https://github.com/vim/vim/commit/43afc581a37a35762dd0ef292f038b9dc5680a24)
- [Github Security Advisory](https://github.com/vim/vim/security/advisories/GHSA-fh26-8f79-wj97)

comment:8 by Bruce Dubbs, 9 days ago

Owner: changed from lfs-book to SecurityAdvisory

Fixed at commit 4a56fb5d78.

Leaving open for security advisories.

comment:9 by Bruce Dubbs, 4 days ago

Resolution: fixed
Status: newclosed

Advisory sa-13.0-136 has been issued.

Note: See TracTickets for help on using tickets.