From 9dfdb739bd3a77dbe8b921c87e3b6565d2fe6f09 Mon Sep 17 00:00:00 2001 From: Peter Kovacs Date: Mon, 15 Jun 2026 23:25:32 +0200 Subject: [PATCH] =?UTF-8?q?This=20fixes=20a=20latent=20UAF,=20found=20in?= =?UTF-8?q?=20a=20debug-CRT-deterministic=20session,=20but=20present=20in?= =?UTF-8?q?=20all=20builds.=20ScViewData::ReadUserDataSequence=20(viewdata?= =?UTF-8?q?.cxx:2821)=20does=20delete=20pTabData[nTab];=20pTabData[nTab]?= =?UTF-8?q?=20=3D=20new=20ScViewDataTable;=20per=20sheet=20but=20never=20r?= =?UTF-8?q?efreshes=20pThisTab=20(which=20pointed=20at=20pTabData[nTabNo])?= =?UTF-8?q?.=20Back=20in=20ScTabView::SetTabNo,=20line=201660=20reads=20aV?= =?UTF-8?q?iewData.=20GetActivePart()=20=E2=86=92=20pThisTab->eWhichActive?= =?UTF-8?q?=20before=20line=201663=20fixes=20pThisTab=20=E2=86=92=20use-af?= =?UTF-8?q?ter-free=20=E2=86=92=20AV=20sc!ScTabView::SetTabNo=20mov=20ecx,?= =?UTF-8?q?[eax+edx*4+0x664]=20with=20edx=3D0xDDDDDDDD=20(pGridWin[0xddddd?= =?UTF-8?q?ddd]).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a genuine latent UAF (reading a dangling pointer is UB); it is masked in release by unspecified allocator behaviour. The delete and new run in the same-thread, adjacent, same size class, with nothing between them. Mainstream allocators keep per-size free lists with MRU/LIFO reuse. New almost always returns the just-freed block — and when it does, pThisTab == pTabData[nTabNo] is true again and points at the live, freshly constructed object. But it is not guaranteed, and can surface non-deterministically (AI anaysis, may be wrong): - Windows LFH randomizes allocation slots (Win8+ exploit mitigation), so new may return a different slot. In release the freed block isn't poisoned, so the immediate read usually still sees plausible old bytes — but the window here is not two instructions: the rest of ReadUserDataSequence parses all other sheets (lots of allocation), any of which can reclaim that block and overwrite the eWhichActive offset with a large value → pGridWin[garbage] → a rare, timing-dependent release crash. - Hardened/diagnostic allocators (PageHeap, Application Verifier, ASan, a custom operator new) don't do MRU reuse at all → they crash release too. - The debug CRT (MSVCR90D) is just the deterministic trigger: it poison-fills freed blocks with 0xDD and delays reuse, so new returns a different block every time. --- main/sc/source/ui/view/viewdata.cxx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/main/sc/source/ui/view/viewdata.cxx b/main/sc/source/ui/view/viewdata.cxx index 26b4b3ec50..cabcd6eaa9 100644 --- a/main/sc/source/ui/view/viewdata.cxx +++ b/main/sc/source/ui/view/viewdata.cxx @@ -2952,6 +2952,15 @@ void ScViewData::ReadUserDataSequence(const uno::Sequence pTabData[nZoomTab]->aPageZoomY = aDefPageZoomY; } + // The loop above delete'd and re-new'd pTabData[] entries (including the + // active one) but left pThisTab pointing at a freed ScViewDataTable. Restore + // the pThisTab == pTabData[nTabNo] invariant before anyone dereferences it + // (e.g. ScTabView::SetTabNo -> GetActivePart()). Mirrors SetTabNo (line + // ~1502). Without it, a debug build AVs on document open (reads 0xDDDDDDDD); + // release masks it only via allocator MRU reuse. + CreateTabData( nTabNo ); + pThisTab = pTabData[nTabNo]; + if (nCount) SetPagebreakMode( bPageMode );