From 93387cc89041692870e95568994489eed237e7fb Mon Sep 17 00:00:00 2001 From: Glyalith Date: Sat, 5 Sep 2026 16:36:14 -0600 Subject: [PATCH] Survive forbidden objects during the frame scan IsForbidden() is no longer sufficient on its own. Objects exist that answer false and then raise "Attempt to access forbidden object from code tainted by an AddOn" on the very next call, GetObjectType(). ScanFrames hits one, the error is swallowed by coroutine.resume returning false rather than propagating, the walk ends mid-tree, and WriteCache commits the truncated set. Every later rescan dies at the same object, so nothing recovers it and GetUnitFrame returns nil for every unit. Measured in a raid on 12.1: replaying the same walk with the same gates, stopping at the first raising node reaches 0 unit frames, while continuing past them reaches 10, with 2990 raising objects in a single pass. They are spread across the UI rather than owned by one addon, roughly 2010 directly under UIParent and the rest under unit frames and raid frame buttons. GetObjectType and the button inspection are wrapped so one bad node is skipped instead of ending the scan. Neither wrapper contains the recursion, since ScanFrames yields and yielding across a pcall boundary is an error in 5.1; that is why the button body moved into its own function. coroutine.resume's result is now checked and reported once per scan through geterrorhandler. With the wrappers above this should be rare, but a silent truncation is indistinguishable from a UI that genuinely has no unit frames, which is what made this hard to find. GetChildren is deliberately left unwrapped: reaching it means GetObjectType already succeeded on that object, and wrapping it would need a table per node across the whole tree. If it ever does raise, the resume check above now reports it instead of hiding it. --- LibGetFrame-1.0.lua | 58 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/LibGetFrame-1.0.lua b/LibGetFrame-1.0.lua index fe424fd..5bbe7db 100644 --- a/LibGetFrame-1.0.lua +++ b/LibGetFrame-1.0.lua @@ -1,5 +1,5 @@ local MAJOR_VERSION = "LibGetFrame-1.0" -local MINOR_VERSION = 77 +local MINOR_VERSION = 78 if not LibStub then error(MAJOR_VERSION .. " requires LibStub.") end @@ -394,28 +394,43 @@ local notAUnitFrameTypeAttribute = { cancelaura = true } +-- IsForbidden() is not sufficient on its own: objects exist that answer false +-- and still raise "Attempt to access forbidden object from code tainted by an +-- AddOn" on the next call. Inspecting a button also touches several APIs that +-- can raise on a restricted object. Both are wrapped so one bad node is skipped +-- instead of killing the whole walk. +-- +-- Neither wrapper may contain the recursion: ScanFrames yields, and a yield +-- across a pcall boundary is an error in 5.1. +local function InspectButton(frame) + local typeAttribute = frame:GetAttribute("type") + if not notAUnitFrameTypeAttribute[typeAttribute] then + local unit = SecureButton_GetUnit(frame) + if unit and frame:IsVisible() then + local name = recurseGetName(frame) + if name then + FrameToFrameName:Add(frame, name) + FrameToUnit:Add(frame, unit) + end + end + end +end + local function ScanFrames(depth, frame, ...) coroutine.yield() if not frame then return end if depth < maxDepth and frame.IsForbidden and not frame:IsForbidden() then - local frameType = frame:GetObjectType() + local ok, frameType = pcall(frame.GetObjectType, frame) + if not ok then + frameType = nil + end if frameType == "Frame" or frameType == "Button" then ScanFrames(depth + 1, frame:GetChildren()) end if frameType == "Button" then - local typeAttribute = frame:GetAttribute("type") - if not notAUnitFrameTypeAttribute[typeAttribute] then - local unit = SecureButton_GetUnit(frame) - if unit and frame:IsVisible() then - local name = recurseGetName(frame) - if name then - FrameToFrameName:Add(frame, name) - FrameToUnit:Add(frame, unit) - end - end - end + pcall(InspectButton, frame) end end ScanFrames(depth, ...) @@ -423,6 +438,7 @@ end local status = "ready" local co +local scanError local coroutineFrame = CreateFrame("Frame") coroutineFrame:Hide() @@ -439,10 +455,24 @@ coroutineFrame:SetScript("OnUpdate", function() -- Limit to 5ms per frame StartProfiling("scan frames") while debugprofilestop() - start < 5 and coroutine.status(co) ~= "dead" do - coroutine.resume(co, 0, UIParent) + -- coroutine.resume reports an error by returning false rather than + -- propagating it. Left unchecked, a raising node ends the walk here and + -- WriteCache below commits whatever partial set was reached, with nothing + -- shown to anyone. + local ok, err = coroutine.resume(co, 0, UIParent) + if not ok then + scanError = err + end end StopProfiling("scan frames") if coroutine.status(co) == "dead" then + if scanError then + -- Surfaced once per scan: a silent truncation is indistinguishable from + -- a UI that genuinely has no unit frames. + local err = scanError + scanError = nil + geterrorhandler()(MAJOR_VERSION .. " frame scan aborted: " .. tostring(err)) + end StartProfiling("callbacks") FrameToFrameName:WriteCache() FrameToUnit:CalcRemoved()