This is surely an old bug. It's easier to explain it with the steps to
reproduce it.
First of all, this happens only when the cursor is positioned in the
last character of the file, and the last line is at the end of the
screen, when you don't see any '~' showing empty lines beyond EOF (when
vi prints those '~', it refreshes the screen and hides the bug).
In the above conditions, type '!' followed by any movement command (or
simply '!!') and then any command that produces output (stdout or
stderr), for example '!!ls'.
Important: once the shell output replaced the text affected by the
movement command, moving the cursor up or down using 'j' or 'k' will
refresh the screen and you won't see the bug; type 'u' instead, and
you'll see the affected text reprinted but with *ghost spaces appended
beyond EOF*. Same as before, if you now move the cursor with 'j' or 'k'
the screen will be refreshed and the ghost spaces will go away; type ESC
instead and you'll see the line is preserved with the corruption. If
you try to delete (eg. using 'x') those apparent extra spaces (they
aren't really spaces) they will be replaced by null-byte corruption
(<^@^@^@^@) appearing in different parts of the line. If the line is
long enogh that there is no space at the end of the screen for the ghost
spaces to appear, vi will hang and you'll have to kill it from another
terminal.
The cause: in db_get() and db_last() (common/line.c), where the cache is
updated, the line cache pointer ep->c_lp is set to point directly into
the DB volatile memory (data.data), which is often reused or dirtied by
error strings or word-alignment padding.
ALTERNATIVE SOLUTION (main canditate diff at bottom):
Just one line. Forcing the screen map to recalculate its offsets. The
diff below adds the SC_SCR_REFORMAT (SC_SCR_REDRAW also works) at the
end of db_insert(). Adding this to db_append() also works. Can be used
in both functions, and it's not a bad idea to use this in conjunction
with the other diff at bottom, as a reinforce. Solves this case but
won't prevent other parts of the code that also use DB from suffering
similar inconsistencies.
Index: common/line.c
===================================================================
RCS file: /cvs/src/usr.bin/vi/common/line.c,v
diff -u -p -u -p -r1.18 line.c
--- common/line.c 20 Apr 2026 10:30:02 -0000 1.18
+++ common/line.c 3 May 2026 14:04:19 -0000
@@ -346,6 +346,9 @@ db_insert(SCR *sp, recno_t lno, char *p,
if (ex_g_insdel(sp, LINE_INSERT, lno))
rval = 1;
+ /* Do not trust the screen's idea of where the line ends. */
+ F_SET(sp, SC_SCR_REFORMAT);
+
/* Update screen. */
return (scr_update(sp, lno, LINE_INSERT, 1) || rval);
}
DEEPER SOLUTION:
The diff at bottom is extensive, but a deeper solution. It uses realloc
and memcpy to create a private copy of the data. This is somewhat
closer to what nvi2 from ports does, where this bug is fixed
(accidentally?) by this intermediate translation layer for wide chars
support:
if (FILE2INT(sp, data.data, data.size, wp, wlen)) {
if (!F_ISSET(sp, SC_CONV_ERROR)) {
F_SET(sp, SC_CONV_ERROR);
msgq(sp, M_ERR, "324|Conversion error on line %d", lno);
}
goto err3;
}
/* Reset the cache. */
if (wp != data.data) {
BINC_GOTOW(sp, sp->c_lp, sp->c_blen, wlen);
MEMCPYW(sp->c_lp, wp, wlen);
}
sp->c_lno = lno;
sp->c_len = wlen;
Index: common/exf.c
===================================================================
RCS file: /cvs/src/usr.bin/vi/common/exf.c,v
diff -u -p -u -p -r1.51 exf.c
--- common/exf.c 20 Apr 2026 10:30:02 -0000 1.51
+++ common/exf.c 4 May 2026 13:25:07 -0000
@@ -434,6 +434,11 @@ oerr: if (F_ISSET(ep, F_RCV_ON))
ep->rcv_path = NULL;
if (ep->db != NULL)
(void)ep->db->close(ep->db);
+
+ /* Free the private cache buffer. */
+ if (ep->c_buf != NULL)
+ free(ep->c_buf);
+
free(ep);
return (open_err ?
@@ -715,6 +720,11 @@ file_end(SCR *sp, EXF *ep, int force)
(void)close(ep->rcv_fd);
free(ep->rcv_path);
free(ep->rcv_mpath);
+
+ /* Free the private cache buffer. */
+ if (ep->c_buf != NULL)
+ free(ep->c_buf);
+
free(ep);
return (0);
}
Index: common/exf.h
===================================================================
RCS file: /cvs/src/usr.bin/vi/common/exf.h,v
diff -u -p -u -p -r1.6 exf.h
--- common/exf.h 20 Feb 2022 19:45:51 -0000 1.6
+++ common/exf.h 4 May 2026 13:25:07 -0000
@@ -24,7 +24,8 @@ struct _exf {
size_t c_len; /* Cached line length. */
recno_t c_lno; /* Cached line number. */
recno_t c_nlines; /* Cached lines in the file. */
-
+ char *c_buf; /* Buffer used to reset cache */
+ size_t c_buf_len; /* Length of buffer used to reset cache
*/
DB *log; /* Log db structure. */
char *l_lp; /* Log buffer. */
size_t l_len; /* Log buffer length. */
Index: common/line.c
===================================================================
RCS file: /cvs/src/usr.bin/vi/common/line.c,v
diff -u -p -u -p -r1.18 line.c
--- common/line.c 20 Apr 2026 10:30:02 -0000 1.18
+++ common/line.c 4 May 2026 13:25:07 -0000
@@ -19,6 +19,7 @@
#include <errno.h>
#include <limits.h>
#include <stdio.h>
+#include <stdlib.h>
#include <string.h>
#include "common.h"
@@ -156,9 +157,8 @@ err3: if (lenp != NULL)
}
/* Reset the cache. */
- ep->c_lno = lno;
- ep->c_len = data.size;
- ep->c_lp = data.data;
+ if (db_cache_update(sp, ep, lno, data.data, data.size))
+ goto err3;
#if defined(DEBUG) && 0
TRACE(sp, "retrieve DB line %lu\n", (u_long)lno);
@@ -326,7 +326,7 @@ db_insert(SCR *sp, recno_t lno, char *p,
}
/* Flush the cache, update line count, before screen update. */
- if (lno >= ep->c_lno)
+ if (lno <= ep->c_lno)
ep->c_lno = OOBLNO;
if (ep->c_nlines != OOBLNO)
++ep->c_nlines;
@@ -484,9 +484,9 @@ db_last(SCR *sp, recno_t *lnop)
/* Fill the cache. */
memcpy(&lno, key.data, sizeof(lno));
+ if (db_cache_update(sp, ep, lno, data.data, data.size))
+ return (1);
ep->c_nlines = ep->c_lno = lno;
- ep->c_len = data.size;
- ep->c_lp = data.data;
/* Return the value. */
*lnop = (F_ISSET(sp, SC_TINPUT) &&
@@ -529,4 +529,30 @@ scr_update(SCR *sp, recno_t lno, lnop_t
if (vs_change(tsp, lno, op))
return (1);
return (current ? vs_change(sp, lno, op) : 0);
+}
+
+/*
+ * db_cache_update --
+ * Update the line cache with a private copy of the data.
+ */
+int
+db_cache_update(SCR *sp, EXF *ep, recno_t lno, void *data, size_t size)
+{
+ if (size > ep->c_buf_len) {
+ REALLOC(sp, ep->c_buf, size);
+ if (ep->c_buf == NULL) {
+ ep->c_buf_len = 0;
+ ep->c_lp = NULL;
+ ep->c_lno = OOBLNO;
+ return (1);
+ }
+ ep->c_buf_len = size;
+ }
+ if (size > 0)
+ memcpy(ep->c_buf, data, size);
+
+ ep->c_lno = lno;
+ ep->c_len = size;
+ ep->c_lp = ep->c_buf;
+ return (0);
}
Index: include/com_extern.h
===================================================================
RCS file: /cvs/src/usr.bin/vi/include/com_extern.h,v
diff -u -p -u -p -r1.17 com_extern.h
--- include/com_extern.h 23 Aug 2025 21:02:10 -0000 1.17
+++ include/com_extern.h 4 May 2026 13:25:07 -0000
@@ -34,6 +34,7 @@ int db_insert(SCR *, recno_t, char *, si
int db_set(SCR *, recno_t, char *, size_t);
int db_exist(SCR *, recno_t);
int db_last(SCR *, recno_t *);
+int db_cache_update(SCR *, EXF *, recno_t, void *, size_t);
void db_err(SCR *, recno_t);
int log_init(SCR *, EXF *);
int log_end(SCR *, EXF *);
--
Walter