From 01adaff8d1cab4db6880b1a9b43320fb88d8786d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=B5=90=E5=8F=8B?= Date: Fri, 26 Jun 2026 22:01:05 +0900 Subject: [PATCH] renderer: Force a viewport resync in EnablePainting (#20269) EnablePainting() assigns _viewport = _pData->GetViewport() directly, but unlike _CheckViewportAndScroll() - the only other writer of _viewport - it neither calls UpdateViewport() nor sets _forceUpdateViewport. UpdateViewport() is the sole writer of _api.s->viewportCellCount, which sizes the engine's row buffer (AtlasEngine's _p.rows). If the viewport grows while painting is disabled (e.g. the render-failure recovery path, ResumeRendering, or init/relayout), EnablePainting() latches _viewport to the new size. The next _CheckViewportAndScroll() then early-returns because srOldViewport == srNewViewport (both already the new size) and _forceUpdateViewport is false, so UpdateViewport() is skipped and the engine viewport - and _p.rows - stay at the old, smaller size. _updateCursorInfo() derives coordCursor.y from the larger _viewport and still reports the cursor as in-viewport, so PaintCursor() indexes _p.rows past its end -> out-of-bounds read and an access violation (GH#20269). A full-heap crash dump confirms the mechanism: at the fault, coordCursor.y = 67 while _p.rows holds 65 fully-valid entries (every in-bounds slot points into _p.unorderedRows), and _p.rows[67] reads past the array into adjacent heap. Set _forceUpdateViewport = true in EnablePainting() so the next _CheckViewportAndScroll() runs UpdateViewport() and resizes the backing buffer to match _viewport before the cursor is painted - keeping the resize on the same render-thread, lock-held path as the cursor move rather than masking the symptom at the read site. --- src/renderer/base/renderer.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/renderer/base/renderer.cpp b/src/renderer/base/renderer.cpp index 3bf90c29046..b5103427587 100644 --- a/src/renderer/base/renderer.cpp +++ b/src/renderer/base/renderer.cpp @@ -58,6 +58,17 @@ void Renderer::EnablePainting() // but once EnablePainting is called it should be safe to retrieve. _viewport = _pData->GetViewport(); + // _viewport feeds the cursor coordinate (_updateCursorInfo), while the engine's + // backing buffer (e.g. AtlasEngine's _p.rows) is sized from viewportCellCount, + // which only UpdateViewport() writes. If the viewport grew while painting was + // disabled, assigning _viewport here without forcing a resync would let the next + // _CheckViewportAndScroll() early-return (srOldViewport == srNewViewport) and skip + // UpdateViewport(), leaving the engine viewport - and thus the row buffer - behind + // _viewport. The cursor could then be reported as in-viewport at a row past the end + // of the buffer (GH#20269). Force the resync so the backing buffer is resized to + // match before the next cursor move is painted. + _forceUpdateViewport = true; + _enable.SetEvent(); if (const auto guard = _threadMutex.lock_exclusive(); !_thread)