Skip to content

Commit bf21df1

Browse files
seandewarchrisbra
authored andcommitted
patch 9.2.0253: various issues with wrong b_nwindows after closing buffers
Problem: close_buffer() callers incorrectly handle b_nwindows, especially after nasty autocmds, allowing it to go out-of-sync. May lead to buffers that can't be unloaded, or buffers that are prematurely freed whilst displayed. Solution: Modify close_buffer() and review its callers; let them decrement b_nwindows if it didn't unload the buffer. Remove some now unneeded workarounds like 8.2.2354, 9.1.0143, 9.1.0764, which didn't always work (Sean Dewar) close_buffer() now doesn't decrement b_nwindows when not unloading buf, or when buf isn't w_buffer after autocmds (they would've already decremented it). Callers are now expected to decrement b_nwindows if w_buffer is not NULL after close_buffer(), and when still intending to switch buffers or close win, for two reasons: - close_buffer() autocmds may have switched buffers. The new w_buffer's b_nwindows would also need decrementing. - After close_buffer(), callers may opt to not switch w_buffer or close win. b_nwindows would need to be incremented again. (unless w_buffer is NULL from being unloaded; callers are already forced to find a new buffer then) These were the main causes of b_nwindows bugs, as these cases could not be reliably detected, and went largely unhandled. NOTE: if close_buffer() autocmds switch buffers, close_buffer() is not called for that new buffer before decrementing b_nwindows. This may skip side-effects like from 'bufhidden', but I think it's mostly harmless, and was already happening in other places. Let's see how this goes... Other details: (I have lots to say!) It's OK to pass a win to close_buffer() that isn't showing buf (used by set_curbuf()). In that case, we skip some side-effects and don't decrement b_nwindows, but may still unload buf if hidden. buf_freeall() now returns whether it freed anything. Removes some repeated checks in close_buffer(). Preserve close_buffer()'s behaviour when called by win_free_popup() after its popup was already removed from the window list. This made win_valid_any_tab() return FALSE, so we skip things that originally checked it in that case. Add "set_context" to close_buffer() to preserve do_ecmd()'s behaviour of only setting b_last_cursor and/or calling buflist_setfpos() when not splitting (see 7.2.041: https://groups.google.com/g/vim_dev/c/ZGgNvaylNzI/m/WHxjhnuxqB0J) Without this, Test_marks_cmd() fails from its ' mark differing. Don't use oldwin though; it's not always the window with the closed buf, especially after BufLeave autocmds in do_ecmd(). Also, only set context if win is really displaying buf. Don't bail in do_ecmd() if buf was deleted but curwin->w_buffer is NULL; that leaves curwin open to a NULL buffer! Use lastbuf instead, like set_curbuf(). I don't think it's possible for buf to be deleted by close_buffer() anyway, as b_locked was set (which I can't see a way to bypass, unlike b_locked_split). Maybe such checks can be removed, but I'd rather not risk that here. Don't set curwin to previouswin in set_curbuf(); shouldn't be needed, otherwise may lead to curbuf != curwin->w_buffer if autocmds switch to a window showing buf, as that skips enter_buffer()? Was introduced back in 7.3.557 to avoid cases where autocmds switch windows, possibly leaving previouswin with a NULL buffer. Since 7.4.2312 and 7.4.2328, close_buffer() and buf_freeall() already handles this. I've added an assert() as a sanity check anyway. In free_all_mem(), set b_nwindows to 0 before close_buffer() so buffers can be wiped if still in a window before win_free_all(). Needed as close_buffer() now skips unloading buffers that aren't hidden if win is NULL. If it's possible for free_all_mem()'s :tabonly! and :only! to not close all windows before freeing, then this issue was also previously possible if b_nwindows > 1. related: #19728 Signed-off-by: Sean Dewar <6256228+seandewar@users.noreply.github.com> Signed-off-by: Christian Brabandt <cb@256bit.org>
1 parent a8fdfd4 commit bf21df1

11 files changed

Lines changed: 286 additions & 128 deletions

File tree

src/alloc.c

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -496,7 +496,9 @@ free_all_mem(void)
496496

497497
set_bufref(&bufref, buf);
498498
nextbuf = buf->b_next;
499-
close_buffer(NULL, buf, DOBUF_WIPE, FALSE, FALSE);
499+
// All windows were freed. Reset b_nwindows so buffers can be wiped.
500+
buf->b_nwindows = 0;
501+
close_buffer(NULL, buf, DOBUF_WIPE, FALSE, FALSE, FALSE);
500502
if (bufref_valid(&bufref))
501503
buf = nextbuf; // didn't work, try next one
502504
else

src/buffer.c

Lines changed: 74 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ open_buffer(
227227
{
228228
// There MUST be a memfile, otherwise we can't do anything
229229
// If we can't create one for the current buffer, take another buffer
230-
close_buffer(NULL, curbuf, 0, FALSE, FALSE);
230+
close_buffer(curwin, curbuf, 0, FALSE, FALSE, FALSE);
231231
FOR_ALL_BUFFERS(curbuf)
232232
if (curbuf->b_ml.ml_mfp != NULL)
233233
break;
@@ -545,36 +545,52 @@ can_unload_buffer(buf_T *buf)
545545
* DOBUF_DEL buffer is unloaded and removed from buffer list
546546
* DOBUF_WIPE buffer is unloaded and really deleted
547547
* DOBUF_WIPE_REUSE idem, and add to buf_reuse list
548-
* When doing all but the first one on the current buffer, the caller should
549-
* get a new buffer very soon!
548+
* When doing all but the first, the caller should get a new buffer very soon!
550549
*
551550
* The 'bufhidden' option can force freeing and deleting.
552551
*
552+
* When "win" is not NULL and we reached the end and unloaded "buf", b_nwindows
553+
* is decremented if w_buffer was "buf" after autocmds. w_buffer is set to
554+
* NULL in that case. Otherwise, w_buffer->b_nwindows is not decremented;
555+
* callers should decrement it if they still intend to switch "win"'s buffer or
556+
* close "win"! If "win" was initially curwin displaying "buf", it is
557+
* re-entered if autocmds switched windows, if still open.
558+
*
553559
* When "abort_if_last" is TRUE then do not close the buffer if autocommands
554560
* cause there to be only one window with this buffer. e.g. when ":quit" is
555561
* supposed to close the window but autocommands close all other windows.
556562
*
557563
* When "ignore_abort" is TRUE don't abort even when aborting() returns TRUE.
558564
*
559-
* Return TRUE when we got to the end and b_nwindows was decremented.
565+
* When "set_context" is TRUE, also call buflist_setfpos for "win" if it's
566+
* showing "buf", and set b_last_cursor if "win" is the buffer's only window.
567+
* Not done for closing popups.
568+
*
569+
* Return TRUE when we got to the end and unloaded "buf".
560570
*/
561571
int
562572
close_buffer(
563-
win_T *win, // if not NULL, set b_last_cursor
573+
win_T *win,
564574
buf_T *buf,
565575
int action,
566576
int abort_if_last,
567-
int ignore_abort)
577+
int ignore_abort,
578+
int set_context)
568579
{
569-
int is_curbuf;
570-
int nwindows;
580+
int hiding_buf;
571581
bufref_T bufref;
572-
int is_curwin = (curwin != NULL && curwin->w_buffer == buf);
573-
win_T *the_curwin = curwin;
582+
int is_curwin = (curwin != NULL && curwin == win
583+
&& curwin->w_buffer == buf);
574584
tabpage_T *the_curtab = curtab;
575585
int unload_buf = (action != 0);
576586
int wipe_buf = (action == DOBUF_WIPE || action == DOBUF_WIPE_REUSE);
577587
int del_buf = (action == DOBUF_DEL || wipe_buf);
588+
int win_valid = win_valid_any_tab(win);
589+
// win_valid is FALSE for closing popups, as they're first removed from the
590+
// window list. If so, skip some side-effects, but treat win as valid.
591+
// Shouldn't be possible for autocmds to prematurely free win then, as it's
592+
// not in the window list.
593+
int closed_popup = win && !win_valid && WIN_IS_POPUP(win);
578594

579595
CHECK_CURBUF;
580596

@@ -649,8 +665,7 @@ close_buffer(
649665
if ((del_buf || wipe_buf) && !can_unload_buffer(buf))
650666
return FALSE;
651667

652-
// check no autocommands closed the window
653-
if (win != NULL && win_valid_any_tab(win))
668+
if (set_context && win_valid && win->w_buffer == buf)
654669
{
655670
// Set b_last_cursor when closing the last window for the buffer.
656671
// Remember the last cursor position and window options of the buffer.
@@ -666,7 +681,8 @@ close_buffer(
666681
set_bufref(&bufref, buf);
667682

668683
// When the buffer is no longer in a window, trigger BufWinLeave
669-
if (buf->b_nwindows == 1)
684+
if ((win_valid || closed_popup) && win->w_buffer == buf
685+
&& buf->b_nwindows == 1)
670686
{
671687
++buf->b_locked;
672688
++buf->b_locked_split;
@@ -707,32 +723,30 @@ close_buffer(
707723
if (!ignore_abort && aborting())
708724
return FALSE;
709725
#endif
726+
win_valid = win_valid && win_valid_any_tab(win);
710727
}
711728

712729
// If the buffer was in curwin and the window has changed, go back to that
713730
// window, if it still exists. This avoids that ":edit x" triggering a
714731
// "tabnext" BufUnload autocmd leaves a window behind without a buffer.
715-
if (is_curwin && curwin != the_curwin && win_valid_any_tab(the_curwin))
732+
if (is_curwin && curwin != win && win_valid)
716733
{
717734
block_autocmds();
718-
goto_tabpage_win(the_curtab, the_curwin);
735+
goto_tabpage_win(the_curtab, win);
719736
unblock_autocmds();
720737
}
721738

722-
nwindows = buf->b_nwindows;
723-
724-
// decrease the link count from windows (unless not in any window)
725-
if (buf->b_nwindows > 0)
726-
--buf->b_nwindows;
739+
// Remember if the buffer may be hidden soon, or is already hidden.
740+
hiding_buf = buf->b_nwindows <= 0 || ((win_valid || closed_popup)
741+
&& win->w_buffer == buf && buf->b_nwindows == 1);
727742

728743
#ifdef FEAT_DIFF
729-
if (diffopt_hiddenoff() && !unload_buf && buf->b_nwindows == 0)
744+
if (diffopt_hiddenoff() && !unload_buf && hiding_buf)
730745
diff_buf_delete(buf); // Clear 'diff' for hidden buffer.
731746
#endif
732747

733-
// Return when a window is displaying the buffer or when it's not
734-
// unloaded.
735-
if (buf->b_nwindows > 0 || !unload_buf)
748+
// Return when another window is displaying the buffer or when not unloaded.
749+
if (!hiding_buf || !unload_buf)
736750
return FALSE;
737751

738752
// Always remove the buffer when there is no file name.
@@ -741,42 +755,20 @@ close_buffer(
741755

742756
// Free all things allocated for this buffer.
743757
// Also calls the "BufDelete" autocommands when del_buf is TRUE.
744-
//
745-
// Remember if we are closing the current buffer. Restore the number of
746-
// windows, so that autocommands in buf_freeall() don't get confused.
747-
is_curbuf = (buf == curbuf);
748-
buf->b_nwindows = nwindows;
749-
750-
buf_freeall(buf, (del_buf ? BFA_DEL : 0)
758+
// Abort if nothing was freed, or if autocommands delete the buffer.
759+
if (!buf_freeall(buf, (del_buf ? BFA_DEL : 0)
751760
+ (wipe_buf ? BFA_WIPE : 0)
752-
+ (ignore_abort ? BFA_IGNORE_ABORT : 0));
753-
754-
// Autocommands may have deleted the buffer.
755-
if (!bufref_valid(&bufref))
761+
+ (ignore_abort ? BFA_IGNORE_ABORT : 0)))
756762
return FALSE;
757-
#ifdef FEAT_EVAL
758-
// autocmds may abort script processing
759-
if (!ignore_abort && aborting())
760-
return FALSE;
761-
#endif
762-
763-
// It's possible that autocommands change curbuf to the one being deleted.
764-
// This might cause the previous curbuf to be deleted unexpectedly. But
765-
// in some cases it's OK to delete the curbuf, because a new one is
766-
// obtained anyway. Therefore only return if curbuf changed to the
767-
// deleted buffer.
768-
if (buf == curbuf && !is_curbuf)
769-
return FALSE;
770-
771-
if (win_valid_any_tab(win) && win->w_buffer == buf)
772-
win->w_buffer = NULL; // make sure we don't use the buffer now
773763

774-
// Autocommands may have opened or closed windows for this buffer.
775-
// Decrement the count for the close we do here.
776-
// Don't decrement b_nwindows if the buffer wasn't displayed in any window
777-
// before calling buf_freeall().
778-
if (nwindows > 0 && buf->b_nwindows > 0)
764+
win_valid = win_valid && win_valid_any_tab(win);
765+
if ((win_valid || closed_popup) && win->w_buffer == buf)
766+
{
767+
// Autocommands may have opened (despite b_locked_split) or closed
768+
// windows for this buffer. Decrement for the close we do here.
779769
--buf->b_nwindows;
770+
win->w_buffer = NULL; // make sure we don't use the buffer now
771+
}
780772

781773
/*
782774
* Remove the buffer from the list.
@@ -839,6 +831,11 @@ close_buffer(
839831
buf->b_p_bl = FALSE;
840832
}
841833
// NOTE: at this point "curbuf" may be invalid!
834+
835+
// When closing curbuf for curwin, is_curwin checks should've ensured
836+
// autocmds don't switch windows, unless they closed curwin. Otherwise
837+
// callers may leave the window open to a NULL buffer!
838+
assert(!win_valid || !is_curwin || win == curwin);
842839
return TRUE;
843840
}
844841

@@ -872,8 +869,10 @@ buf_clear_file(buf_T *buf)
872869
* BFA_WIPE buffer is going to be wiped out
873870
* BFA_KEEP_UNDO do not free undo information
874871
* BFA_IGNORE_ABORT don't abort even when aborting() returns TRUE
872+
*
873+
* Return TRUE when we got to the end.
875874
*/
876-
void
875+
int
877876
buf_freeall(buf_T *buf, int flags)
878877
{
879878
int is_curbuf = (buf == curbuf);
@@ -892,23 +891,23 @@ buf_freeall(buf_T *buf, int flags)
892891
FALSE, buf)
893892
&& !bufref_valid(&bufref))
894893
// autocommands deleted the buffer
895-
return;
894+
return FALSE;
896895
}
897896
if ((flags & BFA_DEL) && buf->b_p_bl)
898897
{
899898
if (apply_autocmds(EVENT_BUFDELETE, buf->b_fname, buf->b_fname,
900899
FALSE, buf)
901900
&& !bufref_valid(&bufref))
902901
// autocommands deleted the buffer
903-
return;
902+
return FALSE;
904903
}
905904
if (flags & BFA_WIPE)
906905
{
907906
if (apply_autocmds(EVENT_BUFWIPEOUT, buf->b_fname, buf->b_fname,
908907
FALSE, buf)
909908
&& !bufref_valid(&bufref))
910909
// autocommands deleted the buffer
911-
return;
910+
return FALSE;
912911
}
913912
--buf->b_locked;
914913
--buf->b_locked_split;
@@ -926,15 +925,15 @@ buf_freeall(buf_T *buf, int flags)
926925
#ifdef FEAT_EVAL
927926
// autocmds may abort script processing
928927
if ((flags & BFA_IGNORE_ABORT) == 0 && aborting())
929-
return;
928+
return FALSE;
930929
#endif
931930

932931
// It's possible that autocommands change curbuf to the one being deleted.
933932
// This might cause curbuf to be deleted unexpectedly. But in some cases
934933
// it's OK to delete the curbuf, because a new one is obtained anyway.
935934
// Therefore only return if curbuf changed to the deleted buffer.
936935
if (buf == curbuf && !is_curbuf)
937-
return;
936+
return FALSE;
938937

939938
// If curbuf, stop Visual mode just before freeing, but after autocmds that
940939
// may restart it. May trigger TextYankPost, but with textlock set.
@@ -982,6 +981,7 @@ buf_freeall(buf_T *buf, int flags)
982981
clear_buf_prop_types(buf);
983982
#endif
984983
buf->b_flags &= ~BF_READERR; // a read error is no longer relevant
984+
return TRUE;
985985
}
986986

987987
/*
@@ -1230,11 +1230,11 @@ handle_swap_exists(bufref_T *old_curbuf)
12301230
// open a new, empty buffer.
12311231
swap_exists_action = SEA_NONE; // don't want it again
12321232
swap_exists_did_quit = TRUE;
1233-
close_buffer(curwin, curbuf, DOBUF_UNLOAD, FALSE, FALSE);
1233+
close_buffer(curwin, curbuf, DOBUF_UNLOAD, FALSE, FALSE, TRUE);
12341234
if (old_curbuf == NULL || !bufref_valid(old_curbuf)
12351235
|| old_curbuf->br_buf == curbuf)
12361236
{
1237-
// Block autocommands here because curwin->w_buffer is NULL.
1237+
// Block autocommands here because curwin->w_buffer may be NULL.
12381238
block_autocmds();
12391239
buf = buflist_new(NULL, NULL, 1L, BLN_CURBUF | BLN_LISTED);
12401240
unblock_autocmds();
@@ -1321,7 +1321,7 @@ empty_curbuf(
13211321
// the old one. But do_ecmd() may have done that already, check
13221322
// if the buffer still exists.
13231323
if (buf != curbuf && bufref_valid(&bufref) && buf->b_nwindows == 0)
1324-
close_buffer(NULL, buf, action, FALSE, FALSE);
1324+
close_buffer(NULL, buf, action, FALSE, FALSE, FALSE);
13251325
if (!close_others)
13261326
need_fileinfo = FALSE;
13271327
else if (retval == OK && !shortmess(SHM_FILEINFO))
@@ -1549,7 +1549,7 @@ do_buffer_ext(
15491549
{
15501550
close_windows(buf, FALSE);
15511551
if (buf != curbuf && bufref_valid(&bufref) && buf->b_nwindows <= 0)
1552-
close_buffer(NULL, buf, action, FALSE, FALSE);
1552+
close_buffer(NULL, buf, action, FALSE, FALSE, FALSE);
15531553
return OK;
15541554
}
15551555

@@ -1882,7 +1882,6 @@ set_curbuf(buf_T *buf, int action)
18821882
bufref_T newbufref;
18831883
bufref_T prevbufref;
18841884
int valid;
1885-
int last_winid = get_last_winid();
18861885

18871886
setpcmark();
18881887
if ((cmdmod.cmod_flags & CMOD_KEEPALT) == 0)
@@ -1896,7 +1895,6 @@ set_curbuf(buf_T *buf, int action)
18961895
prevbuf = curbuf;
18971896
set_bufref(&prevbufref, prevbuf);
18981897
set_bufref(&newbufref, buf);
1899-
int prev_nwindows = prevbuf->b_nwindows;
19001898

19011899
// Autocommands may delete the current buffer and/or the buffer we want to
19021900
// go to. In those cases don't close the buffer.
@@ -1912,34 +1910,25 @@ set_curbuf(buf_T *buf, int action)
19121910
if (prevbuf == curwin->w_buffer)
19131911
reset_synblock(curwin);
19141912
#endif
1915-
// autocommands may have opened a new window
1916-
// with prevbuf, grr
1917-
if (unload ||
1918-
(prev_nwindows <= 1 && last_winid != get_last_winid()
1919-
&& action == DOBUF_GOTO && !buf_hide(prevbuf)))
1913+
if (unload)
19201914
close_windows(prevbuf, FALSE);
19211915
#if defined(FEAT_EVAL)
19221916
if (bufref_valid(&prevbufref) && !aborting())
19231917
#else
19241918
if (bufref_valid(&prevbufref))
19251919
#endif
19261920
{
1927-
win_T *previouswin = curwin;
1928-
19291921
// Do not sync when in Insert mode and the buffer is open in
19301922
// another window, might be a timer doing something in another
19311923
// window.
19321924
if (prevbuf == curbuf
19331925
&& ((State & MODE_INSERT) == 0 || curbuf->b_nwindows <= 1))
19341926
u_sync(FALSE);
1935-
close_buffer(prevbuf == curwin->w_buffer ? curwin : NULL, prevbuf,
1927+
close_buffer(curwin, prevbuf,
19361928
unload ? action : (action == DOBUF_GOTO
19371929
&& !buf_hide(prevbuf)
19381930
&& !bufIsChanged(prevbuf)) ? DOBUF_UNLOAD : 0,
1939-
FALSE, FALSE);
1940-
if (curwin != previouswin && win_valid(previouswin))
1941-
// autocommands changed curwin, Grr!
1942-
curwin = previouswin;
1931+
FALSE, FALSE, TRUE);
19431932
}
19441933
}
19451934
// An autocommand may have deleted "buf", already entered it (e.g., when
@@ -1952,10 +1941,6 @@ set_curbuf(buf_T *buf, int action)
19521941
#endif
19531942
) || curwin->w_buffer == NULL)
19541943
{
1955-
// autocommands changed curbuf and we will move to another
1956-
// buffer soon, so decrement curbuf->b_nwindows
1957-
if (curbuf != NULL && prevbuf != curbuf)
1958-
curbuf->b_nwindows--;
19591944
// If the buffer is not valid but curwin->w_buffer is NULL we must
19601945
// enter some buffer. Using the last one is hopefully OK.
19611946
if (!valid)
@@ -1987,6 +1972,9 @@ enter_buffer(buf_T *buf)
19871972
)
19881973
end_visual_mode();
19891974

1975+
if (curwin->w_buffer != NULL)
1976+
--curwin->w_buffer->b_nwindows;
1977+
19901978
// Get the buffer in the current window.
19911979
curwin->w_buffer = buf;
19921980
curbuf = buf;
@@ -3665,7 +3653,7 @@ setfname(
36653653
return FAIL;
36663654
}
36673655
// delete from the list
3668-
close_buffer(NULL, obuf, DOBUF_WIPE, FALSE, FALSE);
3656+
close_buffer(NULL, obuf, DOBUF_WIPE, FALSE, FALSE, FALSE);
36693657
}
36703658
sfname = vim_strsave(sfname);
36713659
if (ffname == NULL || sfname == NULL)
@@ -6438,7 +6426,7 @@ wipe_buffer(
64386426
if (!aucmd) // Don't trigger BufDelete autocommands here.
64396427
block_autocmds();
64406428

6441-
close_buffer(NULL, buf, DOBUF_WIPE, FALSE, TRUE);
6429+
close_buffer(NULL, buf, DOBUF_WIPE, FALSE, TRUE, FALSE);
64426430

64436431
if (!aucmd)
64446432
unblock_autocmds();

0 commit comments

Comments
 (0)