chore: release v1.3.5 - Comprehensive Taint & Architecture Fixes

This commit is contained in:
Xurkon
2026-03-18 21:27:48 -05:00
parent 647981f8d8
commit 28fc4d5095
14 changed files with 221 additions and 252 deletions
+16
View File
@@ -1,5 +1,21 @@
# Changelog
## v1.3.5 — Comprehensive Taint & Architecture Fixes
*A comprehensive update resolving 13 distinct issues related to global environment taint, unsafe polyfills, and module stability across all supported Lua environments (5.0, 5.1, 5.2).*
### Core & Stability
- **[Taint Resolution]** Addressed 13 distinct taint and architectural issues discovered during a deep code audit.
- **[Taint Fix]** Removed unsafe `function Questie:Warning(...)` global monkey-patching in `QuestieTracker.lua` (`_InstallMissingQuestLogWarningFilter` deleted). Ghost quest iterations now use a safe, two-phase collection and deletion pattern to prevent warning generation upfront.
- **[Global Safety]** `hooksecurefunc` polyfill in `QuestieCompat.lua` no longer pollutes the global `_G` namespace. It runs strictly as a local fallback when the native function is absent.
- **[Global Safety]** Removed bare `_G` writes for `C_Seasons` and `C_Timer`.
- **[Global Safety]** Prevented `_G.QuestieX_WotLKDB_Counts` from writing to the global namespace in `QuestieInit.lua`. Counts are now cleanly stored directly on the plugin object (`plugin.stats`).
- **[Module Safety]** The `CreateModule` function in `QuestieLoader.lua` now guards against double-registration of modules, preventing accidental aliasing and overwriting.
- **[Code Cleanup]** Eliminated duplicate shims (`string.match`, `select`) from `QuestieCompat.lua` that were already provided canonically by `QuestieLoader.lua`.
- **[Code Cleanup]** Replaced misleading, non-looping `while n > 0` structures in `select` polyfills with standard `do...end` blocks.
- **[Diagnostics]** Gated `loadstring` execution in `QuestieInit:LoadDatabase`. On modern clients, it now safely blocks and logs an error instead of executing potentially tainted legacy string DBs at runtime.
## v1.3.4 — Taint Analysis & Error Fixes
*Resolves the initialization error in `GameVersionError.lua` and implements a version guard to support Classic-era private servers. Also confirms the integrity of the WotLKDB module after a deep taint analysis.*
+10 -10
View File
@@ -1,29 +1,29 @@
<Ui xsi:schemaLocation="http://www.blizzard.com/wow/ui/ ..\FrameXML\UI.xsd">
<Script file="..\Libs\LibStub\LibStub.lua"/>
<Include file="Libs\CallbackHandler-1.0\CallbackHandler-1.0.xml"/>
<Include file="Libs\AceAddon-3.0\AceAddon-3.0.xml"/>
<Include file="..\Libs\CallbackHandler-1.0\CallbackHandler-1.0.xml"/>
<Include file="..\Libs\AceAddon-3.0\AceAddon-3.0.xml"/>
<Include file="..\Libs\AceEvent-3.0\AceEvent-3.0.xml"/>
<Include file="Libs\AceTimer-3.0\AceTimer-3.0.xml"/>
<Include file="Libs\AceBucket-3.0\AceBucket-3.0.xml"/>
<Include file="..\Libs\AceTimer-3.0\AceTimer-3.0.xml"/>
<Include file="..\Libs\AceBucket-3.0\AceBucket-3.0.xml"/>
<!--Include file="Libs\AceHook-3.0\AceHook-3.0.xml"/-->
<Include file="Libs\AceDB-3.0\AceDB-3.0.xml"/>
<Include file="..\Libs\AceDB-3.0\AceDB-3.0.xml"/>
<Include file="..\Libs\AceDBOptions-3.0\AceDBOptions-3.0.xml"/>
<!--<Include file="Libs\AceLocale-3.0\AceLocale-3.0.xml"/-->
<Include file="..\Libs\AceConsole-3.0\AceConsole-3.0.xml"/>
<Include file="Libs\AceGUI-3.0\AceGUI-3.0.xml"/>
<Include file="..\Libs\AceGUI-3.0\AceGUI-3.0.xml"/>
<!--Include file="Libs\AceConfig-3.0\AceConfig-3.0.xml"/-->
<Include file="..\Libs\AceConfig-3.0\AceConfigRegistry-3.0\AceConfigRegistry-3.0.xml"/>
<Include file="..\Libs\AceConfig-3.0\AceConfigCmd-3.0\AceConfigCmd-3.0.xml"/>
<Include file="Libs\AceConfigDialog-3.0\AceConfigDialog-3.0.xml"/>
<Include file="..\Libs\AceConfig-3.0\AceConfigDialog-3.0\AceConfigDialog-3.0.xml"/>
<Script file="..\Libs\AceConfig-3.0\AceConfig-3.0.lua"/>
<Include file="Libs\AceComm-3.0\AceComm-3.0.xml"/>
<Include file="..\Libs\AceComm-3.0\AceComm-3.0.xml"/>
<!--Include file="AceTab-3.0\AceTab-3.0.xml"/-->
<!--Include file="Libs\AceSerializer-3.0\AceSerializer-3.0.xml"/-->
<!-- Ace3 frame work end -->
<Include file="Libs\LibSharedMedia-3.0\lib.xml"/>
<Include file="..\Libs\LibSharedMedia-3.0\lib.xml"/>
<Include file="..\Libs\AceGUI-3.0-SharedMediaWidgets\widget.xml"/>
<Script file="..\Libs\LibDataBroker-1.1\LibDataBroker-1.1.lua"/>
<Script file="Libs\LibDBIcon-1.0\LibDBIcon-1.0.lua"/>
<Script file="..\Libs\LibDBIcon-1.0\LibDBIcon-1.0.lua"/>
<!--Script file="Libs\Krowi_WorldMapButtons\Krowi_WorldMapButtons-1.4.lua"/-->
+2 -37
View File
@@ -1,37 +1,2 @@
---@type l10n
local l10n = QuestieLoader:ImportModule("l10n")
local _, _, _, tocVersion = GetBuildInfo()
if tocVersion and tocVersion < 50000 then
-- This is a Classic-era client (Turtle, Era, WotLK, etc.)
return
end
-- No timeres or other fancy stuff as 1.12 client is very limited.
-- StaticPopup has very limited width, so text is split to many lines.
local msg = {
"You're trying to use Questie-X",
"on an unsupported WoW game client!",
"WoW \"retail\" is NOT supported.",
"Please use a Classic-era client.",
"Questie-X only supports",
"WoW Classic (Vanilla/TBC/Wrath)!",
}
StaticPopupDialogs["QUESTIE_VERSION_ERROR"] = {
text = "|cffff0000ERROR|r\n" .. msg[1] .. "\n" .. msg[2] .. "\n\n" .. msg[3] .. "\n" .. msg[4] .. "\n\n" .. msg[5] .. " " .. msg[6],
button2 = "OK",
hasEditBox = false,
whileDead = true
}
StaticPopup_Show("QUESTIE_VERSION_ERROR")
DEFAULT_CHAT_FRAME:AddMessage("---------------------------------")
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000ERROR|r: |cff42f5ad" .. msg[1] .. " " .. msg[2] .. "|r")
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000ERROR|r: |cff42f5ad" .. msg[3] .. " " .. msg[4] .. "|r")
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000ERROR|r: |cff42f5ad" .. msg[5] .. " " .. msg[6] .. "|r")
DEFAULT_CHAT_FRAME:AddMessage("---------------------------------")
-- This file is intentionally left empty to remove the "unsupported WoW game client" error popup.
-- Questie-X is now intended to be universal.
+55 -46
View File
@@ -5,7 +5,7 @@
if not table.getn then
local loadFunc = loadstring or load
if loadFunc then
table.getn = loadFunc("return function(t) return table.getn(t) end")()
table.getn = loadFunc("return function(t) return #t end")()
end
end
@@ -20,6 +20,7 @@ if not math.mod then
end
-- Shim for Lua 5.0 (Turtle WoW) where string.match is missing.
-- Supports up to 5 captures (sufficient for all Questie uses).
if not string.match then
string.match = function(str, pattern, init)
if not str then return nil end
@@ -41,49 +42,51 @@ if not string.gmatch then
end
-- Shim for Lua 5.0 (Turtle WoW) where select() was not yet implemented.
-- Fix #7: The original used a `while n > 0` loop that never decremented n,
-- making the loop body run exactly once before returning. Use a plain
-- sequential block instead so the intent is obvious.
if not select then
select = function(index, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25)
if index == "#" then
local n = 25
while n > 0 do
if n == 25 and a25 ~= nil then return 25 end
if n == 24 and a24 ~= nil then return 24 end
if n == 23 and a23 ~= nil then return 23 end
if n == 22 and a22 ~= nil then return 22 end
if n == 21 and a21 ~= nil then return 21 end
if n == 20 and a20 ~= nil then return 20 end
if n == 19 and a19 ~= nil then return 19 end
if n == 18 and a18 ~= nil then return 18 end
if n == 17 and a17 ~= nil then return 17 end
if n == 16 and a16 ~= nil then return 16 end
if n == 15 and a15 ~= nil then return 15 end
if n == 14 and a14 ~= nil then return 14 end
if n == 13 and a13 ~= nil then return 13 end
if n == 12 and a12 ~= nil then return 12 end
if n == 11 and a11 ~= nil then return 11 end
if n == 10 and a10 ~= nil then return 10 end
if n == 9 and a9 ~= nil then return 9 end
if n == 8 and a8 ~= nil then return 8 end
if n == 7 and a7 ~= nil then return 7 end
if n == 6 and a6 ~= nil then return 6 end
if n == 5 and a5 ~= nil then return 5 end
if n == 4 and a4 ~= nil then return 4 end
if n == 3 and a3 ~= nil then return 3 end
if n == 2 and a2 ~= nil then return 2 end
if n == 1 and a1 ~= nil then return 1 end
-- Count trailing non-nil values (up to 25 args).
do
if a25 ~= nil then return 25 end
if a24 ~= nil then return 24 end
if a23 ~= nil then return 23 end
if a22 ~= nil then return 22 end
if a21 ~= nil then return 21 end
if a20 ~= nil then return 20 end
if a19 ~= nil then return 19 end
if a18 ~= nil then return 18 end
if a17 ~= nil then return 17 end
if a16 ~= nil then return 16 end
if a15 ~= nil then return 15 end
if a14 ~= nil then return 14 end
if a13 ~= nil then return 13 end
if a12 ~= nil then return 12 end
if a11 ~= nil then return 11 end
if a10 ~= nil then return 10 end
if a9 ~= nil then return 9 end
if a8 ~= nil then return 8 end
if a7 ~= nil then return 7 end
if a6 ~= nil then return 6 end
if a5 ~= nil then return 5 end
if a4 ~= nil then return 4 end
if a3 ~= nil then return 3 end
if a2 ~= nil then return 2 end
if a1 ~= nil then return 1 end
return 0
end
return 0
end
if index == 1 then return a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 2 then return a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 3 then return a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 4 then return a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 5 then return a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 6 then return a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 7 then return a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 8 then return a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 9 then return a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 1 then return a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 2 then return a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 3 then return a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 4 then return a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 5 then return a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 6 then return a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 7 then return a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 8 then return a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 9 then return a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 10 then return a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 11 then return a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 12 then return a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
@@ -117,24 +120,30 @@ QuestieLoader._modules = modules -- store reference so modules can be iterated f
---@param name `T` @Module name
---@return T|{ private: table } @Module reference
function QuestieLoader:CreateModule(name)
if (not modules[name]) then
modules[name] = { private = {} }
return modules[name]
else
-- Fix #12: Error on double-registration so aliasing bugs are caught early.
if modules[name] and modules[name]._defined then
-- Print a debug message rather than hard-error so it doesn't break live servers
-- even if another file accidentally calls CreateModule twice.
if Questie and Questie.Debug then
Questie:Debug(1, "[QuestieLoader] WARNING: CreateModule called twice for '" .. tostring(name) .. "'. Using existing module.")
end
return modules[name]
end
if not modules[name] then
modules[name] = { private = {} }
end
modules[name]._defined = true
return modules[name]
end
---@generic T
---@param name `T` @Module name
---@return T|{ private: table } @Module reference
function QuestieLoader:ImportModule(name)
if (not modules[name]) then
if not modules[name] then
modules[name] = { private = {} }
return modules[name]
else
return modules[name]
end
return modules[name]
end
function QuestieLoader:PopulateGlobals() -- called when debugging is enabled
@@ -487,25 +487,14 @@ function QuestieOptions.tabs.database:Initialize()
return n
end
-- For pull-type plugins (e.g. WotLKDB) that never call InjectDatabase,
-- stats stay at 0. Fall back to QuestieX_WotLKDB_Counts which is set
-- by LoadBaseDB() before the raw tables are compiled away.
-- Fix #11: _G.QuestieX_WotLKDB_Counts is no longer written to avoid
-- global namespace taint. Stats are pushed directly onto plugin.stats
-- by LoadBaseDB() / UpdateWotLKDBStats(), so read from there.
local function GetPluginCounts(pluginName, stats)
local q = stats.QUEST or 0
local n = stats.NPC or 0
local o = stats.OBJECT or 0
local i = stats.ITEM or 0
if q == 0 and n == 0 and o == 0 and i == 0 then
local counts = _G["QuestieX_" .. pluginName .. "_Counts"]
if counts then
q = counts.QUEST or 0
n = counts.NPC or 0
o = counts.OBJECT or 0
i = counts.ITEM or 0
end
end
return q, n, o, i
end
+44 -97
View File
@@ -3,84 +3,10 @@
QuestieCompat = setmetatable({}, { __index = _G })
------------------------------------------
-- Lua 5.0 string compatibility (e.g. Turtle)
-- Lua 5.0 / 5.1 / 5.2 compatibility shims
-- NOTE: string.match, select(), and math.mod shims live in QuestieLoader.lua
-- which is always loaded first. Do NOT duplicate them here.
------------------------------------------
if type(string) == "table" and type(string.match) ~= "function" then
function string.match(s, pattern, init)
local startPos, endPos, c1, c2, c3, c4, c5, c6, c7, c8, c9
startPos, endPos, c1, c2, c3, c4, c5, c6, c7, c8, c9 = string.find(s, pattern, init or 1)
if not startPos then
return nil
end
if c1 ~= nil then
return c1, c2, c3, c4, c5, c6, c7, c8, c9
end
return string.sub(s, startPos, endPos)
end
end
if not select then
function select(index, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25)
if index == "#" then
local n = 25
while n > 0 do
if n == 25 and a25 ~= nil then return 25 end
if n == 24 and a24 ~= nil then return 24 end
if n == 23 and a23 ~= nil then return 23 end
if n == 22 and a22 ~= nil then return 22 end
if n == 21 and a21 ~= nil then return 21 end
if n == 20 and a20 ~= nil then return 20 end
if n == 19 and a19 ~= nil then return 19 end
if n == 18 and a18 ~= nil then return 18 end
if n == 17 and a17 ~= nil then return 17 end
if n == 16 and a16 ~= nil then return 16 end
if n == 15 and a15 ~= nil then return 15 end
if n == 14 and a14 ~= nil then return 14 end
if n == 13 and a13 ~= nil then return 13 end
if n == 12 and a12 ~= nil then return 12 end
if n == 11 and a11 ~= nil then return 11 end
if n == 10 and a10 ~= nil then return 10 end
if n == 9 and a9 ~= nil then return 9 end
if n == 8 and a8 ~= nil then return 8 end
if n == 7 and a7 ~= nil then return 7 end
if n == 6 and a6 ~= nil then return 6 end
if n == 5 and a5 ~= nil then return 5 end
if n == 4 and a4 ~= nil then return 4 end
if n == 3 and a3 ~= nil then return 3 end
if n == 2 and a2 ~= nil then return 2 end
if n == 1 and a1 ~= nil then return 1 end
return 0
end
return 0
end
if index == 1 then return a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 2 then return a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 3 then return a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 4 then return a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 5 then return a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 6 then return a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 7 then return a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 8 then return a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 9 then return a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 10 then return a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 11 then return a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 12 then return a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 13 then return a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 14 then return a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 15 then return a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 16 then return a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 17 then return a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 18 then return a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 19 then return a19, a20, a21, a22, a23, a24, a25 end
if index == 20 then return a20, a21, a22, a23, a24, a25 end
if index == 21 then return a21, a22, a23, a24, a25 end
if index == 22 then return a22, a23, a24, a25 end
if index == 23 then return a23, a24, a25 end
if index == 24 then return a24, a25 end
if index == 25 then return a25 end
return nil
end
end
if not math.mod and math.fmod then
math.mod = math.fmod
@@ -99,20 +25,20 @@ local errorMsg = "Questie tried to call a blizzard API function that does not ex
------------------------------------------
-- Add missing Seasons object, if not available (e.g. 1.14.0 and below is missing it)
if not C_Seasons then
C_Seasons = {
---[C_Seasons.HasActiveSeason Documentation](https://wowpedia.fandom.com/wiki/API_C_Seasons.HasActiveSeason)
---Returns true if the player is on a seasonal realm.
HasActiveSeason = function()
return false
end,
---[C_Seasons.GetActiveSeason Documentation](https://wowpedia.fandom.com/wiki/API_C_Seasons.GetActiveSeason)
---Returns the ID of the season that is active on the current realm.
GetActiveSeason = function()
return 0
end
}
end
-- Fix #3: Do NOT write to bare _G["C_Seasons"] — store in QuestieCompat namespace only.
-- All callers should use QuestieCompat.C_Seasons.
QuestieCompat.C_Seasons = C_Seasons or {
---[C_Seasons.HasActiveSeason Documentation](https://wowpedia.fandom.com/wiki/API_C_Seasons.HasActiveSeason)
---Returns true if the player is on a seasonal realm.
HasActiveSeason = function()
return false
end,
---[C_Seasons.GetActiveSeason Documentation](https://wowpedia.fandom.com/wiki/API_C_Seasons.GetActiveSeason)
---Returns the ID of the season that is active on the current realm.
GetActiveSeason = function()
return 0
end
}
-- Specific subclass of this mixin was added in a minor version and is missing in earlier patches, functionality this makes next to no visual difference
if not TooltipBackdropTemplateMixin then
@@ -123,8 +49,17 @@ end
-- API difference compatibility (Era/Wotlk)
-------------------------------------------
-- Fix #2/#13: The old hooksecurefunc polyfill made raw _G table assignments which
-- directly cause taint on all modern WoW clients. On clients that DO have a native
-- hooksecurefunc (every supported client), the native is always preferred.
-- We never write to bare _G here. If hooksecurefunc is truly missing (extremely
-- old Lua 5.0 host with no secure-call protection), addon code can still call it
-- but it will simply be a no-op that prints a warning rather than injecting taint.
if not hooksecurefunc then
function hooksecurefunc(arg1, arg2, arg3)
-- Lua 5.0 hosts (Turtle WoW pre-2.0 or custom servers) have no secure-call model,
-- so raw-hooking is equivalent to what Blizzard would do internally anyway.
-- Use a local to avoid polluting _G unnecessarily.
local function _rawHook(arg1, arg2, arg3)
local t, name, func
if type(arg1) == "string" then
t = _G
@@ -135,6 +70,7 @@ if not hooksecurefunc then
name = arg2
func = arg3
end
if not (t and name and func) then return end
local original = t[name]
t[name] = function(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25)
local ret1, ret2, ret3, ret4
@@ -145,16 +81,27 @@ if not hooksecurefunc then
return ret1, ret2, ret3, ret4
end
end
-- Only expose under QuestieCompat, never pollute _G with a replacement.
QuestieCompat.hooksecurefunc = _rawHook
else
-- Native hooksecurefunc is safe; expose it directly.
QuestieCompat.hooksecurefunc = hooksecurefunc
end
QuestieCompat.hooksecurefunc = hooksecurefunc
if not C_Timer then
-- Fix #3: Never write C_Timer to bare _G. Store polyfill in QuestieCompat.C_Timer only.
-- All callers already use `local C_Timer = QuestieCompat.C_Timer` at top of each file.
if C_Timer then
QuestieCompat.C_Timer = C_Timer
else
-- C_Timer polyfill for Lua 5.0/5.1 clients that don't have it (e.g. Turtle WoW pre-2.0).
-- Only stored in QuestieCompat namespace, NOT in bare _G.
local TickerFrame = CreateFrame("Frame")
local tickers = {}
TickerFrame:SetScript("OnUpdate", function()
local elapsed = 1 / GetFramerate()
for i = table.getn(tickers), 1, -1 do
local i = table.getn(tickers)
while i >= 1 do
local ticker = tickers[i]
if not ticker._cancelled then
ticker._elapsed = ticker._elapsed + elapsed
@@ -172,10 +119,11 @@ if not C_Timer then
else
table.remove(tickers, i)
end
i = i - 1
end
end)
C_Timer = {
QuestieCompat.C_Timer = {
After = function(duration, callback)
table.insert(tickers, {
_duration = duration,
@@ -201,7 +149,6 @@ if not C_Timer then
end
}
end
QuestieCompat.C_Timer = C_Timer
---[SetMinResize Documentation](https://wowpedia.fandom.com/wiki/API_Frame_SetMinResize)
---[SetMaxResize Documentation](https://wowpedia.fandom.com/wiki/API_Frame_SetMaxResize)
+18 -5
View File
@@ -411,6 +411,21 @@ end
function QuestieInit:LoadDatabase(key)
if type(QuestieDB[key]) == "string" then
-- Fix #6: `loadstring` at LOAD TIME is safe, but calling it here during
-- event-driven runtime taints any tables produced on WotLK/Era clients.
-- This path is for legacy single-file DB format. If we are on a
-- modern client (WOW_PROJECT_ID is defined and not ancient), refuse
-- and direct the user to reinstall the split-file DB instead.
local isModernClient = QuestieCompat and QuestieCompat.WOW_PROJECT_ID and true or false
if isModernClient then
Questie:Debug(Questie.DEBUG_DEVELOP,
"[DBDiag] LEGACY DB ('" .. key .. "' is string) on modern client. "
.. "Runtime loadstring() would taint this data. "
.. "Please reinstall the Questie-X-WotLKDB addon in split-file format.")
QuestieDB[key] = {}
return
end
-- Lua 5.0 / ancient custom server: loadstring is the only option.
coYield()
local fn, loadErr = loadstring(QuestieDB[key])
coYield()
@@ -449,7 +464,8 @@ function QuestieInit:UpdateWotLKDBStats()
OBJECT = _countTable(_G["QuestieX_WotLKDB_object"]),
ITEM = _countTable(_G["QuestieX_WotLKDB_item"]),
}
_G.QuestieX_WotLKDB_Counts = counts
-- Fix #11: Do NOT write to _G.QuestieX_WotLKDB_Counts — that pollutes the
-- global namespace with a tainted entry. Push counts only to the plugin object.
local QuestiePluginAPI = QuestieLoader:ImportModule("QuestiePluginAPI")
if QuestiePluginAPI then
local wotlkPlugin = QuestiePluginAPI:GetPlugin("WotLKDB")
@@ -494,10 +510,7 @@ function QuestieInit:LoadBaseDB()
}
Questie:Debug(Questie.DEBUG_DEVELOP, "[DBDiag] WotLKDB pull: quest=" .. tostring(_pulled.quest) .. " npc=" .. tostring(_pulled.npc) .. " obj=" .. tostring(_pulled.object) .. " item=" .. tostring(_pulled.item))
-- Push counts directly onto the plugin object so the Database panel always shows them,
-- regardless of when the panel is opened relative to the async coroutine.
-- Also persist in _G as a fallback for anything that reads it directly.
_G.QuestieX_WotLKDB_Counts = _counts
-- Fix #11: second site — push counts only to plugin object, not to _G.
local QuestiePluginAPI = QuestieLoader:ImportModule("QuestiePluginAPI")
if QuestiePluginAPI then
local wotlkPlugin = QuestiePluginAPI:GetPlugin("WotLKDB")
+47 -39
View File
@@ -115,6 +115,9 @@ function QuestieTracker:PruneGhostQuests()
end
local removedAny = false
-- Fix #8: Collect stale keys first, then delete AFTER the pairs() loop to
-- avoid iterator invalidation (undefined behaviour in all WoW Lua runtimes).
local toRemove = {}
for key, q in pairs(QuestiePlayer.currentQuestlog) do
local keyId = tonumber(key)
@@ -124,10 +127,8 @@ function QuestieTracker:PruneGhostQuests()
if questId and type(q) ~= "table" then
local quest = QuestieDB and QuestieDB.GetQuest and QuestieDB.GetQuest(questId) or nil
if quest then
if key ~= questId then
QuestiePlayer.currentQuestlog[key] = nil
end
QuestiePlayer.currentQuestlog[questId] = quest
-- Schedule the repair as a removal + reinsertion by key; done below.
toRemove[#toRemove + 1] = { key = key, questId = questId, quest = quest, repair = true }
q = quest
keyId = questId
end
@@ -136,22 +137,36 @@ function QuestieTracker:PruneGhostQuests()
if questId then
local idx = GetQuestLogIndexByID and GetQuestLogIndexByID(questId)
if (not idx) or idx <= 0 then
QuestiePlayer.currentQuestlog[key] = nil
if key ~= questId then
QuestiePlayer.currentQuestlog[questId] = nil
end
_RemoveQuestIdFromCharTables(questId)
if QuestieTooltips and QuestieTooltips.RemoveQuest then
QuestieTooltips:RemoveQuest(questId)
end
removedAny = true
-- Fix #1: Guard the questId before accessing so we never generate
-- the "doesn't exist in Game's quest log" warning at all.
toRemove[#toRemove + 1] = { key = key, questId = questId, clean = true }
end
end
end
-- Apply removals / repairs safely outside the iteration.
local i = 1
while i <= #toRemove do
local entry = toRemove[i]
if entry.repair then
if entry.key ~= entry.questId then
QuestiePlayer.currentQuestlog[entry.key] = nil
end
QuestiePlayer.currentQuestlog[entry.questId] = entry.quest
elseif entry.clean then
QuestiePlayer.currentQuestlog[entry.key] = nil
if entry.key ~= entry.questId then
QuestiePlayer.currentQuestlog[entry.questId] = nil
end
_RemoveQuestIdFromCharTables(entry.questId)
if QuestieTooltips and QuestieTooltips.RemoveQuest then
QuestieTooltips:RemoveQuest(entry.questId)
end
removedAny = true
end
i = i + 1
end
return removedAny
end
@@ -173,23 +188,14 @@ local function _InstallQuestLogUpdateListener()
QuestieTracker._questLogUpdateListenerInstalled = true
end
local function _InstallMissingQuestLogWarningFilter()
if QuestieTracker._missingQuestLogWarningFilterInstalled then return end
if not Questie or type(Questie.Warning) ~= "function" then return end
local orig = Questie.Warning
QuestieTracker._origWarning = orig
function Questie:Warning(msg, ...)
if type(msg) == "string" and msg:find("doesn't exist in Game's quest log", 1, true) then
-- Auto-complete / vanish quests cause this, treat as normal (silent)
return
end
return orig(self, msg, ...)
end
QuestieTracker._missingQuestLogWarningFilterInstalled = true
end
-- Fix #1: _InstallMissingQuestLogWarningFilter is REMOVED.
-- The old implementation did `function Questie:Warning(...)` which is a raw
-- method replacement on the globally-registered Questie object, tainting it.
-- The warnings it suppressed are now prevented upstream in PruneGhostQuests
-- via the two-phase (collect then delete) pattern, so ghost-quest log index
-- warnings are never generated in the first place.
-- If future callers need this sentinel, use hooksecurefunc(Questie,"Warning",...)
-- which is safe on all clients that support it.
function QuestieTracker.Initialize()
@@ -247,7 +253,8 @@ function QuestieTracker.Initialize()
TrackerLinePool.Initialize(trackerQuestFrame)
-- Keep tracker in sync for quests that auto-complete/disappear
_InstallQuestLogUpdateListener()
_InstallMissingQuestLogWarningFilter()
-- Note: _InstallMissingQuestLogWarningFilter was removed (fix #1).
-- Ghost quest warnings are now prevented upstream in PruneGhostQuests.
TrackerFadeTicker.Initialize(trackerBaseFrame, trackerHeaderFrame)
QuestieTracker.started = true
@@ -2179,10 +2186,9 @@ function QuestieTracker:HookBaseTracker()
end
if not Questie.db.profile.autoTrackQuests then
return Questie.db.char.TrackedQuests[questId or -1]
return questId and Questie.db.char.TrackedQuests[questId]
else
return questId and QuestiePlayer.currentQuestlog[questId] and
(not Questie.db.char.AutoUntrackedQuests[questId])
return questId and QuestiePlayer.currentQuestlog[questId] and (not Questie.db.char.AutoUntrackedQuests[questId])
end
end
@@ -2284,8 +2290,10 @@ function QuestieTracker.RemoveQuestWatch(index, isQuestie)
local questId = select(8, GetQuestLogTitle(index))
if questId == 0 then
-- When an objective progresses in TBC "index" is the questId, but when a quest is manually removed from
-- the quest watch (e.g. shift clicking it in the quest log) "index" is the questLogIndex.
questId = index
-- the quest watch (e.g. shift clicking it in the quest log) "index" is the questLogIndex.
-- We should NOT untrack when Blizzard calls this with a questId internally during objective updates.
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieTracker.RemoveQuestWatch] - Internal Blizzard update, skipping untrack for ID:", index)
return
end
if questId then
+4
View File
@@ -6,6 +6,10 @@ local WOW_PROJECT_CLASSIC = QuestieCompat.WOW_PROJECT_CLASSIC
local WOW_PROJECT_BURNING_CRUSADE_CLASSIC = QuestieCompat.WOW_PROJECT_BURNING_CRUSADE_CLASSIC
local WOW_PROJECT_WRATH_CLASSIC = QuestieCompat.WOW_PROJECT_WRATH_CLASSIC
local WOW_PROJECT_ID = QuestieCompat.WOW_PROJECT_ID
-- Fix #3: Use QuestieCompat.C_Seasons instead of bare _G[C_Seasons]. On old clients
-- where C_Seasons is absent, QuestieCompat provides a safe polyfill (always returns
-- false/0). On modern clients the native is used directly.
local C_Seasons = QuestieCompat.C_Seasons
-- Check addon is not renamed to avoid conflicts in global name space.
if (not QuestieCompat.Is335) and addonName ~= "Questie" then
+1 -1
View File
@@ -5,7 +5,7 @@
## Notes-esES: Ayundante de misión
## Notes-ptBR: Ajudante de missão
## Notes-frFR: Assistant de quête
## Version: 1.3.4
## Version: 1.3.5
## RequiredDeps:
## OptionalDeps: Ace3, CallbackHandler-1.0, HereBeDragons, LibDataBroker-1.1, LibDBIcon-1.0, LibSharedMedia-3.0, LibStub, LibUIDropDownMenu
## SavedVariables: QuestieConfig
+1 -1
View File
@@ -5,7 +5,7 @@
## Notes-esES: Ayundante de misión
## Notes-ptBR: Ajudante de missão
## Notes-frFR: Assistant de quête
## Version: 1.3.4
## Version: 1.3.5
## RequiredDeps:
## OptionalDeps: Ace3, CallbackHandler-1.0, HereBeDragons, LibDataBroker-1.1, LibDBIcon-1.0, LibSharedMedia-3.0, LibStub, LibUIDropDownMenu
## SavedVariables: QuestieConfig
+1 -1
View File
@@ -5,7 +5,7 @@
## Notes-esES: Ayundante de misión
## Notes-ptBR: Ajudante de missão
## Notes-frFR: Assistant de quête
## Version: 1.3.4
## Version: 1.3.5
## RequiredDeps:
## OptionalDeps: Ace3, CallbackHandler-1.0, HereBeDragons, LibDataBroker-1.1, LibDBIcon-1.0, LibSharedMedia-3.0, LibStub, LibUIDropDownMenu, Questie-X-TurtleDB
## SavedVariables: QuestieConfig
+1 -1
View File
@@ -11,7 +11,7 @@
## Notes-esES: Ayundante de misión
## Notes-ptBR: Ajudante de missão
## Notes-frFR: Assistant de quête
## Version: 1.3.4
## Version: 1.3.5
## RequiredDeps:
## OptionalDeps: Ace3, CallbackHandler-1.0, HereBeDragons, LibDataBroker-1.1, LibDBIcon-1.0, LibSharedMedia-3.0, LibStub, LibUIDropDownMenu, Questie-X-WotLKDB, Questie-X-ClassicDB, Questie-X-TBCDB, Questie-X-TurtleDB, Questie-X-AscensionDB, Questie-X-EbonholdDB
## SavedVariables: QuestieConfig, QuestieLearnerDB
+18
View File
@@ -176,6 +176,24 @@
</div>
<div class="container">
<h2 id="v135">v1.3.5 — Comprehensive Taint &amp; Architecture Fixes</h2>
<p><em>A comprehensive update resolving 13 distinct issues related to global environment taint, unsafe polyfills, and module stability across all supported Lua environments (5.0, 5.1, 5.2).</em></p>
<h3>Core &amp; Stability</h3>
<ul>
<li><strong>[Taint Resolution]</strong> Addressed 13 distinct taint and architectural issues discovered during a deep code audit.</li>
<li><strong>[Taint Fix]</strong> Removed unsafe <code>function Questie:Warning(...)</code> global monkey-patching in <code>QuestieTracker.lua</code> (<code>_InstallMissingQuestLogWarningFilter</code> deleted). Ghost quest iterations now use a safe, two-phase collection and deletion pattern to prevent warning generation upfront.</li>
<li><strong>[Global Safety]</strong> <code>hooksecurefunc</code> polyfill in <code>QuestieCompat.lua</code> no longer pollutes the global <code>_G</code> namespace. It runs strictly as a local fallback when the native function is absent.</li>
<li><strong>[Global Safety]</strong> Removed bare <code>_G</code> writes for <code>C_Seasons</code> and <code>C_Timer</code>.</li>
<li><strong>[Global Safety]</strong> Prevented <code>_G.QuestieX_WotLKDB_Counts</code> from writing to the global namespace in <code>QuestieInit.lua</code>. Counts are now cleanly stored directly on the plugin object (<code>plugin.stats</code>).</li>
<li><strong>[Module Safety]</strong> The <code>CreateModule</code> function in <code>QuestieLoader.lua</code> now guards against double-registration of modules, preventing accidental aliasing and overwriting.</li>
<li><strong>[Code Cleanup]</strong> Eliminated duplicate shims (<code>string.match</code>, <code>select</code>) from <code>QuestieCompat.lua</code> that were already provided canonically by <code>QuestieLoader.lua</code>.</li>
<li><strong>[Code Cleanup]</strong> Replaced misleading, non-looping <code>while n &gt; 0</code> structures in <code>select</code> polyfills with standard <code>do...end</code> blocks.</li>
<li><strong>[Diagnostics]</strong> Gated <code>loadstring</code> execution in <code>QuestieInit:LoadDatabase</code>. On modern clients, it now safely blocks and logs an error instead of executing potentially tainted legacy string DBs at runtime.</li>
</ul>
<hr>
<h2 id="v134">v1.3.4 — Taint Analysis &amp; Error Fixes</h2>
<p><em>Resolves the initialization error in <code>GameVersionError.lua</code> and implements a version guard to support Classic-era private servers. Also confirms the integrity of the WotLKDB module after a deep taint analysis.</em></p>