Fix crash in overlay line drawing on uninitialized linebuf view

screen_draw_overlay_line accessed self->linebuf->line->cpu_cells
without ever calling linebuf_init_line on the shared view. Render
paths that initialize a stack-local Line via render_line_for_virtual_y
left the view's cpu_cells as NULL (the value set by alloc_line via
PyType_GenericAlloc), and the multicell-trim loop then dereferenced
NULL + xstart * sizeof(CPUCell), producing a SIGSEGV at a small
address (e.g. 0x1e for xstart=2). The crash was reachable any time
an IME pre-edit overlay was rendered with the cursor not in column 0
on a screen whose linebuf->line had not been re-pointed by some
unrelated prior call.

Fix by initializing the view at the overlay row on entry. Add a
test_draw_overlay_line method on Screen so the behavior can be
exercised directly from a regression test.
This commit is contained in:
distsystem 2026-04-18 13:10:07 +08:00
parent 9fd83e7cfb
commit 875ca70a55
2 changed files with 38 additions and 0 deletions

View file

@ -4283,6 +4283,11 @@ screen_update_overlay_text(Screen *self, const char *utf8_text) {
static void
screen_draw_overlay_line(Screen *self) {
if (!self->overlay_line.overlay_text) return;
// self->linebuf->line is a shared view that callers may not have pointed
// at the overlay row (e.g. render_line_for_virtual_y inits a stack-local
// Line instead). Without this, line->cpu_cells can be NULL or stale,
// crashing the cell loops below.
linebuf_init_line(self->linebuf, self->overlay_line.ynum);
// Right-align the overlay to ensure that the pre-edit text just entered is visible when the cursor is near the end of the line.
index_type xstart = self->overlay_line.text_len <= self->columns ? self->columns - self->overlay_line.text_len : 0;
if (self->overlay_line.xstart < xstart) xstart = self->overlay_line.xstart;
@ -6066,6 +6071,26 @@ test_create_write_buffer(Screen *screen UNUSED, PyObject *args UNUSED) {
return PyMemoryView_FromMemory((char*)buf, s, PyBUF_WRITE);
}
static PyObject*
test_draw_overlay_line(Screen *self, PyObject *args) {
PyObject *text;
unsigned int xstart, ynum;
if (!PyArg_ParseTuple(args, "UII", &text, &xstart, &ynum)) return NULL;
if (ynum >= self->lines || xstart >= self->columns) {
PyErr_SetString(PyExc_IndexError, "ynum or xstart out of range");
return NULL;
}
Py_INCREF(text);
Py_XDECREF(self->overlay_line.overlay_text);
self->overlay_line.overlay_text = text;
self->overlay_line.text_len = (index_type)PyUnicode_GET_LENGTH(text);
self->overlay_line.xstart = xstart;
self->overlay_line.ynum = ynum;
self->overlay_line.is_active = true;
screen_draw_overlay_line(self);
Py_RETURN_NONE;
}
static PyObject*
test_commit_write_buffer(Screen *screen, PyObject *args) {
RAII_PY_BUFFER(srcbuf); RAII_PY_BUFFER(destbuf);
@ -6153,6 +6178,7 @@ static PyMethodDef methods[] = {
METHODB(test_create_write_buffer, METH_NOARGS),
METHODB(test_commit_write_buffer, METH_VARARGS),
METHODB(test_parse_written_data, METH_VARARGS),
METHODB(test_draw_overlay_line, METH_VARARGS),
MND(line_edge_colors, METH_NOARGS)
MND(line, METH_O)
MND(dump_lines_with_attrs, METH_VARARGS)

View file

@ -14,6 +14,18 @@ class TestMulticell(BaseTest):
def test_multicell(self):
test_multicell(self)
def test_overlay_line_does_not_crash_on_uninit_linebuf_view(self):
# Regression: screen_draw_overlay_line accessed self->linebuf->line->cpu_cells
# without ever calling linebuf_init_line, so on render paths that
# initialize a stack-local Line (render_line_for_virtual_y) the shared
# view's cpu_cells stayed NULL and the multicell-trim loop dereferenced
# NULL + xstart * sizeof(CPUCell). Reproduces by placing a wide cell at
# xstart>0 and triggering the overlay draw directly.
for xstart in (1, 2, 3):
s = self.create_screen(cols=10, lines=5)
s.draw('好好') # two 2-cell wide chars covering columns 0..3
s.test_draw_overlay_line('xy', xstart, 0) # would SIGSEGV without fix
def test_multicell(self: TestMulticell) -> None:
from kitty.tab_bar import as_rgb