Skip to content

Survive forbidden objects during the frame scan - #53

Open
dfrisone wants to merge 1 commit into
mrbuds:masterfrom
dfrisone:fix/survive-forbidden-objects
Open

Survive forbidden objects during the frame scan#53
dfrisone wants to merge 1 commit into
mrbuds:masterfrom
dfrisone:fix/survive-forbidden-objects

Conversation

@dfrisone

@dfrisone dfrisone commented Sep 5, 2026

Copy link
Copy Markdown

The problem

IsForbidden() is no longer sufficient on its own. On 12.1 there are objects that answer false and then raise on the very next call:

bad argument #1 to '?' (Attempt to access forbidden object from code tainted by an AddOn -
Usage: local objectType = self:GetObjectType())

ScanFrames hits one at line 402, and because coroutine.resume reports errors by returning false, err rather than propagating, the error is discarded at line 442. The walk ends mid-tree, the loop exits on coroutine.status(co) == "dead", and WriteCache commits whatever partial set was reached. Every later rescan dies at the same object, so GetUnitFrame returns nil for every unit from then on and nothing recovers it.

For users this presents as a raid-frame glow silently doing nothing, with no error, in some content while working in others.

Evidence

Replaying the same walk over UIParent with the same gates in the same order, differing only in whether it stops at the first raising node. Measured in a raid on 12.1:

Behaviour visited buttons with unit unit frames found
Stop at first error (what happens today) 7414 2179 0 0
Continue past errors 15114 3028 23 10

2990 raising objects in a single pass. They are spread across the UI rather than owned by any one addon: roughly 2010 directly under UIParent, the rest under unit frames and raid frame buttons. The frames the caller wants are perfectly reachable; the scan just never arrives.

Things I ruled out before landing on this, in case they save you time: a stale cache (calling lib.ScanForUnitFrames() and re-checking resolved nothing), refused unit comparison in GetUnitFrames (C_Secrets.CanCompareUnitTokens returns true and UnitIsUnit is not secret in the failing content), and missing name patterns (the frames were already in defaultFramePriorities).

The change

GetObjectType and the button inspection are wrapped, so one bad node is skipped rather than ending the scan. Neither wrapper contains the recursion -- ScanFrames yields, and yielding across a pcall boundary is an error in 5.1 -- which is why the button body moved into its own InspectButton function rather than being wrapped in place.

coroutine.resume's result is now checked and surfaced once per scan through geterrorhandler. With the wrappers above that should be rare, but a silent truncation is indistinguishable from a UI that genuinely has no unit frames, which is most of why this took a while to find.

GetChildren is deliberately left unwrapped: reaching it means GetObjectType already succeeded on that object, and wrapping it would mean a table allocation per node across the whole tree. If it ever does raise, the resume check now reports it rather than hiding it.

MINOR bumped to 78 so a fixed copy wins the LibStub race against embedded 77s.

Happy to adjust any of this -- in particular if you would rather the resume error be silent, or want GetChildren wrapped too despite the allocation.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant