Status: planned (M9). This chapter describes the environment wowlua test
runs addons in (Tooling §16.1): a Lua
VM and a WoW engine written in Rust, faithful enough that a test catches what
the game client would raise.
17.1 Goals
- Fidelity over convenience. A test passes only if the same code would run in the client of that flavor. Where the emulation cannot know the client's behaviour, it raises a clear error instead of guessing.
- The bugs that matter. Taint, combat lockdown, secret values, flavor differences and WoW's own Lua differences are where addons break in game; the emulation models them, and reports them better than the game does.
- Verified against the client. Every behaviour is backed by the Lua 5.1
reference, Blizzard's documentation and UI code, or an in-game probe
(
/stdtest probe), and the verification is automated (§17.8). - One binary. The VM and the engine are part of
wowlua, in Rust, with no C toolchain (decision T185).
17.2 The VM
A Lua 5.1 virtual machine in Rust, in its own crate (wowlua-vm).
- Language. Lua 5.1 as the client runs it: values, tables with metatables
and every metamethod, closures and upvalues, varargs, coroutines,
setfenv/getfenv, weak tables,__gcon userdata, error values andpcall/xpcallwith tracebacks,string.dumpabsent. - Input. The VM runs what the compiler produces: wowlua's output and plain
Lua files, with the compiler's source maps, so every location the VM reports
is a
.wowlua(or.lua) line and column. - Standard library. Exact Lua 5.1 behaviour: Lua patterns,
string.format(including%gand%qoutput byte for byte),tostringof numbers, table sorting, number parsing. - Determinism. Iteration order, random numbers (
math.random,fastrandom, seeded per test), time and memory are deterministic, so a test run repeats exactly. - Instrumentation. Coverage counters (lines, statements, branches, functions, conditions) and profiling are counted by the VM, not inserted into the code (§17.6, Tooling §16.1.6).
- Limits. Lua's limits apply as in the client (200 locals, 60 upvalues, C stack depth), so code that compiles but would not load in game fails here too.
Its design (§17.9): a register bytecode of its own, a tracing garbage collector, and stackless coroutines.
17.3 WoW's Lua
The differences from Lua 5.1 elsewhere, as probed on WoW: Forever (decisions S37, S38):
- Libraries. No
os,io,debugorpackage;date,timeanddifftimeas globals. The client's additions tostring,tableandmath(Types §2.8),bit, and WoW's global aliases and helpers (strsplit,strjoin,strtrim,tinsert,wipe,debugstack,debuglocals,debugprofilestop,geterrorhandler, …). - Arithmetic.
x / 0andx % 0raise "Division by zero";math.sqrtandmath.lograise on a negative argument;string.format("%d", x)raises for an infinitex. - Limits. The client's limits, at its thresholds, from crash-search
probes (
/stdtest crashlimit, separate from/stdtest probe): a C function's frame holds 8000 slots with its arguments (unpackandstring.bytefail beyond, "too many results to unpack", "stack overflow (string slice too long)");strsplitreturns at most 4000 pieces ("strsplit(): Stack overflow"). The client runs its Lua with API assertions on and gives C functions 100 free slots:pcallreturning its argument count plus 100 or more values (xpcallone fewer, its handler takes a slot;pcallwithenvthree fewer, it pushes the function and its environment after them) pushes its status past them, the assertionL->top < L->ci->topfails and the game crashes. Such calls fail with an error naming the call (decision T194). The call depth is Lua 5.1's: a thread's call-info array (a slot per Lua or C call) doubles from 8, so the first "stack overflow" comes at 16384 calls; after one, the array is cut back to 20000 slots and each collection may halve it again (the next overflow comes at 20000, then 19968, 19456 or 18432), and while handling one it may grow to 32768 ("error in error handling"). Nested C calls (pcall, metamethods,loadstring's parser, which counts its syntax levels on from them) stop at 200 with "C stack overflow"; unlike stock Lua 5.1's, the client's coroutines continue their resumer's count, so nestingcoroutine.resumestops there too instead of overflowing the C stack.securecall,securecallfunction,securecallmethodand the coroutine functions return up to the 8000 slots without crashing. - Per flavor. Where flavors differ (a library function, an arithmetic rule), the VM follows the flavor under test. Behaviour not yet probed on a flavor follows the newest probed client and is listed in the emulation's coverage report (§17.8).
17.3.1 Layers
The client's in-game environment, sorted from the client itself
(/stdtest inventory, recorded in crates/wowlua-vm/tests/client/) against
Blizzard's UI code and API documentation for the build:
| Layer | What | Where wowlua has it |
|---|---|---|
| Lua runtime | Lua 5.1's libraries and the client's C extensions: the string, table, math and bit additions, the aliases (strfind, tinsert, …), date/time, debugstack, the secure API, and the FrameScript functions (issecretvalue, securecallmethod, Mixin, …) |
the VM (Vm::wow) |
| Engine API | C functions: the documented API (C_* and globals) and older undocumented ones (GetActionInfo, …) |
the engine (§17.6) |
| Blizzard's Lua | FrameXML and SharedXML: print, tostringall, assertf, the degree-based sin/cos, mixins, and Lua wrappers even inside C_ namespaces (C_Traits.GetEntryInfo) |
Blizzard's own code, loaded by the engine (§17.7) |
| GlueXML | the login and character screens' Lua | not in game; not emulated |
On WoW Forever 1.60.1 (70374): 5,037 documented C functions, 2,059 undocumented ones (151 of them the runtime's), 3,942 Lua functions, every one defined in the published UI code for the build.
17.4 Taint and secure execution
The client's security model, in the VM:
-
Secure and insecure code. Blizzard's code (the engine's Lua and, when loaded, FrameXML, §17.7) runs secure; addon code runs insecure, tainted with its addon's name.
-
Propagation. A value written by insecure code carries that taint; secure code that reads a tainted value becomes tainted for the rest of its execution. Taint follows table fields, globals, upvalues and locals, and crosses coroutines as in the client.
-
The secure API.
issecure,issecurevariable,securecall,securecallfunction,secureexecuterange,hooksecurefunc(hooks run insecure without tainting the hooked call),forceinsecure,scrub, and the taint barriers the client has (error handlers,__gc). -
Protected actions. Protected functions and frame methods are refused from tainted execution: the call does nothing, the client's event fires with the tainting addon's name and the action, and the test fails.
- Protected global functions, always:
ADDON_ACTION_FORBIDDENwith the addon and the action as Lua's debug information names the call ("MyAddon", "UseAction()"fromUseAction(…), as/stdtest taintrecorded;UNKNOWN()throughpcall,/stdtest engine protected); the call returns its documented results' empty values (falsefromClearTarget). The documentation does not mark them; the engine lists the known ones (targeting, casting, using actions and items, movement, macros), the harmless ones checked in game: TargetUnit, ClearTarget, FocusUnit, AssistUnit, SpellStopCasting and SpellStopTargeting, the movement stops and PetFollow are protected; ClearFocus, TargetLastTarget and StopAttack are not. - Protected methods (the documentation's
IsProtectedFunction:Show,Hide,SetPoint,SetSize,SetParent, …) andSetAttributeof a protected frame, in combat lockdown:ADDON_ACTION_BLOCKED, after the method's own argument checks (/stdtest engine blocked). A frame is protected through a secure template (Secure…Template;IsProtectedthen givestrue, true) ormock.protect.
- Protected global functions, always:
-
Taint reports. A refusal reports the taint chain: the protected call, the secure code that became tainted, the value it read, and the addon code (file and line) that wrote that value, through any number of hops through secure code (decision T196). This is what
taint.logcannot show:UseAction() blocked (ADDON_ACTION_BLOCKED) ActionButton_OnClick FrameXML/ActionButton.lua:211 read field 'action' written by ActionBarButton_Update FrameXML/ActionButton.lua:98 while tainted by MyAddon, after reading global 'ACTIONBAR_PAGE' written by MyAddon src/bars.wowlua:42Every tainted value knows the event behind its taint: the store that put it there, and what tainted the code that stored it; every tainted execution, the read that tainted it. Tests can also assert that a value or a handler stays secure, and get the same chain when it does not (decision T197).
17.5 Secret values
Secret values (WoW Forever and the 12.x clients) are a value kind of the VM,
as the client has them (probed on WoW Forever 1.60.1, /stdtest probe):
- What tainted code may do. A secret keeps its type:
type, passing, storing as a table value,select,unpack,pcallanderrorwork, and so does whatever needs only the type: truth tests of a secret number, string or table (a secretnilis false),notof those, and==with a value of another type (false) or of a secretnilwithnil(true). Secret strings and numbers concatenate into a secret string, andtostring,string.format,strjoinandstrconcatof a secret give a secret string. - What it may not. Arithmetic, comparisons (
==of two values of one type too),#, indexing, calls,..of other types, and a secret boolean's truth test fail with the client's error, naming the operand as Lua does: "attempt to compare local 's' (a secret number value, while execution tainted by 'MyAddon')", or without a name "attempt to compare a secret number value (execution tainted by 'MyAddon')". A secret table key fails for reads and writes ("attempted to index a table that cannot be indexed with secret keys"). Library functions see the typesecret("table expected, got secret") and refuse to convert one ("attempt to perform string conversion on a secret string value (execution tainted by …)"); the client's own functions with usage messages add "Secret values are only allowed during untainted execution for this argument". - The functions.
secretwrap,issecretvalue,hasanysecretvalues,scrubsecretvalues,scrub,canaccesssecrets,canaccessvalue,canaccessallvalues,canaccesstable,issecrettable,dropsecretaccess,mapvalues,dumpobject,settablesecurityandsecurecopybehave as in the client. - Secure code may use secrets; what it computes from one is secret again (decision T195, from Blizzard's documentation: addon code cannot observe it).
The engine returns secrets where the client does: from the documentation's
SecretReturns, SecretArguments and SecretWhen… flags, under the
addon restrictions in effect (combat, encounters, challenge modes, PvP
matches, restricted maps and chat lockdown; /stdtest restrictions
records the client's by the restrictions active, in
tests/client/forever-camelot-restrictions.lua). The rules above do not
change with them: every conformance case gives the same result in combat on
a restricted map as out of it.
17.6 The engine
The WoW API the VM's code calls, per flavor.
- Generated. Every function, namespace, event, enum and widget method is
generated from the flavor's API documentation (the same source as the
wow-apideclarations, decision T83): its signature, its existence in the flavor, whether it is protected or restricted, and which of its arguments and results are secret. A function the flavor lacks is absent, as in the client. The documentation comes as data (decision T199):wowlua-types-genwrites each flavor's as JSON, every key kept, tocrates/wowlua-api/data/<flavor>.api.json.gz. Functions the client has but does not document (UseAction, about 2,000 on WoW Forever, from/stdtest inventory) exist too, with their behaviour or "not mocked". - Natives. The API functions are natives of the VM, as the client's are C
functions (decision T198):
[C]in stack traces, the client's argument errors, no taint of their own. - Behaviour. Functions have hand-written behaviour where tests need it:
the widget system (frames, regions, textures, font strings, scripts, events
and unit events, points and sizes, show and hide, parenting, templates,
mixins, protected frames), the event loop and
OnUpdateframes, timers on a virtual clock, combat lockdown, units, auras, spells and items (from test data), saved variables across reloads, slash commands, keybindings, addon messages, settings, tooltips, menus and dialogs. The M8 test mock is the starting point. - Layout. A region's rectangle comes from its points as the client
lays it out: each edge from the points that set it (a
LEFTand aRIGHTpoint give the width, aTOPand aBOTTOMthe height), otherwise its set size placed by the axis's first point;GetSize,GetWidthandGetHeightgive the laid-out size of an anchored region (unverified beyond fresh widgets;/stdtest enginerecordslayoutSizes). - Not mocked. A generated function without behaviour raises
"
<name>is not mocked" when called, unless the project provides a mock (Tooling §16.1.4), so gaps are visible instead of silently returning nil. - Engine-side Lua. Lua the client defines in its engine (not FrameXML) is provided as secure code.
17.7 Blizzard's UI code
With test.blizzard-ui (Manifest §9.18), the test
runner loads the flavor's FrameXML (Lua and XML from wow-ui-source, at the
client version the API declarations come from) as secure code before the
addon. Tests then run against the real Edit Mode, Settings, action bars, unit
frames and the rest, so taint the addon spreads into Blizzard's code, and
Blizzard code that fails because of what an addon did, show up as in game.
Off by default: mock-only tests start faster (decision T186).
The code comes from wow-ui-source at the flavor's client version and the
global strings from the same dump the declarations use, downloaded on first
use into a user cache keyed by version (decision T204; about 10 seconds and
45 MB per flavor, once: $WOWLUA_HOME/cache/blizzard-ui/, at the commit the
flavor's declarations name; WOWLUA_UI_SOURCE names a local copy instead). The engine's loader (wowlua_engine::ui::load)
takes the flavor's Interface folder and its global strings:
- Startup set (as
/stdtest enginerecorded: the 201 Blizzard addons WoW Forever had loaded when StdTest loaded): every Blizzard addon allowed in the game (AllowLoadgame or both) and for the flavor's game types (its id and family, forAllowLoadGameTypeandExcludeLoadGameType), read from the client's suffixed TOC first (_Mainline.toc); aLoadOnDemandone only as a startup addon's dependency;OnlyBetaAndPTRones on beta and PTR clients (WoW Forever runs from_classic_beta_). - Load order (inferred, unverified):
LoadFirstones first, each after itsDep,Dependencies,RequiredDepandOptionalDepaddons, otherwise by name. File lines follow their inline directives;[Family],[Game]and[TextLocale]name the flavor's folders (Mainline,Camelot,enUS).ADDON_LOADEDfires after each addon. - Global strings come first, from the client's
GlobalStringsdump. - Lua files run as secure code with the addon's name and table.
- XML builds widgets as the client does: virtual elements are templates
(for
inheritsandCreateFrame), intrinsic ones frame types; an element's templates apply first, then its attributes, mixins,parentKeyandparentArray, key values, sizes, anchors, layers, child frames and scripts (function,methodor a body;inherit="prepend"/"append");OnLoadruns once the widget and its children are built. Fonts and font families become font objects. - Gaps. Without game data or a recording, Blizzard's code gets empty
values from functions that have no behaviour: secure code calling such a
function gets its documented results' empty values (nil where nilable,
false, 0, "", a table) and the engine lists the function; addon code still
gets "not mocked". Secure code may register events and set scripts the
engine's lists lack. On camelot (70425) the UI loads, logs in and runs
frames without a Lua error, as the client's does (its probe logged none):
enums, constants and namespaces the documentation lacks come from the
client's recording (
/stdtest engine), secure addons run in the client's secure environment, and Blizzard's errors are caught where ScriptErrors reports them (C_Log.LogErrorMessage). The CVars start at the client's defaults (the 248 the UI code reads that have one, recorded by/stdtest engine). - The client's cases. With camelot's UI code loaded (and the addon's
saved variables loaded as the addon's), all 819 cases
/stdtest proberecorded on WoW Forever 1.60.1 give the client's results, the 22 the VM alone cannot pass included (Blizzard'sstring.rtgsub,getglobal, the string metatableRestrictedExecution.luasets up).
17.8 Verification
- Lua 5.1 conformance. Against PUC Lua 5.1 (embedded only in wowlua's own
tests):
cargo xtask corpus vmcompiles every file of the conformance corpus to the same bytecode and line info asluac, and runs its main chunk with the same output and error.cargo xtask corpus lua51runs the official Lua 5.1 test suite. It excludes only what the client does not have, each with its reason: theio,os,debugandpackagelibraries,string.dump, the implicitargof vararg functions,string.gfind, and a 32-bitsize_toverflow. Shims serve the suite's plumbing only (timing, temporary files in memory, loading its helper files).- Differential tests compare snippets' output and errors.
- Sources of truth. In-game probes on the current client first, then
Blizzard's documentation and UI code for the build (the generated API
documentation's
SecretArguments,ProtectedFunction,HasRestrictions,SecureHooksAllowedandSecretWhen…flags,FrameScriptDocumentation). elune (an open reimplementation of an older client) is only a hint: a rule that rests on it alone is listed as unverified in the coverage report. - Taint. StdTest's taint experiments (
/stdtest taint) taint one value on a path the client's secure code takes to a protected function and record whether the action is blocked; the VM must agree. On WoW Forever 1.60.1 (tests/client/forever-camelot-taint.lua): secure code reading a tainted global (Lua or C function) or table field is tainted, and the protected call raisesADDON_ACTION_FORBIDDENnaming the addon;hooksecurefuncleaves the hooked variable secure and the click allowed;setfenvtaints the function itself (its variable stays secure, the click is forbidden). With camelot's UI code loaded, the engine reproduces all eight: the action button's click reachesUseActionthrough Blizzard's ownSecureTemplates.luaand is forbidden exactly where the client forbade it (wowlua-engine'stests/taint.rs). elune's taint cases (vendored incrates/wowlua-vm/tests/elune/) also pass on the VM, but count only where the experiments confirm them. - The client's behaviour. Every WoW-specific rule (§17.3 to §17.5) has a probe in StdTest whose recorded in-game result is a test fixture; a rule changes only with a new probe.
- In-game regressions. Bugs found in game become emulation tests that must
fail the same way:
os.dateinwowlua:format,0 / 0inwowlua:serialize, SuuUI's reparented status-bar containers breaking Edit Mode, and taint met in combat. - Coverage of the emulation. A report lists which API functions have behaviour, which are generated only, and which rules were probed on which flavor.
17.9 VM design
- Execution (decision T190). The VM compiles the compiler's output to a register bytecode of its own, close to Lua 5.1's design. Each instruction carries its source span (from the source maps), and coverage, taint and profiling attach per instruction.
- Memory (decision T191). A tracing garbage collector over an arena, in
safe Rust, stopping the world at the interpreter's allocation points:
- Cycles are collected and weak tables clear.
__gcruns as in the client: newest first and unprotected, so an error in a finalizer reaches the code that caused the collection.collectgarbagereports memory, andstepcounts work, as Lua 5.1's incremental collector would.
- Taint (decision T193). Exact, as the client tracks it: every stack slot, table field, upvalue and object carries the taint (the addon) of the code that wrote it, and each thread its execution's taint (§17.4).
- Coroutines (decision T192). Stackless: each coroutine is its own VM
stack, and a yield unwinds to its
resumewithout the native stack. As in the client's Lua 5.1, a yield cannot crosspcall, a metamethod or another native call ("attempt to yield across metamethod/C-call boundary"); each thread has its own global table (setfenv(0, t)).