Skip to content

Commit f2df31c

Browse files
committed
fix(text): stop inline-icon Maintain from dereferencing stale fontstrings
A destroyed chat-line fontstring left a dangling key in the icon pool's per-fontstring map. Its heap memory was reused as a string buffer, and Maintain's weak LooksReadable() range check waved the reused pointer through; reading the "parent" field (fs+0x9C) then yielded string bytes (ASCII "-Rac"), which the engine's region attach (FUN_0076a750) dereferenced at *(parent+0x1B4) -> ACCESS_VIOLATION (ERROR #132 at 0x0076A7CE). Two compounding defects, both in Maintain: - the stale-entry expiry was gated on shown > 0, so a scrolled-off line (shown == 0) never expired and its dead key was re-dereferenced every frame, waiting for its memory to be reused; - LooksReadable is a bare VA-range test that a reused buffer passes. Fix: verify a key is a LIVE fontstring by its object+0x00 vtable before using it. The vtable is learned at paint time from fontstrings the engine is actively drawing (QueuePlacements), so every fontstring class -- chat's internal ScrollingMessageFrame lines, tooltip/CreateFontString lines, addon fontstrings -- is recognized without hardcoding (one hardcoded value filtered live chat icons out entirely). Maintain now iterates with erase: a key whose memory no longer carries a learned fontstring vtable has its regions hidden (our tex pointers stay valid, parented to the chat frame) and the entry erased, so dead keys can neither accumulate nor be dereferenced.
1 parent df88896 commit f2df31c

1 file changed

Lines changed: 70 additions & 9 deletions

File tree

src/text/InlineTexturePool.cpp

Lines changed: 70 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
#include <string>
2929
#include <unordered_map>
30+
#include <unordered_set>
3031

3132
namespace Text::InlineTexturePool {
3233

@@ -50,6 +51,41 @@ bool LooksReadable(const void *p) {
5051
return a >= 0x00010000u && a < 0xFFFF0000u;
5152
}
5253

54+
// Vtables seen at object+0x00 of REAL, live fontstrings — learned at paint time
55+
// (see LearnFontStringVtable / QueuePlacements). A fontstring is polymorphic, so
56+
// its vtable is a fixed .rdata pointer per class. We LEARN rather than hardcode
57+
// because chat (ScrollingMessageFrame's internal lines), tooltip, and addon
58+
// fontstrings are different classes with different vtables — a single hardcoded
59+
// value filtered live chat icons out entirely.
60+
std::unordered_set<uintptr_t> g_fsVtables;
61+
62+
// Record a live fontstring's vtable. Called only from QueuePlacements, which the
63+
// paint hook invokes with a fontstring the engine is actively drawing — so
64+
// *(void**)fs is a valid vtable here. The module-range gate is belt-and-braces
65+
// against ever poisoning the set with a stray value.
66+
void LearnFontStringVtable(const void *fs) {
67+
auto a = reinterpret_cast<uintptr_t>(fs);
68+
if (a < 0x00010000u || a >= 0xFFFF0000u)
69+
return;
70+
const uintptr_t vt = *reinterpret_cast<const uintptr_t *>(fs);
71+
if (vt >= 0x00400000u && vt < 0x00D2B000u) // WoW.exe image range
72+
g_fsVtables.insert(vt);
73+
}
74+
75+
// True only for a LIVE fontstring: its object+0x00 vtable matches one learned
76+
// from a genuinely-painted fontstring. A bare VA-range check is not enough — a
77+
// freed fontstring whose heap memory is reused (as a string buffer, …) still
78+
// passes it, and reading a garbage "parent" (fs+0x9C) from such a stale key
79+
// crashed the region attach (FUN_0076a750 dereferencing *(parent+0x1B4), parent
80+
// = ASCII "-Rac" string bytes). Reused memory never carries a fontstring vtable.
81+
// The vtable read is safe: the range check bounds the pointer, and these object
82+
// pages stay committed (we already read fs+0xC8 on the same pointer).
83+
bool IsLiveFontString(const void *fs) {
84+
if (!LooksReadable(fs))
85+
return false;
86+
return g_fsVtables.count(*reinterpret_cast<const uintptr_t *>(fs)) != 0;
87+
}
88+
5389
using SetTexCoord_t = void(__thiscall *)(void *tex, const float *coords4);
5490
using SetParentAndLayer_t = void(__thiscall *)(void *region, void *parentFrame, int layer,
5591
int show);
@@ -197,9 +233,23 @@ void ApplyPlacement(IconRegion &r, void *fs, const Placement &p, uint8_t fsAlpha
197233
// (deferred out of the text paint — regions must never be mutated mid-paint).
198234
void Maintain() {
199235
++g_maintainTick;
200-
for (auto &kv : g_fsIcons) {
201-
void *fs = kv.first;
202-
FsPlacements &np = kv.second;
236+
for (auto it = g_fsIcons.begin(); it != g_fsIcons.end();) {
237+
void *fs = it->first;
238+
FsPlacements &np = it->second;
239+
240+
// Stale-key eviction (the crash fix). A destroyed fontstring leaves a
241+
// dangling key here; its heap memory may be reused as unrelated data, so
242+
// a VA-range check is not enough — verify the live-fontstring vtable. If
243+
// it is gone, hide our regions (their tex pointers are OURS, parented to
244+
// the chat frame, so still valid) and erase the entry. This also stops
245+
// dead keys from accumulating: only live fontstrings survive, and the
246+
// chat/tooltip pools reuse a bounded set of them.
247+
if (!IsLiveFontString(fs)) {
248+
for (int i = 0; i < np.shown && i < static_cast<int>(np.regions.size()); ++i)
249+
Hide(np.regions[static_cast<size_t>(i)].tex);
250+
it = g_fsIcons.erase(it);
251+
continue;
252+
}
203253

204254
// Freshness expiry: a LIVE painted icon line re-queues every paint, so
205255
// its touch stamp stays current. A parked/orphaned fs (recycled chat
@@ -213,22 +263,24 @@ void Maintain() {
213263
np.shown = 0;
214264
np.want.clear();
215265
np.dirty = false;
266+
++it;
216267
continue;
217268
}
218269

219-
// Mirror the fontstring's visibility EVERY tick, not just on dirty:
270+
// Mirror the fontstring's own visibility EVERY tick, not just on dirty:
220271
// expired/hidden chat lines get their fs Hidden by the message frame,
221272
// and our regions are parented to the CHAT FRAME (regions can't parent
222-
// regions), so they don't inherit the line's own hide.
223-
const bool fsAlive =
224-
LooksReadable(fs) &&
273+
// regions), so they don't inherit the line's own hide. fs is a live
274+
// fontstring here (checked above), so this read is safe.
275+
const bool fsShown =
225276
*reinterpret_cast<const uint32_t *>(reinterpret_cast<const uint8_t *>(fs) +
226277
Offsets::OFF_REGION_ACTUALLY_SHOWN) != 0;
227-
if (!fsAlive) {
278+
if (!fsShown) {
228279
for (int i = 0; i < np.shown; ++i)
229280
Hide(np.regions[static_cast<size_t>(i)].tex);
230281
np.shown = 0;
231282
np.dirty = true; // re-place if/when the fs shows again
283+
++it;
232284
continue;
233285
}
234286

@@ -240,8 +292,10 @@ void Maintain() {
240292
np.dirty = false;
241293
void *parent = *reinterpret_cast<void **>(reinterpret_cast<uint8_t *>(fs) +
242294
Offsets::OFF_REGION_PARENT);
243-
if (!LooksReadable(parent))
295+
if (!LooksReadable(parent)) {
296+
++it;
244297
continue;
298+
}
245299
const int want = static_cast<int>(np.want.size());
246300
for (int i = 0; i < want; ++i) {
247301
if (i >= static_cast<int>(np.regions.size())) {
@@ -313,6 +367,8 @@ void Maintain() {
313367
ApplyPlacement(r, fs, p, fsAlpha);
314368
}
315369
}
370+
371+
++it;
316372
}
317373
}
318374

@@ -328,6 +384,11 @@ static const Tick::FrameTick::AutoSubscribe _tick{&Maintain};
328384
void QueuePlacements(void *fs, std::vector<Placement> &&icons) {
329385
if (fs == nullptr)
330386
return;
387+
// fs is a fontstring the engine is actively painting — learn its class
388+
// vtable so Maintain's liveness check recognizes it (and stale, reused-memory
389+
// keys, which never carry one). Must precede the early returns below so an
390+
// entry's vtable is always known before Maintain evaluates it.
391+
LearnFontStringVtable(fs);
331392
auto it = g_fsIcons.find(fs);
332393
if (it == g_fsIcons.end()) {
333394
if (icons.empty())

0 commit comments

Comments
 (0)