neovim/test/functional/ui/wildmode_spec.lua
zeertzjq 272ef27115
vim-patch:9.0.2035: [security] use-after-free with wildmenu (#25687)
Problem:  [security] use-after-free with wildmenu
Solution: properly clean up the wildmenu when exiting

Fix wildchar/wildmenu/pum memory corruption with special wildchar's

Currently, using `wildchar=<Esc>` or `wildchar=<C-\>` can lead to a
memory corruption if using wildmenu+pum, or wrong states if only using
wildmenu. This is due to the code only using one single place inside the
cmdline process loop to perform wild menu clean up (by checking
`end_wildmenu`) but there are other odd situations where the loop could
have exited and we need a post-loop clean up just to be sure. If the
clean up was not done you would have a stale popup menu referring to
invalid memory, or if not using popup menu, incorrect status line (if
`laststatus=0`).

For example, if you hit `<Esc>` two times when it's wildchar, there's a
hard-coded behavior to exit command-line as a failsafe for user, and if
you hit `<C-\><C-\><C-N>` it will also exit command-line, but the clean
up code would not have hit because of specialized `<C-\>` handling.

Fix Ctrl-E / Ctrl-Y to not cancel/accept wildmenu if they are also
used for 'wildchar'/'wildcharm'. Currently they don't behave properly,
and also have potentially memory unsafe behavior as the logic is
currently not accounting for this situation and try to do both.
(Previous patch that addressed this: vim/vim#11677)

Also, correctly document Escape key behavior (double-hit it to escape)
in wildchar docs as it's previously undocumented.

In addition, block known invalid chars to be set in `wildchar` option,
such as Ctrl-C and `<CR>`. This is just to make it clear to the user
they shouldn't be set, and is not required for this bug fix.

closes: vim/vim#13361

8f4fb007e4

Co-authored-by: Yee Cheng Chin <ychin.git@gmail.com>
2023-10-17 22:43:42 +08:00

682 lines
20 KiB
Lua

local helpers = require('test.functional.helpers')(after_each)
local Screen = require('test.functional.ui.screen')
local clear, feed, command = helpers.clear, helpers.feed, helpers.command
local funcs = helpers.funcs
local meths = helpers.meths
local eq = helpers.eq
local eval = helpers.eval
local retry = helpers.retry
local testprg = helpers.testprg
local is_os = helpers.is_os
describe("'wildmenu'", function()
local screen
before_each(function()
clear()
screen = Screen.new(25, 5)
screen:attach()
end)
-- oldtest: Test_wildmenu_screendump()
it('works', function()
screen:set_default_attr_ids({
[0] = {bold = true, foreground = Screen.colors.Blue}; -- NonText
[1] = {foreground = Screen.colors.Black, background = Screen.colors.Yellow}; -- WildMenu
[2] = {bold = true, reverse = true}; -- StatusLine
})
-- Test simple wildmenu
feed(':sign <Tab>')
screen:expect{grid=[[
|
{0:~ }|
{0:~ }|
{1:define}{2: jump list > }|
:sign define^ |
]]}
feed('<Tab>')
screen:expect{grid=[[
|
{0:~ }|
{0:~ }|
{2:define }{1:jump}{2: list > }|
:sign jump^ |
]]}
feed('<Tab>')
screen:expect{grid=[[
|
{0:~ }|
{0:~ }|
{2:define jump }{1:list}{2: > }|
:sign list^ |
]]}
-- Looped back to the original value
feed('<Tab><Tab><Tab><Tab>')
screen:expect{grid=[[
|
{0:~ }|
{0:~ }|
{2:define jump list > }|
:sign ^ |
]]}
-- Test that the wild menu is cleared properly
feed('<Space>')
screen:expect{grid=[[
|
{0:~ }|
{0:~ }|
{0:~ }|
:sign ^ |
]]}
-- Test that a different wildchar still works
feed('<Esc>')
command('set wildchar=<Esc>')
feed(':sign <Esc>')
screen:expect{grid=[[
|
{0:~ }|
{0:~ }|
{1:define}{2: jump list > }|
:sign define^ |
]]}
-- Double-<Esc> is a hard-coded method to escape while wildchar=<Esc>. Make
-- sure clean up is properly done in edge case like this.
feed('<Esc>')
screen:expect{grid=[[
^ |
{0:~ }|
{0:~ }|
{0:~ }|
|
]]}
end)
it('C-E to cancel wildmenu completion restore original input', function()
feed(':sign <tab>')
screen:expect([[
|
~ |
~ |
define jump list > |
:sign define^ |
]])
feed('<C-E>')
screen:expect([[
|
~ |
~ |
~ |
:sign ^ |
]])
end)
it('C-Y to apply selection and end wildmenu completion', function()
feed(':sign <tab>')
screen:expect([[
|
~ |
~ |
define jump list > |
:sign define^ |
]])
feed('<tab><C-Y>')
screen:expect([[
|
~ |
~ |
~ |
:sign jump^ |
]])
end)
it(':sign <tab> shows wildmenu completions', function()
command('set wildmenu wildmode=full')
feed(':sign <tab>')
screen:expect([[
|
~ |
~ |
define jump list > |
:sign define^ |
]])
end)
it(':sign <tab> <space> hides wildmenu #8453', function()
command('set wildmode=full')
-- only a regression if status-line open
command('set laststatus=2')
command('set wildmenu')
feed(':sign <tab>')
screen:expect([[
|
~ |
~ |
define jump list > |
:sign define^ |
]])
feed('<space>')
screen:expect([[
|
~ |
~ |
[No Name] |
:sign define ^ |
]])
end)
it('does not crash after cycling back to original text', function()
command('set wildmode=full')
feed(':j<Tab><Tab><Tab>')
screen:expect([[
|
~ |
~ |
join jumps |
:j^ |
]])
-- This would cause nvim to crash before #6650
feed('<BS><Tab>')
screen:expect([[
|
~ |
~ |
! # & < = > @ > |
:!^ |
]])
end)
it('is preserved during :terminal activity', function()
command('set wildmenu wildmode=full')
command('set scrollback=4')
feed((':terminal "%s" REP 5000 !terminal_output!<cr>'):format(testprg('shell-test')))
feed('G') -- Follow :terminal output.
feed([[:sign <Tab>]]) -- Invoke wildmenu.
-- NB: in earlier versions terminal output was redrawn during cmdline mode.
-- For now just assert that the screen remains unchanged.
screen:expect{any='define jump list > |\n:sign define^ |'}
screen:expect_unchanged()
-- cmdline CTRL-D display should also be preserved.
feed([[<C-U>]])
feed([[sign <C-D>]]) -- Invoke cmdline CTRL-D.
screen:expect{grid=[[
:sign |
define place |
jump undefine |
list unplace |
:sign ^ |
]]}
screen:expect_unchanged()
-- Exiting cmdline should show the buffer.
feed([[<C-\><C-N>]])
screen:expect{any=[[!terminal_output!]]}
end)
it('ignores :redrawstatus called from a timer #7108', function()
command('set wildmenu wildmode=full')
command([[call timer_start(10, {->execute('redrawstatus')}, {'repeat':-1})]])
feed([[<C-\><C-N>]])
feed([[:sign <Tab>]]) -- Invoke wildmenu.
screen:expect{grid=[[
|
~ |
~ |
define jump list > |
:sign define^ |
]]}
screen:expect_unchanged()
end)
it('with laststatus=0, :vsplit, :term #2255', function()
-- Because this test verifies a _lack_ of activity after screen:sleep(), we
-- must wait the full timeout. So make it reasonable.
screen.timeout = 1000
if not is_os('win') then
command('set shell=sh') -- Need a predictable "$" prompt.
command('let $PS1 = "$"')
end
command('set laststatus=0')
command('vsplit')
command('term')
-- Check for a shell prompt to verify that the terminal loaded.
retry(nil, nil, function()
if is_os('win') then
eq('Microsoft', eval("matchstr(join(getline(1, '$')), 'Microsoft')"))
else
eq('$', eval([[matchstr(getline(1), '\$')]]))
end
end)
feed([[<C-\><C-N>]])
feed([[:<Tab>]]) -- Invoke wildmenu.
-- Check only the last 2 lines, because the shell output is
-- system-dependent.
screen:expect{any='! # & < = > @ > |\n:!^'}
screen:expect_unchanged()
end)
it('wildmode=list,full and messages interaction #10092', function()
-- Need more than 5 rows, else tabline is covered and will be redrawn.
screen:try_resize(25, 7)
command('set wildmenu wildmode=list,full')
command('set showtabline=2')
feed(':set wildm<tab>')
screen:expect([[
[No Name] |
|
~ |
|
:set wildm |
wildmenu wildmode |
:set wildm^ |
]])
feed('<tab>') -- trigger wildmode full
screen:expect([[
[No Name] |
|
|
:set wildm |
wildmenu wildmode |
wildmenu wildmode |
:set wildmenu^ |
]])
feed('<Esc>')
screen:expect([[
[No Name] |
^ |
~ |
~ |
~ |
~ |
|
]])
end)
it('wildmode=longest,list', function()
-- Need more than 5 rows, else tabline is covered and will be redrawn.
screen:try_resize(25, 7)
command('set wildmenu wildmode=longest,list')
-- give wildmode-longest something to expand to
feed(':sign u<tab>')
screen:expect([[
|
~ |
~ |
~ |
~ |
~ |
:sign un^ |
]])
feed('<tab>') -- trigger wildmode list
screen:expect([[
|
~ |
~ |
|
:sign un |
undefine unplace |
:sign un^ |
]])
feed('<Esc>')
screen:expect([[
^ |
~ |
~ |
~ |
~ |
~ |
|
]])
-- give wildmode-longest something it cannot expand, use list
feed(':sign un<tab>')
screen:expect([[
|
~ |
~ |
|
:sign un |
undefine unplace |
:sign un^ |
]])
feed('<tab>')
screen:expect_unchanged()
feed('<Esc>')
screen:expect([[
^ |
~ |
~ |
~ |
~ |
~ |
|
]])
end)
it('wildmode=list,longest', function()
-- Need more than 5 rows, else tabline is covered and will be redrawn.
screen:try_resize(25, 7)
command('set wildmenu wildmode=list,longest')
feed(':sign u<tab>')
screen:expect([[
|
~ |
~ |
|
:sign u |
undefine unplace |
:sign u^ |
]])
feed('<tab>') -- trigger wildmode longest
screen:expect([[
|
~ |
~ |
|
:sign u |
undefine unplace |
:sign un^ |
]])
feed('<Esc>')
screen:expect([[
^ |
~ |
~ |
~ |
~ |
~ |
|
]])
end)
it('multiple <C-D> renders correctly', function()
screen:try_resize(25, 7)
command('set laststatus=2')
feed(':set wildm')
feed('<c-d>')
screen:expect([[
|
~ |
~ |
|
:set wildm |
wildmenu wildmode |
:set wildm^ |
]])
feed('<c-d>')
screen:expect([[
|
|
:set wildm |
wildmenu wildmode |
:set wildm |
wildmenu wildmode |
:set wildm^ |
]])
feed('<Esc>')
screen:expect([[
^ |
~ |
~ |
~ |
~ |
[No Name] |
|
]])
end)
it('works with c_CTRL_Z standard mapping', function()
screen:set_default_attr_ids {
[1] = {bold = true, foreground = Screen.colors.Blue1};
[2] = {foreground = Screen.colors.Grey0, background = Screen.colors.Yellow};
[3] = {bold = true, reverse = true};
}
-- Wildcharm? where we are going we aint't no need no wildcharm.
eq(0, meths.get_option_value('wildcharm', {}))
-- Don't mess the defaults yet (neovim is about backwards compatibility)
eq(9, meths.get_option_value('wildchar', {}))
-- Lol what is cnoremap? Some say it can define mappings.
command 'set wildchar=0'
eq(0, meths.get_option_value('wildchar', {}))
command 'cnoremap <f2> <c-z>'
feed(':syntax <f2>')
screen:expect{grid=[[
|
{1:~ }|
{1:~ }|
{2:case}{3: clear cluster > }|
:syntax case^ |
]]}
feed '<esc>'
command 'set wildmode=longest:full,full'
-- this will get cleaner once we have native lua expr mappings:
command [[cnoremap <expr> <tab> luaeval("not rawset(_G, 'coin', not coin).coin") ? "<c-z>" : "c"]]
feed ':syntax <tab>'
screen:expect{grid=[[
|
{1:~ }|
{1:~ }|
{1:~ }|
:syntax c^ |
]]}
feed '<tab>'
screen:expect{grid=[[
|
{1:~ }|
{1:~ }|
{3:case clear cluster > }|
:syntax c^ |
]]}
feed '<tab>'
screen:expect{grid=[[
|
{1:~ }|
{1:~ }|
{1:~ }|
:syntax cc^ |
]]}
end)
end)
describe('command line completion', function()
local screen
before_each(function()
clear()
screen = Screen.new(40, 5)
screen:set_default_attr_ids({
[1] = {bold = true, foreground = Screen.colors.Blue1},
[2] = {foreground = Screen.colors.Grey0, background = Screen.colors.Yellow},
[3] = {bold = true, reverse = true},
})
screen:attach()
end)
after_each(function()
os.remove('Xtest-functional-viml-compl-dir')
end)
it('lists directories with empty PATH', function()
local tmp = funcs.tempname()
command('e '.. tmp)
command('cd %:h')
command("call mkdir('Xtest-functional-viml-compl-dir')")
command('let $PATH=""')
feed(':!<tab><bs>')
screen:expect([[
|
{1:~ }|
{1:~ }|
{1:~ }|
:!Xtest-functional-viml-compl-dir^ |
]])
end)
it('completes env var names #9681', function()
command('let $XTEST_1 = "foo" | let $XTEST_2 = "bar"')
command('set wildmenu wildmode=full')
feed(':!echo $XTEST_<tab>')
screen:expect([[
|
{1:~ }|
{1:~ }|
{2:XTEST_1}{3: XTEST_2 }|
:!echo $XTEST_1^ |
]])
end)
it('completes (multibyte) env var names #9655', function()
clear({env={
['XTEST_1AaあB']='foo',
['XTEST_2']='bar',
}})
screen:attach()
command('set wildmenu wildmode=full')
feed(':!echo $XTEST_<tab>')
screen:expect([[
|
{1:~ }|
{1:~ }|
{2:XTEST_1AaあB}{3: XTEST_2 }|
:!echo $XTEST_1AaあB^ |
]])
end)
it('does not leak memory with <S-Tab> with wildmenu and only one match #19874', function()
meths.set_option_value('wildmenu', true, {})
meths.set_option_value('wildmode', 'full', {})
meths.set_option_value('wildoptions', 'pum', {})
feed(':sign unpla<S-Tab>')
screen:expect([[
|
{1:~ }|
{1:~ }|
{1:~ }|
:sign unplace^ |
]])
feed('<Space>buff<Tab>')
screen:expect([[
|
{1:~ }|
{1:~ }|
{1:~ }|
:sign unplace buffer=^ |
]])
end)
it('does not show matches with <S-Tab> without wildmenu with wildmode=full', function()
meths.set_option_value('wildmenu', false, {})
meths.set_option_value('wildmode', 'full', {})
feed(':sign <S-Tab>')
screen:expect([[
|
{1:~ }|
{1:~ }|
{1:~ }|
:sign unplace^ |
]])
end)
it('shows matches with <S-Tab> without wildmenu with wildmode=list', function()
meths.set_option_value('wildmenu', false, {})
meths.set_option_value('wildmode', 'list', {})
feed(':sign <S-Tab>')
screen:expect([[
{3: }|
:sign define |
define list undefine |
jump place unplace |
:sign unplace^ |
]])
end)
end)
describe('ui/ext_wildmenu', function()
local screen
before_each(function()
clear()
screen = Screen.new(25, 5)
screen:attach({rgb=true, ext_wildmenu=true})
end)
it('works with :sign <tab>', function()
local expected = {
'define',
'jump',
'list',
'place',
'undefine',
'unplace',
}
command('set wildmode=full')
command('set wildmenu')
feed(':sign <tab>')
screen:expect{grid=[[
|
~ |
~ |
~ |
:sign define^ |
]], wildmenu_items=expected, wildmenu_pos=0}
feed('<tab>')
screen:expect{grid=[[
|
~ |
~ |
~ |
:sign jump^ |
]], wildmenu_items=expected, wildmenu_pos=1}
feed('<left><left>')
screen:expect{grid=[[
|
~ |
~ |
~ |
:sign ^ |
]], wildmenu_items=expected, wildmenu_pos=-1}
feed('<right>')
screen:expect{grid=[[
|
~ |
~ |
~ |
:sign define^ |
]], wildmenu_items=expected, wildmenu_pos=0}
feed('a')
screen:expect{grid=[[
|
~ |
~ |
~ |
:sign definea^ |
]]}
end)
end)