mirror of
https://github.com/neovim/neovim.git
synced 2025-01-01 17:23:36 -07:00
272ef27115
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>
682 lines
20 KiB
Lua
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)
|