From 708372f24921c25a67fda4489df61d69163f2dca Mon Sep 17 00:00:00 2001 From: n-WN Date: Sat, 16 May 2026 17:00:01 +0800 Subject: [PATCH] tests: regression for apply_selection with paused-rendering + scrollback Add a regression test that exercises the code path which crashed in v0.46.2 (#10017): when paused_rendering is active and a selection extends into the scrollback, the inner loop of apply_selection iterates with a negative y. Without the recently-added paused_y translation and the paused_y < 0 guard, the call to linebuf_init_line treats the negative y as a huge unsigned index_type and reads ~4GB out of bounds in line_attrs[idx], crashing with SIGBUS. The test reproduces the trigger deterministically via the Screen Python bindings and asserts that current_selections() returns the expected buffer instead of crashing. --- kitty_tests/screen.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/kitty_tests/screen.py b/kitty_tests/screen.py index e444a6318..b58c5d9a3 100644 --- a/kitty_tests/screen.py +++ b/kitty_tests/screen.py @@ -741,6 +741,35 @@ class TestScreen(BaseTest): self.ae(s.text_for_selection(False, True), ('1234 ', '5')) self.ae(s.text_for_selection(True, True), ('1234 ', '5', '')) + def test_apply_selection_with_paused_rendering_and_scrollback(self): + # Regression test: in 0.46.2 the paused-rendering branch of + # apply_selection passed the (possibly negative) loop variable y + # directly to linebuf_init_line, which interprets it as an unsigned + # index_type and reads ~4GB out of bounds in line_attrs[idx]. The fix + # translates to paused_y = y + scrolled_by and guards paused_y < 0. + # Real-world trigger: a TUI sending DCS =1s (DEC synchronized output) + # while the user has scrolled back and has an active scrollback + # selection. + s = self.create_screen(cols=10, lines=3, scrollback=50) + for i in range(40): + s.draw(f"row{i:03d}") + s.carriage_return() + s.linefeed() + s.scroll(20, True) + self.assertGreater(s.scrolled_by, 0) + # Selection that crosses the top of the visible area into scrollback, + # so the inner loop iterates with negative y. + s.start_selection(0, 0) + s.update_selection(2, 1) + self.assertTrue(s.has_selection()) + self.assertTrue(s.pause_rendering(True, 5000)) + # Must not crash and must return the visible-area buffer. + result = s.current_selections() + self.ae(len(result), s.lines * s.columns) + # The visible portion of the selection must have at least one byte + # marked (set_mask = 1 for plain selections). + self.assertIn(1, result) + def test_soft_hyphen(self): s = self.create_screen() s.draw('a\u00adb')