diff --git a/kitty/screen.c b/kitty/screen.c index 9a184f497..0c7d16b2a 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -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) diff --git a/kitty_tests/multicell.py b/kitty_tests/multicell.py index 2a046f29b..49b5acf22 100644 --- a/kitty_tests/multicell.py +++ b/kitty_tests/multicell.py @@ -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