Mercurial > vim
view src/testdir/test_cursor_func.vim @ 33811:06219b3bdaf3 v9.0.2121
patch 9.0.2121: [security]: use-after-free in ex_substitute
Commit: https://github.com/vim/vim/commit/26c11c56888d01e298cd8044caf860f3c26f57bb
Author: Christian Brabandt <cb@256bit.org>
Date: Wed Nov 22 21:26:41 2023 +0100
patch 9.0.2121: [security]: use-after-free in ex_substitute
Problem: [security]: use-after-free in ex_substitute
Solution: always allocate memory
closes: #13552
A recursive :substitute command could cause a heap-use-after free in Vim
(CVE-2023-48706).
The whole reproducible test is a bit tricky, I can only reproduce this
reliably when no previous substitution command has been used yet
(which is the reason, the test needs to run as first one in the
test_substitute.vim file) and as a combination of the `:~` command
together with a :s command that contains the special substitution atom `~\=`
which will make use of a sub-replace special atom and calls a vim script
function.
There was a comment in the existing :s code, that already makes the
`sub` variable allocate memory so that a recursive :s call won't be able
to cause any issues here, so this was known as a potential problem
already. But for the current test-case that one does not work, because
the substitution does not start with `\=` but with `~\=` (and since
there does not yet exist a previous substitution atom, Vim will simply
increment the `sub` pointer (which then was not allocated dynamically)
and later one happily use a sub-replace special expression (which could
then free the `sub` var).
The following commit fixes this, by making the sub var always using
allocated memory, which also means we need to free the pointer whenever
we leave the function. Since sub is now always an allocated variable,
we also do no longer need the sub_copy variable anymore, since this one
was used to indicated when sub pointed to allocated memory (and had
therefore to be freed on exit) and when not.
Github Security Advisory:
https://github.com/vim/vim/security/advisories/GHSA-c8qm-x72m-q53q
Signed-off-by: Christian Brabandt <cb@256bit.org>
author | Christian Brabandt <cb@256bit.org> |
---|---|
date | Wed, 22 Nov 2023 22:15:05 +0100 |
parents | bfe07ef45143 |
children | 28e1e956f42c |
line wrap: on
line source
" Tests for cursor() and other functions that get/set the cursor position source check.vim func Test_wrong_arguments() call assert_fails('call cursor(1. 3)', 'E474:') call assert_fails('call cursor(test_null_list())', 'E474:') endfunc func Test_move_cursor() new call setline(1, ['aaa', 'bbb', 'ccc', 'ddd']) call cursor([1, 1, 0, 1]) call assert_equal([1, 1, 0, 1], getcurpos()[1:]) call cursor([4, 3, 0, 3]) call assert_equal([4, 3, 0, 3], getcurpos()[1:]) call cursor(2, 2) call assert_equal([2, 2, 0, 2], getcurpos()[1:]) " line number zero keeps the line number call cursor(0, 1) call assert_equal([2, 1, 0, 1], getcurpos()[1:]) " col number zero keeps the column call cursor(3, 0) call assert_equal([3, 1, 0, 1], getcurpos()[1:]) " below last line goes to last line eval [9, 1]->cursor() call assert_equal([4, 1, 0, 1], getcurpos()[1:]) " pass string arguments call cursor('3', '3') call assert_equal([3, 3, 0, 3], getcurpos()[1:]) call setline(1, ["\<TAB>"]) call cursor(1, 1, 1) call assert_equal([1, 1, 1], getcurpos()[1:3]) call assert_fails('call cursor(-1, -1)', 'E475:') quit! endfunc func Test_curswant_maxcol() new call setline(1, 'foo') " Test that after "$" command curswant is set to the same value as v:maxcol. normal! 1G$ call assert_equal(v:maxcol, getcurpos()[4]) call assert_equal(v:maxcol, winsaveview().curswant) quit! endfunc " Very short version of what matchparen does. function s:Highlight_Matching_Pair() let save_cursor = getcurpos() eval save_cursor->setpos('.') endfunc func Test_curswant_with_autocommand() new call setline(1, ['func()', '{', '}', '----']) autocmd! CursorMovedI * call s:Highlight_Matching_Pair() call test_override("char_avail", 1) exe "normal! 3Ga\<Down>X\<Esc>" call test_override("char_avail", 0) call assert_equal('-X---', getline(4)) autocmd! CursorMovedI * quit! endfunc " Tests for behavior of curswant with cursorcolumn/line func Test_curswant_with_cursorcolumn() new call setline(1, ['01234567', '']) exe "normal! ggf6j" call assert_equal(6, winsaveview().curswant) set cursorcolumn call assert_equal(6, winsaveview().curswant) quit! endfunc func Test_curswant_with_cursorline() new call setline(1, ['01234567', '']) exe "normal! ggf6j" call assert_equal(6, winsaveview().curswant) set cursorline call assert_equal(6, winsaveview().curswant) quit! endfunc func Test_screenpos() rightbelow new rightbelow 20vsplit call setline(1, ["\tsome text", "long wrapping line here", "next line"]) redraw let winid = win_getid() let [winrow, wincol] = win_screenpos(winid) call assert_equal({'row': winrow, \ 'col': wincol + 0, \ 'curscol': wincol + 7, \ 'endcol': wincol + 7}, winid->screenpos(1, 1)) call assert_equal({'row': winrow, \ 'col': wincol + 13, \ 'curscol': wincol + 13, \ 'endcol': wincol + 13}, winid->screenpos(1, 7)) call assert_equal({'row': winrow + 2, \ 'col': wincol + 1, \ 'curscol': wincol + 1, \ 'endcol': wincol + 1}, screenpos(winid, 2, 22)) setlocal number call assert_equal({'row': winrow + 3, \ 'col': wincol + 9, \ 'curscol': wincol + 9, \ 'endcol': wincol + 9}, screenpos(winid, 2, 22)) let wininfo = getwininfo(winid)[0] call setline(3, ['x']->repeat(wininfo.height)) call setline(line('$') + 1, 'x'->repeat(wininfo.width * 3)) setlocal nonumber display=lastline so=0 exe "normal G\<C-Y>\<C-Y>" redraw call assert_equal({'row': winrow + wininfo.height - 1, \ 'col': wincol + 7, \ 'curscol': wincol + 7, \ 'endcol': wincol + 7}, winid->screenpos(line('$'), 8)) call assert_equal({'row': 0, 'col': 0, 'curscol': 0, 'endcol': 0}, \ winid->screenpos(line('$'), 22)) 1split " w_leftcol should be subtracted setlocal nowrap normal G050zl$ redraw call assert_equal({'row': winrow + 0, \ 'col': wincol + 10 - 1, \ 'curscol': wincol + 10 - 1, \ 'endcol': wincol + 10 - 1}, \ screenpos(win_getid(), line('.'), col('.'))) " w_skipcol should be taken into account setlocal wrap normal $ redraw call assert_equal({'row': winrow + 0, \ 'col': wincol + 20 - 1, \ 'curscol': wincol + 20 - 1, \ 'endcol': wincol + 20 - 1}, \ screenpos(win_getid(), line('.'), col('.'))) call assert_equal({'row': 0, 'col': 0, 'curscol': 0, 'endcol': 0}, \ screenpos(win_getid(), line('.'), col('.') - 20)) setlocal number redraw call assert_equal({'row': winrow + 0, \ 'col': wincol + 16 - 1, \ 'curscol': wincol + 16 - 1, \ 'endcol': wincol + 16 - 1}, \ screenpos(win_getid(), line('.'), col('.'))) call assert_equal({'row': 0, 'col': 0, 'curscol': 0, 'endcol': 0}, \ screenpos(win_getid(), line('.'), col('.') - 16)) set cpoptions+=n redraw call assert_equal({'row': winrow + 0, \ 'col': wincol + 4 - 1, \ 'curscol': wincol + 4 - 1, \ 'endcol': wincol + 4 - 1}, \ screenpos(win_getid(), line('.'), col('.'))) call assert_equal({'row': 0, 'col': 0, 'curscol': 0, 'endcol': 0}, \ screenpos(win_getid(), line('.'), col('.') - 4)) wincmd + call setline(line('$') + 1, 'last line') setlocal smoothscroll normal G$ redraw call assert_equal({'row': winrow + 1, \ 'col': wincol + 4 + 9 - 1, \ 'curscol': wincol + 4 + 9 - 1, \ 'endcol': wincol + 4 + 9 - 1}, \ screenpos(win_getid(), line('.'), col('.'))) set cpoptions-=n redraw call assert_equal({'row': winrow + 1, \ 'col': wincol + 4 + 9 - 1, \ 'curscol': wincol + 4 + 9 - 1, \ 'endcol': wincol + 4 + 9 - 1}, \ screenpos(win_getid(), line('.'), col('.'))) setlocal nonumber redraw call assert_equal({'row': winrow + 1, \ 'col': wincol + 9 - 1, \ 'curscol': wincol + 9 - 1, \ 'endcol': wincol + 9 - 1}, \ screenpos(win_getid(), line('.'), col('.'))) close call assert_equal({}, screenpos(999, 1, 1)) bwipe! set display& call assert_equal(#{col: 1, row: 1, endcol: 1, curscol: 1}, screenpos(win_getid(), 1, 1)) nmenu WinBar.TEST : call assert_equal(#{col: 1, row: 2, endcol: 1, curscol: 1}, screenpos(win_getid(), 1, 1)) nunmenu WinBar.TEST endfunc func Test_screenpos_fold() CheckFeature folding enew! call setline(1, range(10)) 3,5fold redraw call assert_equal(2, screenpos(1, 2, 1).row) call assert_equal(#{col: 1, row: 3, endcol: 1, curscol: 1}, screenpos(1, 3, 1)) call assert_equal(#{col: 1, row: 3, endcol: 1, curscol: 1}, screenpos(1, 4, 1)) call assert_equal(#{col: 1, row: 3, endcol: 1, curscol: 1}, screenpos(1, 5, 1)) setlocal number call assert_equal(#{col: 5, row: 3, endcol: 5, curscol: 5}, screenpos(1, 3, 1)) call assert_equal(#{col: 5, row: 3, endcol: 5, curscol: 5}, screenpos(1, 4, 1)) call assert_equal(#{col: 5, row: 3, endcol: 5, curscol: 5}, screenpos(1, 5, 1)) call assert_equal(4, screenpos(1, 6, 1).row) bwipe! endfunc func Test_screenpos_diff() CheckFeature diff enew! call setline(1, ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i']) vnew call setline(1, ['a', 'b', 'c', 'g', 'h', 'i']) windo diffthis wincmd w call assert_equal(#{col: 3, row: 7, endcol: 3, curscol: 3}, screenpos(0, 4, 1)) call assert_equal(#{col: 3, row: 8, endcol: 3, curscol: 3}, screenpos(0, 5, 1)) exe "normal! 3\<C-E>" call assert_equal(#{col: 3, row: 4, endcol: 3, curscol: 3}, screenpos(0, 4, 1)) call assert_equal(#{col: 3, row: 5, endcol: 3, curscol: 3}, screenpos(0, 5, 1)) exe "normal! \<C-E>" call assert_equal(#{col: 3, row: 3, endcol: 3, curscol: 3}, screenpos(0, 4, 1)) call assert_equal(#{col: 3, row: 4, endcol: 3, curscol: 3}, screenpos(0, 5, 1)) exe "normal! \<C-E>" call assert_equal(#{col: 3, row: 2, endcol: 3, curscol: 3}, screenpos(0, 4, 1)) call assert_equal(#{col: 3, row: 3, endcol: 3, curscol: 3}, screenpos(0, 5, 1)) exe "normal! \<C-E>" call assert_equal(#{col: 3, row: 1, endcol: 3, curscol: 3}, screenpos(0, 4, 1)) call assert_equal(#{col: 3, row: 2, endcol: 3, curscol: 3}, screenpos(0, 5, 1)) windo diffoff bwipe! bwipe! endfunc func Test_screenpos_number() rightbelow new rightbelow 73vsplit call setline (1, repeat('x', 66)) setlocal number redraw let winid = win_getid() let [winrow, wincol] = win_screenpos(winid) let pos = screenpos(winid, 1, 66) call assert_equal(winrow, pos.row) call assert_equal(wincol + 66 + 3, pos.col) call assert_fails('echo screenpos(0, 2, 1)', 'E966:') close bwipe! endfunc " Save the visual start character position func SaveVisualStartCharPos() call add(g:VisualStartPos, getcharpos('v')) return '' endfunc " Save the current cursor character position in insert mode func SaveInsertCurrentCharPos() call add(g:InsertCurrentPos, getcharpos('.')) return '' endfunc " Test for the getcharpos() function func Test_getcharpos() call assert_fails('call getcharpos({})', 'E731:') call assert_equal([0, 0, 0, 0], getcharpos(0)) new call setline(1, ['', "01\tà4è678", 'Ⅵ', '012345678', ' │ x']) " Test for '.' and '$' normal 1G call assert_equal([0, 1, 1, 0], getcharpos('.')) call assert_equal([0, 5, 1, 0], getcharpos('$')) normal 2G6l call assert_equal([0, 2, 7, 0], getcharpos('.')) normal 3G$ call assert_equal([0, 3, 1, 0], getcharpos('.')) normal 4G$ call assert_equal([0, 4, 9, 0], getcharpos('.')) " Test for a mark normal 2G7lmmgg call assert_equal([0, 2, 8, 0], getcharpos("'m")) delmarks m call assert_equal([0, 0, 0, 0], getcharpos("'m")) " Check mark does not move normal 5Gfxma call assert_equal([0, 5, 5, 0], getcharpos("'a")) call assert_equal([0, 5, 5, 0], getcharpos("'a")) call assert_equal([0, 5, 5, 0], getcharpos("'a")) " Test for the visual start column vnoremap <expr> <F3> SaveVisualStartCharPos() let g:VisualStartPos = [] exe "normal 2G6lv$\<F3>ohh\<F3>o\<F3>" call assert_equal([[0, 2, 7, 0], [0, 2, 10, 0], [0, 2, 5, 0]], g:VisualStartPos) call assert_equal([0, 2, 9, 0], getcharpos('v')) let g:VisualStartPos = [] exe "normal 3Gv$\<F3>o\<F3>" call assert_equal([[0, 3, 1, 0], [0, 3, 2, 0]], g:VisualStartPos) let g:VisualStartPos = [] exe "normal 1Gv$\<F3>o\<F3>" call assert_equal([[0, 1, 1, 0], [0, 1, 1, 0]], g:VisualStartPos) vunmap <F3> " Test for getting the position in insert mode with the cursor after the " last character in a line inoremap <expr> <F3> SaveInsertCurrentCharPos() let g:InsertCurrentPos = [] exe "normal 1GA\<F3>" exe "normal 2GA\<F3>" exe "normal 3GA\<F3>" exe "normal 4GA\<F3>" exe "normal 2G6li\<F3>" call assert_equal([[0, 1, 1, 0], [0, 2, 10, 0], [0, 3, 2, 0], [0, 4, 10, 0], \ [0, 2, 7, 0]], g:InsertCurrentPos) iunmap <F3> %bw! endfunc " Test for the setcharpos() function func Test_setcharpos() call assert_equal(-1, setcharpos('.', test_null_list())) new call setline(1, ['', "01\tà4è678", 'Ⅵ', '012345678']) call setcharpos('.', [0, 1, 1, 0]) call assert_equal([1, 1], [line('.'), col('.')]) call setcharpos('.', [0, 2, 7, 0]) call assert_equal([2, 9], [line('.'), col('.')]) call setcharpos('.', [0, 3, 4, 0]) call assert_equal([3, 1], [line('.'), col('.')]) call setcharpos('.', [0, 3, 1, 0]) call assert_equal([3, 1], [line('.'), col('.')]) call setcharpos('.', [0, 4, 0, 0]) call assert_equal([4, 1], [line('.'), col('.')]) call setcharpos('.', [0, 4, 20, 0]) call assert_equal([4, 9], [line('.'), col('.')]) " Test for mark delmarks m call setcharpos("'m", [0, 2, 9, 0]) normal `m call assert_equal([2, 11], [line('.'), col('.')]) " unload the buffer and try to set the mark let bnr = bufnr() enew! call assert_equal(-1, setcharpos("'m", [bnr, 2, 2, 0])) %bw! call assert_equal(-1, setcharpos('.', [10, 3, 1, 0])) endfunc func SaveVisualStartCharCol() call add(g:VisualStartCol, charcol('v')) return '' endfunc func SaveInsertCurrentCharCol() call add(g:InsertCurrentCol, charcol('.')) return '' endfunc " Test for the charcol() function func Test_charcol() call assert_fails('call charcol({})', 'E1222:') call assert_fails('call charcol(".", [])', 'E1210:') call assert_fails('call charcol(0)', 'E1222:') new call setline(1, ['', "01\tà4è678", 'Ⅵ', '012345678']) " Test for '.' and '$' normal 1G call assert_equal(1, charcol('.')) call assert_equal(1, charcol('$')) normal 2G6l call assert_equal(7, charcol('.')) call assert_equal(10, charcol('$')) normal 3G$ call assert_equal(1, charcol('.')) call assert_equal(2, charcol('$')) normal 4G$ call assert_equal(9, charcol('.')) call assert_equal(10, charcol('$')) " Test for [lnum, '$'] call assert_equal(1, charcol([1, '$'])) call assert_equal(10, charcol([2, '$'])) call assert_equal(2, charcol([3, '$'])) call assert_equal(0, charcol([5, '$'])) " Test for a mark normal 2G7lmmgg call assert_equal(8, charcol("'m")) delmarks m call assert_equal(0, charcol("'m")) " Test for the visual start column vnoremap <expr> <F3> SaveVisualStartCharCol() let g:VisualStartCol = [] exe "normal 2G6lv$\<F3>ohh\<F3>o\<F3>" call assert_equal([7, 10, 5], g:VisualStartCol) call assert_equal(9, charcol('v')) let g:VisualStartCol = [] exe "normal 3Gv$\<F3>o\<F3>" call assert_equal([1, 2], g:VisualStartCol) let g:VisualStartCol = [] exe "normal 1Gv$\<F3>o\<F3>" call assert_equal([1, 1], g:VisualStartCol) vunmap <F3> " Test for getting the column number in insert mode with the cursor after " the last character in a line inoremap <expr> <F3> SaveInsertCurrentCharCol() let g:InsertCurrentCol = [] exe "normal 1GA\<F3>" exe "normal 2GA\<F3>" exe "normal 3GA\<F3>" exe "normal 4GA\<F3>" exe "normal 2G6li\<F3>" call assert_equal([1, 10, 2, 10, 7], g:InsertCurrentCol) iunmap <F3> " Test for getting the column number in another window. let winid = win_getid() new call win_execute(winid, 'normal 1G') call assert_equal(1, charcol('.', winid)) call assert_equal(1, charcol('$', winid)) call win_execute(winid, 'normal 2G6l') call assert_equal(7, charcol('.', winid)) call assert_equal(10, charcol('$', winid)) " calling from another tab page also works tabnew call assert_equal(7, charcol('.', winid)) call assert_equal(10, charcol('$', winid)) tabclose " unknown window ID call assert_equal(0, charcol('.', 10001)) %bw! endfunc func SaveInsertCursorCharPos() call add(g:InsertCursorPos, getcursorcharpos('.')) return '' endfunc " Test for getcursorcharpos() func Test_getcursorcharpos() call assert_equal(getcursorcharpos(), getcursorcharpos(0)) call assert_equal([0, 0, 0, 0, 0], getcursorcharpos(-1)) call assert_equal([0, 0, 0, 0, 0], getcursorcharpos(1999)) new call setline(1, ['', "01\tà4è678", 'Ⅵ', '012345678']) normal 1G9l call assert_equal([0, 1, 1, 0, 1], getcursorcharpos()) normal 2G9l call assert_equal([0, 2, 9, 0, 14], getcursorcharpos()) normal 3G9l call assert_equal([0, 3, 1, 0, 1], getcursorcharpos()) normal 4G9l call assert_equal([0, 4, 9, 0, 9], getcursorcharpos()) " Test for getting the cursor position in insert mode with the cursor after " the last character in a line inoremap <expr> <F3> SaveInsertCursorCharPos() let g:InsertCursorPos = [] exe "normal 1GA\<F3>" exe "normal 2GA\<F3>" exe "normal 3GA\<F3>" exe "normal 4GA\<F3>" exe "normal 2G6li\<F3>" call assert_equal([[0, 1, 1, 0, 1], [0, 2, 10, 0, 15], [0, 3, 2, 0, 2], \ [0, 4, 10, 0, 10], [0, 2, 7, 0, 12]], g:InsertCursorPos) iunmap <F3> let winid = win_getid() normal 2G5l wincmd w call assert_equal([0, 2, 6, 0, 11], getcursorcharpos(winid)) %bw! endfunc " Test for setcursorcharpos() func Test_setcursorcharpos() call assert_fails('call setcursorcharpos(test_null_list())', 'E474:') call assert_fails('call setcursorcharpos([1])', 'E474:') call assert_fails('call setcursorcharpos([1, 1, 1, 1, 1])', 'E474:') new call setline(1, ['', "01\tà4è678", 'Ⅵ', '012345678']) normal G call setcursorcharpos([1, 1]) call assert_equal([1, 1], [line('.'), col('.')]) call setcursorcharpos([2, 7, 0]) call assert_equal([2, 9], [line('.'), col('.')]) call setcursorcharpos([0, 7, 0]) call assert_equal([2, 9], [line('.'), col('.')]) call setcursorcharpos(0, 7, 0) call assert_equal([2, 9], [line('.'), col('.')]) call setcursorcharpos(3, 4) call assert_equal([3, 1], [line('.'), col('.')]) call setcursorcharpos([3, 1]) call assert_equal([3, 1], [line('.'), col('.')]) call setcursorcharpos([4, 0, 0, 0]) call assert_equal([4, 1], [line('.'), col('.')]) call setcursorcharpos([4, 20]) call assert_equal([4, 9], [line('.'), col('.')]) normal 1G call setcursorcharpos([100, 100, 100, 100]) call assert_equal([4, 9], [line('.'), col('.')]) normal 1G call setcursorcharpos('$', 1) call assert_equal([4, 1], [line('.'), col('.')]) %bw! endfunc " Test for virtcol2col() func Test_virtcol2col() new call setline(1, ["a\tb\tc"]) call assert_equal(1, virtcol2col(0, 1, 1)) call assert_equal(2, virtcol2col(0, 1, 2)) call assert_equal(2, virtcol2col(0, 1, 8)) call assert_equal(3, virtcol2col(0, 1, 9)) call assert_equal(4, virtcol2col(0, 1, 10)) call assert_equal(4, virtcol2col(0, 1, 16)) call assert_equal(5, virtcol2col(0, 1, 17)) call assert_equal(-1, virtcol2col(10, 1, 1)) call assert_equal(-1, virtcol2col(0, 10, 1)) call assert_equal(-1, virtcol2col(0, -1, 1)) call assert_equal(-1, virtcol2col(0, 1, -1)) call assert_equal(5, virtcol2col(0, 1, 20)) " Multibyte character call setline(1, ['a✅✅✅']) call assert_equal(1, virtcol2col(0, 1, 1)) call assert_equal(2, virtcol2col(0, 1, 3)) call assert_equal(5, virtcol2col(0, 1, 5)) call assert_equal(8, virtcol2col(0, 1, 7)) call assert_equal(8, virtcol2col(0, 1, 8)) " These used to cause invalid memory access call setline(1, '') call assert_equal(0, virtcol2col(0, 1, 1)) call assert_equal(0, virtcol2col(0, 1, 2)) let w = winwidth(0) call setline(2, repeat('a', w + 2)) let win_nosbr = win_getid() split setlocal showbreak=!! let win_sbr = win_getid() call assert_equal(w, virtcol2col(win_nosbr, 2, w)) call assert_equal(w + 1, virtcol2col(win_nosbr, 2, w + 1)) call assert_equal(w + 2, virtcol2col(win_nosbr, 2, w + 2)) call assert_equal(w + 2, virtcol2col(win_nosbr, 2, w + 3)) call assert_equal(w, virtcol2col(win_sbr, 2, w)) call assert_equal(w + 1, virtcol2col(win_sbr, 2, w + 1)) call assert_equal(w + 1, virtcol2col(win_sbr, 2, w + 2)) call assert_equal(w + 1, virtcol2col(win_sbr, 2, w + 3)) call assert_equal(w + 2, virtcol2col(win_sbr, 2, w + 4)) call assert_equal(w + 2, virtcol2col(win_sbr, 2, w + 5)) close call assert_fails('echo virtcol2col("0", 1, 20)', 'E1210:') call assert_fails('echo virtcol2col(0, "1", 20)', 'E1210:') call assert_fails('echo virtcol2col(0, 1, "1")', 'E1210:') bw! endfunc " vim: shiftwidth=2 sts=2 expandtab