Skip to content

Commit e66d915

Browse files
author
mergetest
committed
Rewrite NativeBackend to Krathe's proven per-consumer container pattern
The combat freeze was NOT the container (taint.log: zero blocked actions after the SafeSetFont + legacy-scan taint fixes). It was our backend deviating from Krathe's live-validated pattern. Realign to reference/ContainerOverlay.lua: - Plain per-consumer CreateFrame(AuraContainer, nil, handle.frame, CustomAuraContainerTemplate) — insecure is fine (proven live in combat by the AD overlay PoC); drop the SecureGroupHeader auraContainerTemplate detour, button.AuraContainer acquisition, shared-container namespacing + group-parking. Each consumer owns its container -> its own flow layout (also fixes the one-container-one-layout constraint). - create -> SetAllPoints -> SetUnit -> AddAuraGroup/AddAuraSlot(initializeFrame) -> SetEnabled LAST. - Standing/config-driven: built once, Blizzard drives it; DriveBuff/DefensiveFactory already only rebuild on sig change / retarget on unit change (verified, no per-UNIT_AURA container ops). - Defer ALL secure ops to PLAYER_REGEN_ENABLED: build self-defers in lockdown; SetUnit/Rebuild/ Destroy/enable route through the existing _queueOp regen machinery. - Kept verbatim: styleButton_regions/bindNative (initializeFrame), probe, NOT_CANCELABLE + taint fixes. Consumers (Auras.lua/Icons.lua) needed no changes. Unverified in combat — the retest.
1 parent 8d6f2c9 commit e66d915

2 files changed

Lines changed: 70 additions & 93 deletions

File tree

Frames/AuraContainer.lua

Lines changed: 70 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,7 @@ local DBG = "AURACONTAINER"
6868
-- taint, native dispel reject) logs ONCE, not once per button.
6969
local warnedCurve, warnedBorder, warnedNativeDispel = false, false, false
7070
local warnedRestyle, warnedRefresh, warnedMouse = false, false, false
71-
local warnedNoContainer = false
72-
-- Per-handle key-namespace counter: multiple consumers (buff, defensive, ...) share the ONE
73-
-- secure AuraContainer that lives on each unit button, so their group/slot keys must be unique.
74-
local _handleSeq = 0
71+
local warnedCreate = false
7572

7673
-- ============================================================
7774
-- CAPABILITY DETECTION (the version gate + PTR-4 feature gates)
@@ -568,6 +565,13 @@ end
568565
-- per button (in lazy batches of 10) to style it. isNativeSlots = true. Row layout is the
569566
-- container's flow layout (SetAuraLayout* translation lands in P1); overlay slots are
570567
-- addon-anchored via the button AddAuraSlot returns.
568+
--
569+
-- Each backend owns its OWN plain container (Krathe's proven ContainerOverlay pattern —
570+
-- reference ContainerOverlay.lua buildOverlay): insecure CreateFrame is combat-legal for the
571+
-- aura pipeline (taint.log-proven; the earlier freeze was unrelated secret-value compares,
572+
-- since fixed). One container per consumer = one flow layout per row (independent
573+
-- positioning) and a trivial recreate-on-structural-change teardown. The container is
574+
-- STANDING: built once from config, then Blizzard drives it — no per-UNIT_AURA touches.
571575
-- ============================================================
572576
local NativeBackend = {}
573577
NativeBackend.__index = NativeBackend
@@ -578,35 +582,37 @@ end
578582

579583
function NativeBackend:isNativeSlots() return true end
580584

581-
-- Order: SetUnit -> AddAuraGroup/AddAuraSlot(each filter, initializeFrame) -> SetEnabled
582-
-- LAST. SetEnabled gates aura-event registration (IsVisible() and IsEnabled()); without
583-
-- the LAST enable the row renders once then goes permanently stale.
585+
-- Order (Krathe's ContainerOverlay.lua buildOverlay, proven live in combat on 68569):
586+
-- CreateFrame("AuraContainer", nil, ours, "CustomAuraContainerTemplate") -> SetAllPoints ->
587+
-- SetUnit -> AddAuraGroup/AddAuraSlot(each filter, initializeFrame) -> SetEnabled LAST.
588+
-- SetEnabled gates aura-event registration (IsVisible() and IsEnabled()); without the LAST
589+
-- enable the row renders once then goes permanently stale. After build the container is
590+
-- STANDING — Blizzard drives it; DF touches it again only on a structural rebuild, a unit
591+
-- retarget, or teardown.
584592
function NativeBackend:build()
585593
local handle = self.handle
586594
local config = handle.config
587-
-- ACQUIRE the SECURE container the SecureGroupHeader created on the unit button (via the
588-
-- auraContainerTemplate attribute). An addon-created container is tainted → its aura
589-
-- Show/Hide/create are BLOCKED in combat (renders OOC, freezes in combat). This one is
590-
-- Blizzard-created → untainted → combat-safe. It's SHARED per button across consumers
591-
-- (buff/defensive/...), so keys are namespaced and we never destroy it.
592-
local button = handle.parentButton
593-
local c = button and button.AuraContainer
594-
if not c then
595-
if not warnedNoContainer then
596-
warnedNoContainer = true
597-
DF:DebugWarn(DBG, "no secure AuraContainer on the unit button (auraContainerTemplate unset on the header, or a non-header frame) — unit=%s", tostring(config.unit))
595+
596+
-- Never stand up a container in combat: in-lockdown create/enable is a hard client
597+
-- error pcall can't catch (ContainerOverlay gotcha 1). Every caller already gates this
598+
-- (Create / _rebuild / the regen handler); a stray path defers instead of dying.
599+
if InCombatLockdown() then handle:_deferRebuild(); return end
600+
601+
-- OUR OWN plain per-consumer container, parented to the handle's anchor frame. Insecure
602+
-- creation is fine — taint.log proved the old combat freeze was unrelated secret-value
603+
-- compares (Config.lua SafeSetFont / Auras.lua legacy scan), both fixed — and this exact
604+
-- plain-create pattern runs live in combat in the AD ContainerOverlay PoC.
605+
local ok, c = pcall(CreateFrame, "AuraContainer", nil, handle.frame, "CustomAuraContainerTemplate")
606+
if not ok or not c then
607+
if not warnedCreate then
608+
warnedCreate = true
609+
DF:DebugWarn(DBG, "CreateFrame(AuraContainer) failed: %s", tostring(c))
598610
end
599611
self.container = nil
600612
return
601613
end
602614
self.container = c
603-
-- Position the (secure) container over the unit button. The SecureGroupHeader creates it
604-
-- with NO anchors and NO size, so without this its flow-laid buttons never resolve a rect
605-
-- and never draw. taint.log CONFIRMED this base-Frame call does NOT taint the container —
606-
-- the combat freeze was unrelated secret-value compares (Config.lua:773 / Auras.lua:1300),
607-
-- now fixed. OOC only (build runs OOC / on regen). config.layout → inbound SetAuraLayout*
608-
-- lands in P1, but anchoring the container to the button is DF's job regardless.
609-
if not InCombatLockdown() then pcall(function() c:SetAllPoints(button); c:Show() end) end
615+
c:SetAllPoints(handle.frame)
610616
if type(config.unit) == "string" then pcall(function() c:SetUnit(config.unit) end) end
611617

612618
-- Fresh generation: buttons are created in lazy batches (of 10) as needed, so a slot's
@@ -616,85 +622,72 @@ function NativeBackend:build()
616622
handle._gen = (handle._gen or 0) + 1
617623
local initFn = handle:_makeInitializeFrame(handle._gen)
618624

619-
-- Park the previous generation's groups (topology is add-only — no RemoveAuraGroup), then
620-
-- add this generation under per-handle + per-gen namespaced keys (no collision with other
621-
-- consumers sharing this container, no collision with our own parked-but-not-removed keys).
622-
self:_parkGroups()
623-
self.groupKeys = {}
624-
local keyPrefix = handle._keyBase .. "_g" .. handle._gen .. "_"
625+
-- Declare one AuraGroup per filter (row) / one AuraSlot per filter (overlay). The
626+
-- container is exclusively ours, so keys need no cross-consumer namespacing.
625627
local filters = normalizeFilters(config.filter)
626628
local maxCount = handle:_slotCount()
627629
local isOverlay = config.mode == "overlay"
628630
for i, f in ipairs(filters) do
629631
if AuraUtil and AuraUtil.IsValidFilterString and not AuraUtil.IsValidFilterString(f) then
630632
DF:DebugWarn(DBG, "filter rejected by IsValidFilterString: %s (group skipped)", tostring(f))
631633
else
632-
local key = keyPrefix .. i
634+
local key = "df" .. i
633635
if isOverlay then
634-
local ok, btn = pcall(function() return c:AddAuraSlot(key, f, { initializeFrame = initFn }) end)
635-
if ok and btn then self.groupKeys[key] = "slot"; pcall(function() btn:SetAllPoints(button) end)
636-
elseif not ok then DF:DebugWarn(DBG, "AddAuraSlot failed: %s", tostring(btn)) end
636+
local okSlot, btn = pcall(function() return c:AddAuraSlot(key, f, { initializeFrame = initFn }) end)
637+
if okSlot and btn then pcall(function() btn:SetAllPoints(handle.frame) end)
638+
elseif not okSlot then DF:DebugWarn(DBG, "AddAuraSlot failed: %s", tostring(btn)) end
637639
else
638-
local ok, err = pcall(function() c:AddAuraGroup(key, f, { maxFrameCount = maxCount, initializeFrame = initFn }) end)
639-
if ok then self.groupKeys[key] = "group"
640-
else DF:DebugWarn(DBG, "AddAuraGroup failed: %s", tostring(err)) end
640+
local okGroup, err = pcall(function() c:AddAuraGroup(key, f, { maxFrameCount = maxCount, initializeFrame = initFn }) end)
641+
if not okGroup then DF:DebugWarn(DBG, "AddAuraGroup failed: %s", tostring(err)) end
641642
end
642643
end
643644
end
644645

645-
-- No SetEnabled call: the header-created container defaults enabled=true, and AddAuraGroup
646-
-- triggers UpdateEventRegistrations, so it registers UNIT_AURA on its own. Calling the
647-
-- base SetEnabled from insecure code would taint it (see the SetAllPoints note above).
646+
-- SetEnabled LAST — after the groups/slots + filters are declared (ContainerOverlay.lua
647+
-- gotcha 2). This is what arms the parse + UNIT_AURA registration.
648+
pcall(function() c:SetEnabled(config.enabled ~= false) end)
648649

649-
pcall(function()
650-
DF:Debug(DBG, "built (native) unit=%s mode=%s groups=%d",
651-
tostring(config.unit), tostring(config.mode or "row"), #filters)
652-
end)
653-
end
654-
655-
-- Park (don't remove — topology is add-only) this backend's groups by zeroing their frame
656-
-- count, so a rebuild/teardown releases their buttons without destroying the shared container.
657-
function NativeBackend:_parkGroups()
658-
local c = self.container
659-
if c and self.groupKeys then
660-
for key, kind in pairs(self.groupKeys) do
661-
pcall(function()
662-
if kind == "group" and c.SetAuraGroupMaxFrameCount then
663-
c:SetAuraGroupMaxFrameCount(key, 0)
664-
elseif kind == "slot" and c.SetAuraSlotFilterString then
665-
c:SetAuraSlotFilterString(key, "HELPFUL|!HELPFUL") -- match-nothing: parks the slot
666-
end
667-
end)
668-
end
669-
end
670-
self.groupKeys = nil
650+
DF:Debug(DBG, "built (native) unit=%s mode=%s groups=%d",
651+
tostring(config.unit), tostring(config.mode or "row"), #filters)
671652
end
672653

673654
function NativeBackend:setUnit(unit)
674655
if self.container and type(unit) == "string" then pcall(function() self.container:SetUnit(unit) end) end
675656
end
676657

658+
-- Callers combat-gate this (Handle:_applyEnabled defers to regen in lockdown) — enabling
659+
-- a container in combat is forbidden, same class of op as creating one.
677660
function NativeBackend:setEnabled(on)
678-
-- No-op: the shared container is enabled by default and must NOT be driven by the base
679-
-- SetEnabled from insecure code (taints it → combat block). We hide OUR row by parking our
680-
-- groups (maxFrameCount 0), not by disabling the whole container.
661+
local c = self.container
662+
if c then pcall(function() c:SetEnabled(on and true or false) end) end
681663
end
682664

683665
-- 68569: UpdateAllAuras() is an addon-callable dirty-mark (processed next OnUpdate while
684-
-- visible) — the real refresh. Fall back to the Hide/Show bounce only if it's absent.
666+
-- visible) — the real refresh. The Hide/Show bounce fallback is legal again (the container
667+
-- is ours alone), but only out of combat (Show re-arms the parse, same class as enable).
685668
function NativeBackend:refresh()
686669
local c = self.container
687670
if not c then return end
688671
if type(c.UpdateAllAuras) == "function" then
689672
pcall(function() c:UpdateAllAuras() end)
673+
elseif not InCombatLockdown() then
674+
pcall(function() c:Hide(); c:Show() end)
690675
end
691-
-- (no Hide/Show fallback: the container is shared + button-owned — never bounce it)
692676
end
693677

694-
-- The container is OWNED BY THE UNIT BUTTON (secure header created it) and SHARED across
695-
-- consumers — never Hide/disable/destroy it. Just park OUR groups and drop our ref.
678+
-- The container is OURS (per-consumer): teardown mirrors ContainerOverlay's teardownEntry —
679+
-- disable, drop its buttons, hide, release the ref. The next build creates a fresh container
680+
-- (topology is add-only — no RemoveAuraGroup/Slot — so recreate IS the sanctioned removal).
681+
-- Callers gate teardown out of combat (Destroy/_rebuild defer to regen in lockdown).
696682
function NativeBackend:teardown()
697-
self:_parkGroups()
683+
local c = self.container
684+
if c then
685+
pcall(function() c:SetEnabled(false) end)
686+
if type(c.RemoveAllAuraFrames) == "function" then
687+
pcall(function() c:RemoveAllAuraFrames() end)
688+
end
689+
pcall(function() c:Hide() end)
690+
end
698691
self.container = nil
699692
end
700693

@@ -921,14 +914,10 @@ function Handle:SetSort(sort)
921914
-- TODO(PTR-4): route to the managed backend's sort setter (rule + direction).
922915
end
923916

924-
-- Force a re-scan of the container. There is NO addon-callable Refresh() on b8f90f2a:
925-
-- container:UpdateAllAuras() is an empty stub on the inbound handle (the real refresh
926-
-- lives on the private mixin, reached only via OnShow/OnHide/OnEnabledChanged/
927-
-- OnUnitChanged), so the sanctioned trigger is a Hide();Show() bounce -> OnShow -> the
928-
-- secure refresh, with no filter rebuild or invalid-unit blip [Krathe, source-confirmed
929-
-- b8f90f2a]. Use on a dynamic-unit consumer (target/focus/mouseover) when the underlying
930-
-- unit changes but the token does not. (In-combat bounce safety is on the PTR audit
931-
-- list; a real Refresh() / wired UpdateAllAuras is a PTR-4 candidate.)
917+
-- Force a re-scan of the container. 68569: UpdateAllAuras() is an addon-callable
918+
-- dirty-mark (processed on the next OnUpdate while visible) — the real refresh. Use on
919+
-- a dynamic-unit consumer (target/focus/mouseover) when the underlying unit changes but
920+
-- the token does not. Falls back to an out-of-combat Hide/Show bounce if absent.
932921
function Handle:Refresh()
933922
if not self.backend then return end
934923
local ok, err = pcall(function() self.backend:refresh() end)
@@ -1116,11 +1105,8 @@ function AuraContainer:Create(parent, config)
11161105
local h = setmetatable({ config = cfg, buttons = {} }, Handle)
11171106
AuraContainer._handles = AuraContainer._handles or setmetatable({}, { __mode = "k" })
11181107
AuraContainer._handles[h] = true -- weak-keyed registry so a dropped handle GCs (else rebuild-forever on test toggle)
1119-
-- The parent IS the secure unit button; its .AuraContainer (created securely by the
1120-
-- SecureGroupHeader's auraContainerTemplate attribute) is what we render into.
1121-
h.parentButton = parent
1122-
_handleSeq = _handleSeq + 1
1123-
h._keyBase = "df" .. _handleSeq
1108+
-- h.frame is the plain anchor frame DF positions; the backend parents its OWN
1109+
-- CustomAuraContainer to it (per-consumer container — Krathe's ContainerOverlay pattern).
11241110
h.frame = CreateFrame("Frame", nil, parent)
11251111
-- Both modes: h.frame occupies the unit-frame rect (row layout anchors are relative
11261112
-- to it; overlay covers it). To reposition: h:ClearAllPoints() then h:SetPoint(...).

Frames/Headers.lua

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1111,15 +1111,6 @@ function DF:CreatePartyHeader()
11111111
-- Template
11121112
DF.partyHeader:SetAttribute("template", "DandersUnitButtonTemplate")
11131113

1114-
-- 12.1 (68569): when set BEFORE the header creates its unit buttons, the SecureGroupHeader
1115-
-- creates one AuraContainer per button (parented to it, exposed as button.AuraContainer) via
1116-
-- Blizzard's SECURE code — so it's untainted and its aura Show/Hide work in COMBAT. An
1117-
-- addon-created container is tainted and freezes in combat. Must precede the startingIndex
1118-
-- force-create below. Gated on the 12.1 container API being present.
1119-
if DF.AuraContainer and DF.AuraContainer.IsSupported and DF.AuraContainer.IsSupported() then
1120-
DF.partyHeader:SetAttribute("auraContainerTemplate", "CustomAuraContainerTemplate")
1121-
end
1122-
11231114
-- Layout attributes
11241115
local horizontal = db.growHorizontal
11251116
local spacing = db.frameSpacing or 2

0 commit comments

Comments
 (0)