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.