mirror of
https://github.com/brazilofmux/tinymux
synced 2026-08-13 00:23:11 -04:00
Addresses both blocking objections from #1750's adversarial review. Chunk-granularity exactly-once. The review's probe proved the first version's reasoning wrong at the chunk level: statement-form mux.pemit(1,"X") followed by a DECLINING read delivered the message twice -- the effect ran compiled, the later decline failed the run, and the interpreter re-ran the whole chunk. The chunk is the rerun unit, so effectful members (notify, pemit, set, and eval -- arbitrary softcode, doubled identically in the probe) now decline at COMPILE time in every form. Chunks containing them run interpreted, once. Pure members (name, get, type, owner, ...) still compile and pcall the real bridge functions. Cost is nothing real: ECALL-bound shapes bench at parity (#1741). Nested context stomp. Setup/teardown now SAVE/RESTORE the registry's exec-ctx instead of clearing to nil, at all three sites including the interpreter leg -- the review measured the pre-existing clear as a route-dependent divergence under the production default (inner run's teardown nil'd the outer run's context). Harness: the mux.eval pins move to AGREE, asserting the ANSWER while the decline is the required behavior -- with a comment that an eval case starting to execute means the exactly-once argument must be re-made, not waved through. A mux.name EXEC pin proves the pure-member compiled path is real (lua_run_ok advances; equality with the interpreter is the assertion). Decline budget 4 -> 6, both newcomers permanent by design. make test exit 0; smoke 1561/0 (TC013/TC014 Succeeded); luajit 143 chunks 0 wrong; test-config green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1655 lines
48 KiB
C++
1655 lines
48 KiB
C++
/*! \file lua_mod.cpp
|
|
* \brief Lua 5.4 scripting — embedded in engine.so.
|
|
*
|
|
* Embeds a sandboxed Lua 5.4 interpreter. Scripts live as LUA_* attributes
|
|
* on objects. The lua() softcode function dispatches through this module.
|
|
*
|
|
* Bridge functions use engine-internal APIs (externs.h) for permission
|
|
* checks and COM interfaces for cross-layer operations.
|
|
*/
|
|
|
|
#include "copyright.h"
|
|
#include "autoconf.h"
|
|
#include "config.h"
|
|
#include "externs.h"
|
|
#include "libmux.h"
|
|
#include "modules.h"
|
|
#include "lua_mod.h"
|
|
|
|
#include <cstring>
|
|
#include <cstdlib>
|
|
#include <cstdio>
|
|
#include <vector>
|
|
|
|
// Global pointer for bridge functions to reach the module instance.
|
|
// Safe because there is exactly one CLuaMod instance per process.
|
|
//
|
|
static CLuaMod *g_pLuaMod = nullptr;
|
|
|
|
// =========================================================================
|
|
// Per-execution context stored in Lua registry.
|
|
// =========================================================================
|
|
|
|
struct lua_exec_ctx
|
|
{
|
|
dbref executor;
|
|
dbref caller;
|
|
dbref enactor;
|
|
const UTF8 *pArgs[10];
|
|
int nArgs;
|
|
};
|
|
|
|
#define LUA_EXEC_CTX_KEY "mux_exec_ctx"
|
|
#define LUA_MOD_KEY "mux_lua_mod"
|
|
|
|
static lua_exec_ctx *get_exec_ctx(lua_State *L)
|
|
{
|
|
lua_getfield(L, LUA_REGISTRYINDEX, LUA_EXEC_CTX_KEY);
|
|
lua_exec_ctx *ctx = static_cast<lua_exec_ctx *>(lua_touserdata(L, -1));
|
|
lua_pop(L, 1);
|
|
return ctx;
|
|
}
|
|
|
|
static CLuaMod *get_lua_mod(lua_State *L)
|
|
{
|
|
(void)L;
|
|
return g_pLuaMod;
|
|
}
|
|
|
|
// =========================================================================
|
|
// mux.* bridge functions (Lua C functions)
|
|
// =========================================================================
|
|
|
|
// mux.notify(dbref, message) — send text to a player.
|
|
//
|
|
// Permission model matches @pemit: executor must be nearby, have
|
|
// Long_Fingers, or control the target. If pemit_players is on and
|
|
// the target is a connected player, page-lock is checked. If
|
|
// pemit_any is on, any @pemit to a player is allowed.
|
|
//
|
|
static int bridge_notify(lua_State *L)
|
|
{
|
|
CLuaMod *mod = get_lua_mod(L);
|
|
if (nullptr == mod) return 0;
|
|
|
|
lua_exec_ctx *ctx = get_exec_ctx(L);
|
|
if (nullptr == ctx) return 0;
|
|
|
|
dbref target = static_cast<dbref>(luaL_checkinteger(L, 1));
|
|
const char *msg = luaL_checkstring(L, 2);
|
|
dbref executor = ctx->executor;
|
|
|
|
if (!Good_obj(target))
|
|
{
|
|
lua_pushboolean(L, 0);
|
|
return 1;
|
|
}
|
|
|
|
// Enforce locality constraints (matches do_pemit_single).
|
|
//
|
|
bool ok = nearby(executor, target)
|
|
|| Long_Fingers(executor)
|
|
|| Controls(executor, target);
|
|
|
|
if ( !ok
|
|
&& isPlayer(target)
|
|
&& mudconf.pemit_players)
|
|
{
|
|
// Check page-lock without side effects.
|
|
//
|
|
ok = Connected(target)
|
|
&& could_doit(executor, target, A_LPAGE)
|
|
&& could_doit(target, executor, A_LPAGE);
|
|
}
|
|
|
|
if (!ok && mudconf.pemit_any)
|
|
{
|
|
ok = true;
|
|
}
|
|
|
|
if (!ok)
|
|
{
|
|
lua_pushboolean(L, 0);
|
|
return 1;
|
|
}
|
|
|
|
mux_INotify *pNotify = nullptr;
|
|
MUX_RESULT mr = mux_CreateInstance(CID_Notify, nullptr, UseSameProcess,
|
|
IID_INotify, reinterpret_cast<void **>(&pNotify));
|
|
if (MUX_SUCCEEDED(mr) && nullptr != pNotify)
|
|
{
|
|
pNotify->Notify(target,
|
|
reinterpret_cast<const UTF8 *>(msg));
|
|
pNotify->Release();
|
|
}
|
|
lua_pushboolean(L, 1);
|
|
return 1;
|
|
}
|
|
|
|
// mux.name(dbref) — return object name.
|
|
//
|
|
// Permission model matches name(): if read_rem_name is off, requires
|
|
// nearby_or_control, isPlayer, or Long_Fingers.
|
|
//
|
|
static int bridge_name(lua_State *L)
|
|
{
|
|
lua_exec_ctx *ctx = get_exec_ctx(L);
|
|
if (nullptr == ctx) { lua_pushnil(L); return 1; }
|
|
|
|
dbref obj = static_cast<dbref>(luaL_checkinteger(L, 1));
|
|
|
|
if (!Good_obj(obj))
|
|
{
|
|
lua_pushnil(L);
|
|
return 1;
|
|
}
|
|
|
|
if (!mudconf.read_rem_name)
|
|
{
|
|
if ( !Controls(ctx->executor, obj)
|
|
&& !nearby(ctx->executor, obj)
|
|
&& !isPlayer(obj)
|
|
&& !Long_Fingers(ctx->executor))
|
|
{
|
|
lua_pushnil(L);
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
const UTF8 *pName = Name(obj);
|
|
if (nullptr != pName)
|
|
{
|
|
// For exits, return only the name before the first semicolon.
|
|
//
|
|
if (isExit(obj))
|
|
{
|
|
const char *semi = strchr(reinterpret_cast<const char *>(pName), ';');
|
|
if (semi)
|
|
{
|
|
lua_pushlstring(L, reinterpret_cast<const char *>(pName),
|
|
semi - reinterpret_cast<const char *>(pName));
|
|
return 1;
|
|
}
|
|
}
|
|
lua_pushstring(L, reinterpret_cast<const char *>(pName));
|
|
return 1;
|
|
}
|
|
lua_pushnil(L);
|
|
return 1;
|
|
}
|
|
|
|
// mux.owner(dbref) — return owner dbref.
|
|
// Softcode owner() uses match_thing + Owner(); absolute #dbref is allowed
|
|
// there too. Require Good_obj only (same as softcode after a successful match).
|
|
//
|
|
static int bridge_owner(lua_State *L)
|
|
{
|
|
dbref obj = static_cast<dbref>(luaL_checkinteger(L, 1));
|
|
if (!Good_obj(obj))
|
|
{
|
|
lua_pushnil(L);
|
|
return 1;
|
|
}
|
|
|
|
mux_IObjectInfo *pOI = nullptr;
|
|
MUX_RESULT mr = mux_CreateInstance(CID_ObjectInfo, nullptr,
|
|
UseSameProcess, IID_IObjectInfo,
|
|
reinterpret_cast<void **>(&pOI));
|
|
if (MUX_SUCCEEDED(mr) && nullptr != pOI)
|
|
{
|
|
dbref owner;
|
|
mr = pOI->GetOwner(obj, &owner);
|
|
pOI->Release();
|
|
if (MUX_SUCCEEDED(mr))
|
|
{
|
|
lua_pushinteger(L, owner);
|
|
return 1;
|
|
}
|
|
}
|
|
lua_pushnil(L);
|
|
return 1;
|
|
}
|
|
|
|
// mux.location(dbref) — return location dbref.
|
|
//
|
|
// Permission model matches loc(): requires locatable(executor, obj,
|
|
// enactor), which respects UNFINDABLE, nearby, see_all.
|
|
//
|
|
static int bridge_location(lua_State *L)
|
|
{
|
|
lua_exec_ctx *ctx = get_exec_ctx(L);
|
|
if (nullptr == ctx) { lua_pushnil(L); return 1; }
|
|
|
|
dbref obj = static_cast<dbref>(luaL_checkinteger(L, 1));
|
|
|
|
if (!Good_obj(obj))
|
|
{
|
|
lua_pushnil(L);
|
|
return 1;
|
|
}
|
|
|
|
if (!locatable(ctx->executor, obj, ctx->enactor))
|
|
{
|
|
lua_pushnil(L);
|
|
return 1;
|
|
}
|
|
|
|
lua_pushinteger(L, Location(obj));
|
|
return 1;
|
|
}
|
|
|
|
// mux.get(dbref, attrname) — read an attribute value.
|
|
//
|
|
static int bridge_get(lua_State *L)
|
|
{
|
|
lua_exec_ctx *ctx = get_exec_ctx(L);
|
|
if (nullptr == ctx) { lua_pushnil(L); return 1; }
|
|
|
|
int obj = static_cast<int>(luaL_checkinteger(L, 1));
|
|
const char *attrname = luaL_checkstring(L, 2);
|
|
|
|
mux_IAttributeAccess *pAA = nullptr;
|
|
MUX_RESULT mr = mux_CreateInstance(CID_AttributeAccess, nullptr,
|
|
UseSameProcess, IID_IAttributeAccess,
|
|
reinterpret_cast<void **>(&pAA));
|
|
if (MUX_SUCCEEDED(mr) && nullptr != pAA)
|
|
{
|
|
UTF8 value[8000];
|
|
size_t nValue = 0;
|
|
mr = pAA->GetAttribute(ctx->executor, static_cast<dbref>(obj),
|
|
reinterpret_cast<const UTF8 *>(attrname),
|
|
value, sizeof(value), &nValue);
|
|
pAA->Release();
|
|
if (MUX_SUCCEEDED(mr))
|
|
{
|
|
lua_pushlstring(L, reinterpret_cast<const char *>(value), nValue);
|
|
return 1;
|
|
}
|
|
}
|
|
lua_pushnil(L);
|
|
return 1;
|
|
}
|
|
|
|
// mux.set(dbref, attrname, value) — write an attribute value.
|
|
//
|
|
static int bridge_set(lua_State *L)
|
|
{
|
|
lua_exec_ctx *ctx = get_exec_ctx(L);
|
|
if (nullptr == ctx) return luaL_error(L, "no execution context");
|
|
|
|
int obj = static_cast<int>(luaL_checkinteger(L, 1));
|
|
const char *attrname = luaL_checkstring(L, 2);
|
|
const char *value = luaL_checkstring(L, 3);
|
|
|
|
mux_IAttributeAccess *pAA = nullptr;
|
|
MUX_RESULT mr = mux_CreateInstance(CID_AttributeAccess, nullptr,
|
|
UseSameProcess, IID_IAttributeAccess,
|
|
reinterpret_cast<void **>(&pAA));
|
|
if (MUX_SUCCEEDED(mr) && nullptr != pAA)
|
|
{
|
|
mr = pAA->SetAttribute(ctx->executor, static_cast<dbref>(obj),
|
|
reinterpret_cast<const UTF8 *>(attrname),
|
|
reinterpret_cast<const UTF8 *>(value));
|
|
pAA->Release();
|
|
if (MUX_FAILED(mr))
|
|
{
|
|
return luaL_error(L, "permission denied");
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// mux.eval(expression) — evaluate softcode expression.
|
|
//
|
|
static int bridge_eval(lua_State *L)
|
|
{
|
|
lua_exec_ctx *ctx = get_exec_ctx(L);
|
|
if (nullptr == ctx) { lua_pushnil(L); return 1; }
|
|
|
|
const char *expr = luaL_checkstring(L, 1);
|
|
|
|
mux_IEvaluator *pEval = nullptr;
|
|
MUX_RESULT mr = mux_CreateInstance(CID_Evaluator, nullptr,
|
|
UseSameProcess, IID_IEvaluator,
|
|
reinterpret_cast<void **>(&pEval));
|
|
if (MUX_SUCCEEDED(mr) && nullptr != pEval)
|
|
{
|
|
UTF8 result[8000];
|
|
size_t nResult = 0;
|
|
mr = pEval->Eval(ctx->executor, ctx->caller, ctx->enactor,
|
|
reinterpret_cast<const UTF8 *>(expr),
|
|
result, sizeof(result), &nResult);
|
|
pEval->Release();
|
|
if (MUX_SUCCEEDED(mr))
|
|
{
|
|
lua_pushlstring(L, reinterpret_cast<const char *>(result),
|
|
nResult);
|
|
return 1;
|
|
}
|
|
}
|
|
lua_pushnil(L);
|
|
return 1;
|
|
}
|
|
|
|
// mux.type(dbref) — return object type string.
|
|
//
|
|
static int bridge_type(lua_State *L)
|
|
{
|
|
int obj = static_cast<int>(luaL_checkinteger(L, 1));
|
|
|
|
mux_IObjectInfo *pOI = nullptr;
|
|
MUX_RESULT mr = mux_CreateInstance(CID_ObjectInfo, nullptr,
|
|
UseSameProcess, IID_IObjectInfo,
|
|
reinterpret_cast<void **>(&pOI));
|
|
if (MUX_SUCCEEDED(mr) && nullptr != pOI)
|
|
{
|
|
int type = -1;
|
|
mr = pOI->GetType(static_cast<dbref>(obj), &type);
|
|
pOI->Release();
|
|
if (MUX_SUCCEEDED(mr))
|
|
{
|
|
// Type constants: 0=ROOM, 1=THING, 2=EXIT, 3=PLAYER
|
|
static const char *types[] = {"ROOM", "THING", "EXIT", "PLAYER"};
|
|
if (type >= 0 && type <= 3)
|
|
{
|
|
lua_pushstring(L, types[type]);
|
|
}
|
|
else
|
|
{
|
|
lua_pushstring(L, "UNKNOWN");
|
|
}
|
|
return 1;
|
|
}
|
|
}
|
|
lua_pushnil(L);
|
|
return 1;
|
|
}
|
|
|
|
// mux.flags(dbref) — return flag string.
|
|
//
|
|
static int bridge_flags(lua_State *L)
|
|
{
|
|
lua_exec_ctx *ctx = get_exec_ctx(L);
|
|
int obj = static_cast<int>(luaL_checkinteger(L, 1));
|
|
|
|
mux_IObjectInfo *pOI = nullptr;
|
|
MUX_RESULT mr = mux_CreateInstance(CID_ObjectInfo, nullptr,
|
|
UseSameProcess, IID_IObjectInfo,
|
|
reinterpret_cast<void **>(&pOI));
|
|
if (MUX_SUCCEEDED(mr) && nullptr != pOI)
|
|
{
|
|
UTF8 *pFlags = nullptr;
|
|
dbref looker = (ctx != nullptr) ? ctx->executor : static_cast<dbref>(1);
|
|
mr = pOI->DecodeFlags(looker, static_cast<dbref>(obj), &pFlags);
|
|
pOI->Release();
|
|
if (MUX_SUCCEEDED(mr) && nullptr != pFlags)
|
|
{
|
|
lua_pushstring(L, reinterpret_cast<const char *>(pFlags));
|
|
return 1;
|
|
}
|
|
}
|
|
lua_pushnil(L);
|
|
return 1;
|
|
}
|
|
|
|
// mux.isplayer(dbref) — check if object is a player.
|
|
//
|
|
static int bridge_isplayer(lua_State *L)
|
|
{
|
|
int obj = static_cast<int>(luaL_checkinteger(L, 1));
|
|
|
|
mux_IObjectInfo *pOI = nullptr;
|
|
MUX_RESULT mr = mux_CreateInstance(CID_ObjectInfo, nullptr,
|
|
UseSameProcess, IID_IObjectInfo,
|
|
reinterpret_cast<void **>(&pOI));
|
|
if (MUX_SUCCEEDED(mr) && nullptr != pOI)
|
|
{
|
|
bool bPlayer = false;
|
|
mr = pOI->IsPlayer(static_cast<dbref>(obj), &bPlayer);
|
|
pOI->Release();
|
|
if (MUX_SUCCEEDED(mr))
|
|
{
|
|
lua_pushboolean(L, bPlayer ? 1 : 0);
|
|
return 1;
|
|
}
|
|
}
|
|
lua_pushboolean(L, 0);
|
|
return 1;
|
|
}
|
|
|
|
// mux.isconnected(dbref) — check if player is connected.
|
|
// Match softcode hasflag(obj,CONNECTED): pub_flags || Examinable || self (#1287).
|
|
//
|
|
static int bridge_isconnected(lua_State *L)
|
|
{
|
|
lua_exec_ctx *ctx = get_exec_ctx(L);
|
|
if (nullptr == ctx) { lua_pushboolean(L, 0); return 1; }
|
|
|
|
dbref obj = static_cast<dbref>(luaL_checkinteger(L, 1));
|
|
if ( !Good_obj(obj)
|
|
|| ( !mudconf.pub_flags
|
|
&& !Examinable(ctx->executor, obj)
|
|
&& obj != ctx->executor
|
|
&& obj != ctx->enactor))
|
|
{
|
|
lua_pushboolean(L, 0);
|
|
return 1;
|
|
}
|
|
|
|
mux_IObjectInfo *pOI = nullptr;
|
|
MUX_RESULT mr = mux_CreateInstance(CID_ObjectInfo, nullptr,
|
|
UseSameProcess, IID_IObjectInfo,
|
|
reinterpret_cast<void **>(&pOI));
|
|
if (MUX_SUCCEEDED(mr) && nullptr != pOI)
|
|
{
|
|
bool bConn = false;
|
|
mr = pOI->IsConnected(obj, &bConn);
|
|
pOI->Release();
|
|
if (MUX_SUCCEEDED(mr))
|
|
{
|
|
lua_pushboolean(L, bConn ? 1 : 0);
|
|
return 1;
|
|
}
|
|
}
|
|
lua_pushboolean(L, 0);
|
|
return 1;
|
|
}
|
|
|
|
// mux.pennies(dbref) — return object pennies.
|
|
// Match softcode money(): Examinable required (#1287). Absolute dbref
|
|
// without Examinable used to return balance for any object.
|
|
//
|
|
static int bridge_pennies(lua_State *L)
|
|
{
|
|
lua_exec_ctx *ctx = get_exec_ctx(L);
|
|
if (nullptr == ctx) { lua_pushnil(L); return 1; }
|
|
|
|
dbref obj = static_cast<dbref>(luaL_checkinteger(L, 1));
|
|
if (!Good_obj(obj) || !Examinable(ctx->executor, obj))
|
|
{
|
|
lua_pushnil(L);
|
|
return 1;
|
|
}
|
|
|
|
mux_IObjectInfo *pOI = nullptr;
|
|
MUX_RESULT mr = mux_CreateInstance(CID_ObjectInfo, nullptr,
|
|
UseSameProcess, IID_IObjectInfo,
|
|
reinterpret_cast<void **>(&pOI));
|
|
if (MUX_SUCCEEDED(mr) && nullptr != pOI)
|
|
{
|
|
int pennies = 0;
|
|
mr = pOI->GetPennies(obj, &pennies);
|
|
pOI->Release();
|
|
if (MUX_SUCCEEDED(mr))
|
|
{
|
|
lua_pushinteger(L, pennies);
|
|
return 1;
|
|
}
|
|
}
|
|
lua_pushnil(L);
|
|
return 1;
|
|
}
|
|
|
|
// mux.iswizard(dbref) — check if object is a wizard.
|
|
// Match softcode hasflag() object form: pub_flags || Examinable || self (#1287).
|
|
//
|
|
static int bridge_iswizard(lua_State *L)
|
|
{
|
|
lua_exec_ctx *ctx = get_exec_ctx(L);
|
|
if (nullptr == ctx) { lua_pushboolean(L, 0); return 1; }
|
|
|
|
dbref obj = static_cast<dbref>(luaL_checkinteger(L, 1));
|
|
if ( !Good_obj(obj)
|
|
|| ( !mudconf.pub_flags
|
|
&& !Examinable(ctx->executor, obj)
|
|
&& obj != ctx->executor
|
|
&& obj != ctx->enactor))
|
|
{
|
|
lua_pushboolean(L, 0);
|
|
return 1;
|
|
}
|
|
|
|
mux_IPermissions *pPerms = nullptr;
|
|
MUX_RESULT mr = mux_CreateInstance(CID_Permissions, nullptr,
|
|
UseSameProcess, IID_IPermissions,
|
|
reinterpret_cast<void **>(&pPerms));
|
|
if (MUX_SUCCEEDED(mr) && nullptr != pPerms)
|
|
{
|
|
bool bWizard = false;
|
|
mr = pPerms->IsWizard(obj, &bWizard);
|
|
pPerms->Release();
|
|
if (MUX_SUCCEEDED(mr))
|
|
{
|
|
lua_pushboolean(L, bWizard ? 1 : 0);
|
|
return 1;
|
|
}
|
|
}
|
|
lua_pushboolean(L, 0);
|
|
return 1;
|
|
}
|
|
|
|
// mux.controls(who, what) — check if who controls what.
|
|
//
|
|
// Permission model: executor can only query control relationships
|
|
// where 'who' is themselves or an object they control. This
|
|
// prevents scripts from probing wizard control relationships.
|
|
//
|
|
static int bridge_controls(lua_State *L)
|
|
{
|
|
lua_exec_ctx *ctx = get_exec_ctx(L);
|
|
if (nullptr == ctx) { lua_pushboolean(L, 0); return 1; }
|
|
|
|
dbref who = static_cast<dbref>(luaL_checkinteger(L, 1));
|
|
dbref what = static_cast<dbref>(luaL_checkinteger(L, 2));
|
|
|
|
if ( !Good_obj(who)
|
|
|| !Good_obj(what))
|
|
{
|
|
lua_pushboolean(L, 0);
|
|
return 1;
|
|
}
|
|
|
|
// Restrict: executor must be 'who' or must control 'who'.
|
|
//
|
|
if ( who != ctx->executor
|
|
&& !Controls(ctx->executor, who))
|
|
{
|
|
lua_pushboolean(L, 0);
|
|
return 1;
|
|
}
|
|
|
|
lua_pushboolean(L, Controls(who, what) ? 1 : 0);
|
|
return 1;
|
|
}
|
|
|
|
// Bridge function table.
|
|
//
|
|
static const luaL_Reg bridge_funcs[] = {
|
|
{"notify", bridge_notify},
|
|
{"pemit", bridge_notify},
|
|
{"name", bridge_name},
|
|
{"owner", bridge_owner},
|
|
{"location", bridge_location},
|
|
{"type", bridge_type},
|
|
{"flags", bridge_flags},
|
|
{"isplayer", bridge_isplayer},
|
|
{"isconnected", bridge_isconnected},
|
|
{"pennies", bridge_pennies},
|
|
{"get", bridge_get},
|
|
{"set", bridge_set},
|
|
{"eval", bridge_eval},
|
|
{"iswizard", bridge_iswizard},
|
|
{"controls", bridge_controls},
|
|
{nullptr, nullptr}
|
|
};
|
|
|
|
// =========================================================================
|
|
// Custom memory allocator with limit enforcement.
|
|
// =========================================================================
|
|
|
|
void *CLuaMod::LuaAlloc(void *ud, void *ptr, size_t osize, size_t nsize)
|
|
{
|
|
CLuaMod *self = static_cast<CLuaMod *>(ud);
|
|
|
|
if (nsize == 0)
|
|
{
|
|
// Free.
|
|
if (ptr != nullptr)
|
|
{
|
|
self->m_nMemUsed -= osize;
|
|
free(ptr);
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
// Check memory limit.
|
|
//
|
|
size_t delta = nsize - (ptr ? osize : 0);
|
|
if (self->m_nMemUsed + delta > static_cast<size_t>(self->m_nMemLimit))
|
|
{
|
|
self->m_bMemExceeded = true;
|
|
return nullptr; // Allocation denied — triggers Lua OOM error.
|
|
}
|
|
|
|
void *newptr = realloc(ptr, nsize);
|
|
if (newptr != nullptr)
|
|
{
|
|
self->m_nMemUsed += delta;
|
|
if (self->m_nMemUsed > self->m_nMemPeak)
|
|
{
|
|
self->m_nMemPeak = self->m_nMemUsed;
|
|
}
|
|
}
|
|
return newptr;
|
|
}
|
|
|
|
// =========================================================================
|
|
// Instruction count hook — enforces execution limits.
|
|
// =========================================================================
|
|
|
|
// Fires every m_nInsnPoll VM instructions (#1591).
|
|
//
|
|
// The Lua module used to bound a chunk only by its own instruction and memory
|
|
// limits, neither of which is what the rest of the server bounds on: softcode
|
|
// bounds on wall time. alarm_clock is armed per command
|
|
// (alarm_clock.set(mudconf.max_cmdsecs) in cque.cpp), the AST evaluator polls
|
|
// it and answers "#-1 CPU LIMITED", and the JIT is handed the same flag as
|
|
// dbt->alarm_flag. Lua referenced it nowhere, so lua() ignored max_cmdsecs
|
|
// entirely.
|
|
//
|
|
// Shape borrowed from the JIT's guest-loop budget (#1571): count cheaply, do
|
|
// the real check periodically. The hook fires on a poll interval rather than
|
|
// once at the limit, checks the alarm, and only then accounts instructions --
|
|
// so the instruction limit keeps its previous meaning while wall time becomes
|
|
// the bound that agrees with everything else.
|
|
//
|
|
// This does NOT bound time spent inside a C function; the hook cannot fire
|
|
// there. The pattern-matcher half of #1591 is MatchInterrupt below, installed
|
|
// into lstrlib.c for the same pcall window.
|
|
//
|
|
// Wall-clock escape for the Lua pattern matcher (#1591).
|
|
//
|
|
// InsnCountHook below cannot cover this: lua_sethook(LUA_MASKCOUNT) counts VM
|
|
// instructions and does not fire inside a C function, and a pathological
|
|
// pattern spends all its time inside one call to string.find. lstrlib.c's
|
|
// own MAXCCALLS bounds recursion depth, which stops a stack overflow but not
|
|
// exponential backtracking -- that grows in breadth, so depth stays under 200
|
|
// while the matcher runs unbounded.
|
|
//
|
|
// lstrlib.c calls this every MATCH_INTERRUPT_MASK+1 match() steps when the
|
|
// pointer is installed. Returning non-zero raises "cpu limited" there, which
|
|
// lands in the same m_bCpuLimited path as the instruction hook, so the caller
|
|
// still sees "#-1 CPU LIMITED" -- one budget, one message, all four routes.
|
|
//
|
|
extern "C" int (*lua_match_interrupt)(lua_State *L);
|
|
|
|
int CLuaMod::MatchInterrupt(lua_State *L)
|
|
{
|
|
if (!alarm_clock.alarmed)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
// Same instance recovery as InsnCountHook. Flagging the module is what
|
|
// turns the Lua error into the softcode answer; without it the player
|
|
// would get a raw "#-1 LUA ERROR: cpu limited" instead.
|
|
void *ud = nullptr;
|
|
lua_getallocf(L, &ud);
|
|
CLuaMod *self = static_cast<CLuaMod *>(ud);
|
|
if (nullptr != self)
|
|
{
|
|
self->m_bCpuLimited = true;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
void CLuaMod::InsnCountHook(lua_State *L, lua_Debug *ar)
|
|
{
|
|
(void)ar;
|
|
|
|
// The allocator ud is the module instance (lua_newstate(LuaAlloc, this)).
|
|
void *ud = nullptr;
|
|
lua_getallocf(L, &ud);
|
|
CLuaMod *self = static_cast<CLuaMod *>(ud);
|
|
|
|
if (nullptr != self)
|
|
{
|
|
if (alarm_clock.alarmed)
|
|
{
|
|
self->m_bCpuLimited = true;
|
|
luaL_error(L, "cpu limited");
|
|
}
|
|
|
|
self->m_nInsnUsed += self->m_nInsnPoll;
|
|
if (self->m_nInsnUsed < self->m_nInsnLimit)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
luaL_error(L, "instruction limit exceeded");
|
|
}
|
|
|
|
// =========================================================================
|
|
// Lua state creation and sandbox setup.
|
|
// =========================================================================
|
|
|
|
bool CLuaMod::CreateLuaState(void)
|
|
{
|
|
m_nMemUsed = 0;
|
|
m_nMemPeak = 0;
|
|
m_bMemExceeded = false;
|
|
|
|
m_L = lua_newstate(LuaAlloc, this);
|
|
if (nullptr == m_L)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Open whitelisted libraries.
|
|
//
|
|
luaL_requiref(m_L, "_G", luaopen_base, 1);
|
|
lua_pop(m_L, 1);
|
|
luaL_requiref(m_L, "string", luaopen_string, 1);
|
|
// string.dump reifies bytecode and is an escape hatch for reconstructing
|
|
// blocked loaders; softcode has no equivalent. (#1287)
|
|
lua_getglobal(m_L, "string");
|
|
if (lua_istable(m_L, -1))
|
|
{
|
|
lua_pushnil(m_L);
|
|
lua_setfield(m_L, -2, "dump");
|
|
}
|
|
lua_pop(m_L, 1);
|
|
luaL_requiref(m_L, "table", luaopen_table, 1);
|
|
lua_pop(m_L, 1);
|
|
luaL_requiref(m_L, "math", luaopen_math, 1);
|
|
lua_pop(m_L, 1);
|
|
luaL_requiref(m_L, "utf8", luaopen_utf8, 1);
|
|
lua_pop(m_L, 1);
|
|
luaL_requiref(m_L, "coroutine", luaopen_coroutine, 1);
|
|
lua_pop(m_L, 1);
|
|
|
|
// Remove dangerous functions from the global table.
|
|
//
|
|
// string.dump is removed via the string library patch below if present;
|
|
// base-library escapes that rebuild loaders are nilled here.
|
|
static const char *blocked[] = {
|
|
"load", "loadfile", "dofile", "require",
|
|
"rawget", "rawset", "rawequal", "rawlen",
|
|
"collectgarbage", nullptr
|
|
};
|
|
for (int i = 0; blocked[i] != nullptr; i++)
|
|
{
|
|
lua_pushnil(m_L);
|
|
lua_setglobal(m_L, blocked[i]);
|
|
}
|
|
|
|
// Remap print to do nothing (scripts should use mux.notify).
|
|
//
|
|
lua_pushcfunction(m_L, [](lua_State *) -> int { return 0; });
|
|
lua_setglobal(m_L, "print");
|
|
|
|
// Register mux.* bridge table.
|
|
//
|
|
luaL_newlib(m_L, bridge_funcs);
|
|
lua_setglobal(m_L, "mux");
|
|
|
|
// Store module pointer in registry for bridge function access.
|
|
//
|
|
lua_pushlightuserdata(m_L, this);
|
|
lua_setfield(m_L, LUA_REGISTRYINDEX, LUA_MOD_KEY);
|
|
|
|
return true;
|
|
}
|
|
|
|
void CLuaMod::DestroyLuaState(void)
|
|
{
|
|
if (nullptr != m_L)
|
|
{
|
|
lua_close(m_L);
|
|
m_L = nullptr;
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// Chunk execution with sandbox.
|
|
// =========================================================================
|
|
|
|
// Execution-context setup shared by the interpreter and compiled routes.
|
|
//
|
|
// The bridge C functions (mux.eval, mux.name, ...) read the executor and
|
|
// friends from the registry, and per-run values (mux.executor, mux.args)
|
|
// are injected into the global mux table. This used to live inline in
|
|
// ExecuteChunk only -- the interpreter leg -- so a bridge function invoked
|
|
// from a COMPILED run found a cleared registry and stale mux fields. Now
|
|
// that compiled chunks call the real bridge functions through the ordinary
|
|
// Lua call path (#1745 follow-up), both routes must stage the same context.
|
|
//
|
|
// ctx is caller-owned: the registry holds a lightuserdata pointing at it,
|
|
// so it must outlive the run. Returns the PREVIOUS registry value so the
|
|
// teardown can restore rather than clear: nested runs (softcode -> lua
|
|
// under brackets can re-enter) used to stomp the outer run's context to
|
|
// nil, which #1750's adversarial review measured as a route-dependent
|
|
// divergence. Restore with lua_restore_exec_context.
|
|
//
|
|
static lua_exec_ctx *lua_setup_exec_context(lua_State *L, lua_exec_ctx &ctx,
|
|
dbref executor, dbref caller, dbref enactor,
|
|
const UTF8 *pArgs[], int nArgs)
|
|
{
|
|
lua_getfield(L, LUA_REGISTRYINDEX, LUA_EXEC_CTX_KEY);
|
|
lua_exec_ctx *prev =
|
|
static_cast<lua_exec_ctx *>(lua_touserdata(L, -1));
|
|
lua_pop(L, 1);
|
|
|
|
ctx.executor = executor;
|
|
ctx.caller = caller;
|
|
ctx.enactor = enactor;
|
|
ctx.nArgs = (nArgs > 10) ? 10 : nArgs;
|
|
for (int i = 0; i < ctx.nArgs; i++)
|
|
{
|
|
ctx.pArgs[i] = (pArgs != nullptr) ? pArgs[i] : nullptr;
|
|
}
|
|
lua_pushlightuserdata(L, &ctx);
|
|
lua_setfield(L, LUA_REGISTRYINDEX, LUA_EXEC_CTX_KEY);
|
|
|
|
// Inject mux.executor, mux.caller, mux.enactor, mux.args into
|
|
// the global mux table.
|
|
//
|
|
lua_getglobal(L, "mux");
|
|
lua_pushinteger(L, executor);
|
|
lua_setfield(L, -2, "executor");
|
|
lua_pushinteger(L, caller);
|
|
lua_setfield(L, -2, "caller");
|
|
lua_pushinteger(L, enactor);
|
|
lua_setfield(L, -2, "enactor");
|
|
|
|
// Build mux.args table.
|
|
//
|
|
int nSafe = (nArgs > 0 && pArgs != nullptr) ? nArgs : 0;
|
|
lua_createtable(L, nSafe, 0);
|
|
for (int i = 0; i < nSafe; i++)
|
|
{
|
|
if (pArgs[i] != nullptr)
|
|
{
|
|
lua_pushstring(L, reinterpret_cast<const char *>(pArgs[i]));
|
|
}
|
|
else
|
|
{
|
|
lua_pushstring(L, "");
|
|
}
|
|
lua_rawseti(L, -2, i + 1);
|
|
}
|
|
lua_setfield(L, -2, "args");
|
|
lua_pop(L, 1); // pop mux table
|
|
|
|
return prev;
|
|
}
|
|
|
|
static void lua_restore_exec_context(lua_State *L, lua_exec_ctx *prev)
|
|
{
|
|
if (prev != nullptr)
|
|
{
|
|
lua_pushlightuserdata(L, prev);
|
|
}
|
|
else
|
|
{
|
|
lua_pushnil(L);
|
|
}
|
|
lua_setfield(L, LUA_REGISTRYINDEX, LUA_EXEC_CTX_KEY);
|
|
}
|
|
|
|
bool CLuaMod::ExecuteChunk(lua_State *L, dbref executor, dbref caller,
|
|
dbref enactor, const UTF8 *pArgs[], int nArgs,
|
|
UTF8 *pResult, size_t nResultMax, size_t *pnResultLen)
|
|
{
|
|
// The compiled chunk is on top of the Lua stack. Set up the execution
|
|
// context and call it.
|
|
|
|
lua_exec_ctx ctx;
|
|
lua_exec_ctx *prev_ctx =
|
|
lua_setup_exec_context(L, ctx, executor, caller, enactor,
|
|
pArgs, nArgs);
|
|
|
|
// Set instruction count hook.
|
|
//
|
|
m_bMemExceeded = false;
|
|
m_bCpuLimited = false;
|
|
|
|
// Poll often enough to notice the alarm, but never less often than the
|
|
// instruction limit itself -- a small configured limit must still fire
|
|
// where it always did.
|
|
m_nInsnPoll = (m_nInsnLimit < LUA_ALARM_POLL_INSNS)
|
|
? m_nInsnLimit : LUA_ALARM_POLL_INSNS;
|
|
if (m_nInsnPoll < 1)
|
|
{
|
|
m_nInsnPoll = 1;
|
|
}
|
|
m_nInsnUsed = 0;
|
|
lua_sethook(L, InsnCountHook, LUA_MASKCOUNT, m_nInsnPoll);
|
|
|
|
// Cover the C-function gap the count hook cannot reach (#1591). Set on
|
|
// every call rather than once at startup: the pointer lives in lstrlib.c
|
|
// and costs nothing to reassign, and this way it cannot be left dangling
|
|
// by a module unload.
|
|
lua_match_interrupt = CLuaMod::MatchInterrupt;
|
|
|
|
// Call the chunk (it's below the mux table stuff we just popped).
|
|
//
|
|
int status = lua_pcall(L, 0, 1, 0);
|
|
|
|
// Remove the hook.
|
|
//
|
|
lua_sethook(L, nullptr, 0, 0);
|
|
lua_match_interrupt = nullptr;
|
|
|
|
lua_restore_exec_context(L, prev_ctx);
|
|
|
|
if (status != LUA_OK)
|
|
{
|
|
// Error.
|
|
const char *errmsg = lua_tostring(L, -1);
|
|
if (nullptr == errmsg) errmsg = "unknown error";
|
|
|
|
if (m_bCpuLimited)
|
|
{
|
|
// Answer exactly as the AST evaluator and the JIT do, rather than
|
|
// wrapping it as a Lua error: one budget, one message (#1591).
|
|
m_stats.cpu_limit_hits++;
|
|
m_stats.errors++;
|
|
const UTF8 *kMsg = S_("#-1 CPU LIMITED");
|
|
size_t n = strlen(reinterpret_cast<const char *>(kMsg));
|
|
if (n >= nResultMax)
|
|
{
|
|
n = nResultMax - 1;
|
|
}
|
|
memcpy(pResult, kMsg, n);
|
|
pResult[n] = '\0';
|
|
*pnResultLen = n;
|
|
lua_pop(L, 1);
|
|
return MUX_S_OK;
|
|
}
|
|
|
|
if (m_bMemExceeded)
|
|
{
|
|
m_stats.mem_limit_hits++;
|
|
errmsg = "memory limit exceeded";
|
|
}
|
|
else if (strstr(errmsg, "instruction limit") != nullptr)
|
|
{
|
|
m_stats.insn_limit_hits++;
|
|
}
|
|
|
|
m_stats.errors++;
|
|
|
|
size_t n = mux_snprintf(pResult, nResultMax,
|
|
T("#-1 LUA ERROR: %s"), errmsg);
|
|
*pnResultLen = n;
|
|
|
|
lua_pop(L, 1);
|
|
return false;
|
|
}
|
|
|
|
// Success — convert return value to string.
|
|
//
|
|
size_t len = 0;
|
|
const char *result = nullptr;
|
|
|
|
if (lua_isnil(L, -1) || lua_isnone(L, -1))
|
|
{
|
|
result = "";
|
|
len = 0;
|
|
}
|
|
else if (lua_isboolean(L, -1))
|
|
{
|
|
result = lua_toboolean(L, -1) ? "1" : "0";
|
|
len = 1;
|
|
}
|
|
else
|
|
{
|
|
result = lua_tolstring(L, -1, &len);
|
|
if (nullptr == result)
|
|
{
|
|
result = "";
|
|
len = 0;
|
|
}
|
|
}
|
|
|
|
if (len >= nResultMax)
|
|
{
|
|
len = nResultMax - 1;
|
|
}
|
|
memcpy(pResult, result, len);
|
|
pResult[len] = '\0';
|
|
*pnResultLen = len;
|
|
|
|
lua_pop(L, 1);
|
|
return true;
|
|
}
|
|
|
|
// =========================================================================
|
|
// CLuaMod — main module class.
|
|
// =========================================================================
|
|
|
|
CLuaMod::CLuaMod(void) : m_cRef(1),
|
|
m_pILog(nullptr),
|
|
m_pIServerEventsControl(nullptr),
|
|
m_pINotify(nullptr),
|
|
m_pIObjectInfo(nullptr),
|
|
m_pIAttributeAccess(nullptr),
|
|
m_pIEvaluator(nullptr),
|
|
m_pIPermissions(nullptr),
|
|
m_pIJITCompile(nullptr),
|
|
m_L(nullptr),
|
|
m_nInsnLimit(LUA_DEFAULT_INSN_LIMIT),
|
|
m_bCpuLimited(false),
|
|
m_nInsnPoll(LUA_ALARM_POLL_INSNS),
|
|
m_nInsnUsed(0),
|
|
m_nMemLimit(LUA_DEFAULT_MEM_LIMIT),
|
|
m_nMemUsed(0),
|
|
m_nMemPeak(0),
|
|
m_bMemExceeded(false),
|
|
m_nCacheMaxSize(LUA_DEFAULT_CACHE_SIZE)
|
|
{
|
|
memset(&m_stats, 0, sizeof(m_stats));
|
|
g_pLuaMod = this;
|
|
}
|
|
|
|
MUX_RESULT CLuaMod::FinalConstruct(void)
|
|
{
|
|
MUX_RESULT mr;
|
|
|
|
// Acquire logging interface.
|
|
//
|
|
mr = mux_CreateInstance(CID_Log, nullptr, UseSameProcess,
|
|
IID_ILog, reinterpret_cast<void **>(&m_pILog));
|
|
if (MUX_FAILED(mr))
|
|
{
|
|
return mr;
|
|
}
|
|
|
|
// Register for server events.
|
|
//
|
|
mux_IServerEventsSink *pSink = nullptr;
|
|
mr = QueryInterface(IID_IServerEventsSink,
|
|
reinterpret_cast<void **>(&pSink));
|
|
if (MUX_SUCCEEDED(mr))
|
|
{
|
|
mr = mux_CreateInstance(CID_ServerEventsSource, nullptr,
|
|
UseSameProcess, IID_IServerEventsControl,
|
|
reinterpret_cast<void **>(&m_pIServerEventsControl));
|
|
if (MUX_SUCCEEDED(mr))
|
|
{
|
|
m_pIServerEventsControl->Advise(pSink);
|
|
}
|
|
pSink->Release();
|
|
}
|
|
|
|
// Acquire core interfaces.
|
|
//
|
|
mux_CreateInstance(CID_Notify, nullptr, UseSameProcess,
|
|
IID_INotify, reinterpret_cast<void **>(&m_pINotify));
|
|
|
|
mux_CreateInstance(CID_ObjectInfo, nullptr, UseSameProcess,
|
|
IID_IObjectInfo, reinterpret_cast<void **>(&m_pIObjectInfo));
|
|
|
|
mux_CreateInstance(CID_AttributeAccess, nullptr, UseSameProcess,
|
|
IID_IAttributeAccess,
|
|
reinterpret_cast<void **>(&m_pIAttributeAccess));
|
|
|
|
mux_CreateInstance(CID_Evaluator, nullptr, UseSameProcess,
|
|
IID_IEvaluator, reinterpret_cast<void **>(&m_pIEvaluator));
|
|
|
|
mux_CreateInstance(CID_Permissions, nullptr, UseSameProcess,
|
|
IID_IPermissions, reinterpret_cast<void **>(&m_pIPermissions));
|
|
|
|
// Acquire JIT compile interface (optional — graceful degradation).
|
|
mux_CreateInstance(CID_JITCompile, nullptr, UseSameProcess,
|
|
IID_IJITCompile, reinterpret_cast<void **>(&m_pIJITCompile));
|
|
|
|
// Create the Lua state.
|
|
//
|
|
if (!CreateLuaState())
|
|
{
|
|
if (nullptr != m_pILog)
|
|
{
|
|
bool fStarted;
|
|
m_pILog->start_log(&fStarted, LOG_ALWAYS, T("INI"), T("ERR"));
|
|
if (fStarted)
|
|
{
|
|
m_pILog->log_text(T("Lua module: failed to create Lua state."));
|
|
m_pILog->end_log();
|
|
}
|
|
}
|
|
return MUX_E_FAIL;
|
|
}
|
|
|
|
// Log that we are alive.
|
|
//
|
|
if (nullptr != m_pILog)
|
|
{
|
|
bool fStarted;
|
|
m_pILog->start_log(&fStarted, LOG_ALWAYS, T("INI"), T("INFO"));
|
|
if (fStarted)
|
|
{
|
|
m_pILog->log_text(T("Lua module loaded (Lua 5.4)."));
|
|
m_pILog->end_log();
|
|
}
|
|
}
|
|
|
|
return MUX_S_OK;
|
|
}
|
|
|
|
CLuaMod::~CLuaMod()
|
|
{
|
|
CacheClear();
|
|
DestroyLuaState();
|
|
|
|
if (nullptr != m_pILog)
|
|
{
|
|
bool fStarted;
|
|
m_pILog->start_log(&fStarted, LOG_ALWAYS, T("INI"), T("INFO"));
|
|
if (fStarted)
|
|
{
|
|
m_pILog->log_text(T("Lua module unloading."));
|
|
m_pILog->end_log();
|
|
}
|
|
m_pILog->Release();
|
|
m_pILog = nullptr;
|
|
}
|
|
|
|
if (nullptr != m_pIServerEventsControl)
|
|
{
|
|
m_pIServerEventsControl->Release();
|
|
m_pIServerEventsControl = nullptr;
|
|
}
|
|
|
|
if (nullptr != m_pINotify)
|
|
{
|
|
m_pINotify->Release();
|
|
m_pINotify = nullptr;
|
|
}
|
|
|
|
if (nullptr != m_pIObjectInfo)
|
|
{
|
|
m_pIObjectInfo->Release();
|
|
m_pIObjectInfo = nullptr;
|
|
}
|
|
|
|
if (nullptr != m_pIAttributeAccess)
|
|
{
|
|
m_pIAttributeAccess->Release();
|
|
m_pIAttributeAccess = nullptr;
|
|
}
|
|
|
|
if (nullptr != m_pIEvaluator)
|
|
{
|
|
m_pIEvaluator->Release();
|
|
m_pIEvaluator = nullptr;
|
|
}
|
|
|
|
if (nullptr != m_pIPermissions)
|
|
{
|
|
m_pIPermissions->Release();
|
|
m_pIPermissions = nullptr;
|
|
}
|
|
|
|
if (nullptr != m_pIJITCompile)
|
|
{
|
|
m_pIJITCompile->Release();
|
|
m_pIJITCompile = nullptr;
|
|
}
|
|
|
|
if (g_pLuaMod == this)
|
|
{
|
|
g_pLuaMod = nullptr;
|
|
}
|
|
}
|
|
|
|
MUX_RESULT CLuaMod::QueryInterface(MUX_IID iid, void **ppv)
|
|
{
|
|
if (mux_IID_IUnknown == iid)
|
|
{
|
|
*ppv = static_cast<mux_ILuaControl *>(this);
|
|
}
|
|
else if (IID_ILuaControl == iid)
|
|
{
|
|
*ppv = static_cast<mux_ILuaControl *>(this);
|
|
}
|
|
else if (IID_IServerEventsSink == iid)
|
|
{
|
|
*ppv = static_cast<mux_IServerEventsSink *>(this);
|
|
}
|
|
else
|
|
{
|
|
*ppv = nullptr;
|
|
return MUX_E_NOINTERFACE;
|
|
}
|
|
reinterpret_cast<mux_IUnknown *>(*ppv)->AddRef();
|
|
return MUX_S_OK;
|
|
}
|
|
|
|
uint32_t CLuaMod::AddRef(void)
|
|
{
|
|
return m_cRef.fetch_add(1, std::memory_order_relaxed) + 1;
|
|
}
|
|
|
|
uint32_t CLuaMod::Release(void)
|
|
{
|
|
uint32_t prev = m_cRef.fetch_sub(1, std::memory_order_acq_rel);
|
|
if (1 == prev)
|
|
{
|
|
delete this;
|
|
return 0;
|
|
}
|
|
return prev - 1;
|
|
}
|
|
|
|
// =========================================================================
|
|
// Bytecode cache — LRU keyed by source text.
|
|
// =========================================================================
|
|
|
|
// LoadCached: try the cache first, compile on miss.
|
|
// On success, the compiled chunk is on top of the Lua stack.
|
|
// Returns true on success, false on compile error (error string on stack).
|
|
//
|
|
bool CLuaMod::LoadCached(const char *source, size_t nSource,
|
|
const char *chunkname)
|
|
{
|
|
std::string key(source, nSource);
|
|
|
|
auto it = m_cache.find(key);
|
|
if (it != m_cache.end())
|
|
{
|
|
// Cache hit — push the cached chunk.
|
|
//
|
|
m_stats.cache_hits++;
|
|
lua_rawgeti(m_L, LUA_REGISTRYINDEX, it->second.lua_ref);
|
|
|
|
// Move to front of LRU.
|
|
//
|
|
m_cache_lru.erase(it->second.lru_it);
|
|
m_cache_lru.push_front(key);
|
|
it->second.lru_it = m_cache_lru.begin();
|
|
return true;
|
|
}
|
|
|
|
// Cache miss — compile.
|
|
//
|
|
m_stats.cache_misses++;
|
|
int status = luaL_loadbufferx(m_L, source, nSource, chunkname, "t");
|
|
if (status != LUA_OK)
|
|
{
|
|
return false; // Error string is on top of stack.
|
|
}
|
|
|
|
// Store in cache: push a copy, get a registry reference.
|
|
//
|
|
lua_pushvalue(m_L, -1); // duplicate the chunk
|
|
int ref = luaL_ref(m_L, LUA_REGISTRYINDEX);
|
|
|
|
// Evict if full.
|
|
//
|
|
if (static_cast<int>(m_cache.size()) >= m_nCacheMaxSize)
|
|
{
|
|
CacheEvict();
|
|
}
|
|
|
|
// Insert.
|
|
//
|
|
m_cache_lru.push_front(key);
|
|
cache_entry entry;
|
|
entry.lua_ref = ref;
|
|
entry.lru_it = m_cache_lru.begin();
|
|
entry.jit_key = 0;
|
|
entry.jit_eligible = false;
|
|
m_cache[key] = entry;
|
|
|
|
return true;
|
|
}
|
|
|
|
void CLuaMod::CacheEvict(void)
|
|
{
|
|
if (m_cache_lru.empty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Remove the least recently used entry (back of list).
|
|
//
|
|
const std::string &oldest = m_cache_lru.back();
|
|
auto it = m_cache.find(oldest);
|
|
if (it != m_cache.end())
|
|
{
|
|
luaL_unref(m_L, LUA_REGISTRYINDEX, it->second.lua_ref);
|
|
if (it->second.jit_key != 0 && nullptr != m_pIJITCompile)
|
|
{
|
|
m_pIJITCompile->Invalidate(it->second.jit_key);
|
|
}
|
|
m_cache.erase(it);
|
|
}
|
|
m_cache_lru.pop_back();
|
|
}
|
|
|
|
void CLuaMod::CacheClear(void)
|
|
{
|
|
for (auto &pair : m_cache)
|
|
{
|
|
if (nullptr != m_L)
|
|
{
|
|
luaL_unref(m_L, LUA_REGISTRYINDEX, pair.second.lua_ref);
|
|
}
|
|
if (pair.second.jit_key != 0 && nullptr != m_pIJITCompile)
|
|
{
|
|
m_pIJITCompile->Invalidate(pair.second.jit_key);
|
|
}
|
|
}
|
|
m_cache.clear();
|
|
m_cache_lru.clear();
|
|
}
|
|
|
|
// =========================================================================
|
|
// lua_dump writer callback — accumulates bytecode into a vector.
|
|
// =========================================================================
|
|
|
|
struct dump_buffer {
|
|
std::vector<uint8_t> data;
|
|
};
|
|
|
|
static int dump_writer(lua_State *L, const void *p, size_t sz, void *ud) {
|
|
(void)L;
|
|
dump_buffer *buf = static_cast<dump_buffer *>(ud);
|
|
const uint8_t *bytes = static_cast<const uint8_t *>(p);
|
|
buf->data.insert(buf->data.end(), bytes, bytes + sz);
|
|
return 0;
|
|
}
|
|
|
|
// =========================================================================
|
|
// TryJIT: attempt to JIT-compile a cached chunk.
|
|
// The chunk must be on top of the Lua stack.
|
|
// Returns true if JIT succeeded and result is in pResult.
|
|
// Returns false on JIT failure (caller should fall through to lua_pcall).
|
|
// Does NOT pop the chunk from the stack.
|
|
// =========================================================================
|
|
|
|
bool CLuaMod::TryJIT(cache_entry &entry, dbref executor, dbref caller,
|
|
dbref enactor, const UTF8 *pArgs[], int nArgs,
|
|
UTF8 *pResult, size_t nResultMax, size_t *pnResultLen)
|
|
{
|
|
if (nullptr == m_pIJITCompile) return false;
|
|
|
|
// Safety gate (#1309): Lua JIT is off by default until the never-run
|
|
// lowering/codegen path is green. When off, fall through to the Lua
|
|
// interpreter — same behavior as before the loader fix made the JIT
|
|
// reachable.
|
|
//
|
|
if (!mudconf.lua_jit) return false;
|
|
|
|
// Already tried and failed?
|
|
if (entry.jit_eligible) {
|
|
// Already have a compiled key? Run it.
|
|
if (entry.jit_key != 0) {
|
|
// Save Lua stack — ECALL handlers may push tables/functions.
|
|
int saved_top = lua_gettop(m_L);
|
|
// Stage the same execution context the interpreter leg gets:
|
|
// compiled chunks reach the bridge C functions and the per-run
|
|
// mux fields through ordinary Lua calls now, and without this
|
|
// they saw a cleared registry and the PREVIOUS run's mux table.
|
|
lua_exec_ctx jit_ctx;
|
|
lua_exec_ctx *prev_ctx =
|
|
lua_setup_exec_context(m_L, jit_ctx, executor, caller,
|
|
enactor, pArgs, nArgs);
|
|
MUX_RESULT mr = m_pIJITCompile->RunCompiled(entry.jit_key,
|
|
executor, caller, enactor, pArgs, nArgs,
|
|
pResult, nResultMax, pnResultLen, m_L);
|
|
lua_restore_exec_context(m_L, prev_ctx);
|
|
lua_settop(m_L, saved_top); // restore stack
|
|
if (MUX_E_NOTFOUND != mr) {
|
|
return MUX_SUCCEEDED(mr);
|
|
}
|
|
|
|
// The compiled program is gone. jitstats(flush) clears the
|
|
// JIT-side program cache (jit_lua_clear_cache) but cannot reach
|
|
// this Lua-side latch, so jit_key still names a key that no
|
|
// longer resolves. Left alone, every later call takes this
|
|
// branch, misses again, and falls back to the Lua interpreter
|
|
// for the life of the process: correct answers with a dead JIT,
|
|
// which is exactly the failure #1309 is about and which no
|
|
// result-equality test can see. It also inverts the purpose of
|
|
// flush, whose whole job is to force a recompile against new
|
|
// codegen (#1316). Clear the latch and compile again below.
|
|
//
|
|
entry.jit_key = 0;
|
|
entry.jit_eligible = false;
|
|
} else {
|
|
return false; // Previously failed to compile.
|
|
}
|
|
}
|
|
|
|
// First attempt: dump the chunk to bytecode and try JIT compilation.
|
|
entry.jit_eligible = true;
|
|
|
|
// lua_dump expects the function on top of stack. We have it there
|
|
// from LoadCached. Push a copy so we don't consume it.
|
|
lua_pushvalue(m_L, -1);
|
|
|
|
dump_buffer buf;
|
|
int dump_status = lua_dump(m_L, dump_writer, &buf, 0);
|
|
lua_pop(m_L, 1); // pop the copy
|
|
|
|
if (dump_status != 0 || buf.data.empty()) {
|
|
return false;
|
|
}
|
|
|
|
// Try to compile.
|
|
uint64_t key = 0;
|
|
MUX_RESULT mr = m_pIJITCompile->CompileLuaBytecode(
|
|
buf.data.data(), buf.data.size(), &key);
|
|
if (MUX_FAILED(mr) || key == 0) {
|
|
return false; // JIT doesn't support this bytecode; fall through.
|
|
}
|
|
|
|
entry.jit_key = key;
|
|
|
|
// Run the compiled program.
|
|
// Save/restore Lua stack — ECALL handlers for table ops, getglobal,
|
|
// and generic calls push values onto the Lua stack that must be
|
|
// cleaned up after JIT execution completes. Execution context staged
|
|
// exactly as on the cached-key path above.
|
|
int saved_top = lua_gettop(m_L);
|
|
lua_exec_ctx jit_ctx;
|
|
lua_exec_ctx *prev_ctx =
|
|
lua_setup_exec_context(m_L, jit_ctx, executor, caller, enactor,
|
|
pArgs, nArgs);
|
|
mr = m_pIJITCompile->RunCompiled(key, executor, caller, enactor,
|
|
pArgs, nArgs, pResult, nResultMax, pnResultLen, m_L);
|
|
lua_restore_exec_context(m_L, prev_ctx);
|
|
lua_settop(m_L, saved_top); // restore stack
|
|
return MUX_SUCCEEDED(mr);
|
|
}
|
|
|
|
// =========================================================================
|
|
// mux_ILuaControl implementation.
|
|
// =========================================================================
|
|
|
|
MUX_RESULT CLuaMod::CallAttr(dbref executor, dbref caller, dbref enactor,
|
|
dbref obj, const UTF8 *pAttrName,
|
|
const UTF8 *pArgs[], int nArgs,
|
|
UTF8 *pResult, size_t nResultMax, size_t *pnResultLen)
|
|
{
|
|
if (nullptr == m_L)
|
|
{
|
|
return MUX_E_FAIL;
|
|
}
|
|
|
|
// Read the attribute via COM interface (permission-checked).
|
|
//
|
|
if (nullptr == m_pIAttributeAccess)
|
|
{
|
|
return MUX_E_FAIL;
|
|
}
|
|
|
|
UTF8 source[8000];
|
|
size_t nSource = 0;
|
|
MUX_RESULT mr = m_pIAttributeAccess->GetAttribute(executor, obj,
|
|
pAttrName, source, sizeof(source), &nSource);
|
|
if (MUX_FAILED(mr))
|
|
{
|
|
size_t n = mux_snprintf(pResult, nResultMax,
|
|
T("#-1 LUA ERROR: cannot read attribute"));
|
|
*pnResultLen = n;
|
|
return MUX_S_OK;
|
|
}
|
|
|
|
if (nSource == 0)
|
|
{
|
|
pResult[0] = '\0';
|
|
*pnResultLen = 0;
|
|
return MUX_S_OK;
|
|
}
|
|
|
|
m_stats.calls++;
|
|
|
|
// Compile (or load from cache).
|
|
//
|
|
char chunkname[128];
|
|
mux_snprintf(reinterpret_cast<UTF8 *>(chunkname), sizeof(chunkname),
|
|
T("@#%d/%s"), static_cast<int>(obj), pAttrName);
|
|
|
|
if (!LoadCached(reinterpret_cast<const char *>(source), nSource,
|
|
chunkname))
|
|
{
|
|
const char *errmsg = lua_tostring(m_L, -1);
|
|
if (nullptr == errmsg) errmsg = "compile error";
|
|
m_stats.errors++;
|
|
size_t n = mux_snprintf(pResult, nResultMax,
|
|
T("#-1 LUA ERROR: %s"), errmsg);
|
|
*pnResultLen = n;
|
|
lua_pop(m_L, 1);
|
|
return MUX_S_OK;
|
|
}
|
|
|
|
// Try JIT execution. The compiled chunk is on top of the Lua stack.
|
|
// If JIT succeeds, pop the chunk and return.
|
|
//
|
|
std::string cache_key(reinterpret_cast<const char *>(source), nSource);
|
|
auto cache_it = m_cache.find(cache_key);
|
|
if (cache_it != m_cache.end())
|
|
{
|
|
if (TryJIT(cache_it->second, executor, caller, enactor,
|
|
pArgs, nArgs, pResult, nResultMax, pnResultLen))
|
|
{
|
|
lua_pop(m_L, 1); // pop the chunk
|
|
return MUX_S_OK;
|
|
}
|
|
}
|
|
|
|
// Fall through to Lua VM execution.
|
|
//
|
|
ExecuteChunk(m_L, executor, caller, enactor, pArgs, nArgs,
|
|
pResult, nResultMax, pnResultLen);
|
|
return MUX_S_OK;
|
|
}
|
|
|
|
MUX_RESULT CLuaMod::Eval(dbref executor, dbref caller, dbref enactor,
|
|
const UTF8 *pSource, size_t nSource,
|
|
UTF8 *pResult, size_t nResultMax, size_t *pnResultLen)
|
|
{
|
|
if (nullptr == m_L)
|
|
{
|
|
return MUX_E_FAIL;
|
|
}
|
|
|
|
// Wizard-only check.
|
|
//
|
|
if (nullptr != m_pIPermissions)
|
|
{
|
|
bool bWizard = false;
|
|
m_pIPermissions->IsWizard(executor, &bWizard);
|
|
if (!bWizard)
|
|
{
|
|
size_t n = mux_snprintf(pResult, nResultMax,
|
|
T("#-1 LUA ERROR: wizard-only"));
|
|
*pnResultLen = n;
|
|
return MUX_S_OK;
|
|
}
|
|
}
|
|
|
|
m_stats.calls++;
|
|
|
|
if (!LoadCached(reinterpret_cast<const char *>(pSource), nSource,
|
|
"@inline"))
|
|
{
|
|
const char *errmsg = lua_tostring(m_L, -1);
|
|
if (nullptr == errmsg) errmsg = "compile error";
|
|
m_stats.errors++;
|
|
size_t n = mux_snprintf(pResult, nResultMax,
|
|
T("#-1 LUA ERROR: %s"), errmsg);
|
|
*pnResultLen = n;
|
|
lua_pop(m_L, 1);
|
|
return MUX_S_OK;
|
|
}
|
|
|
|
// Try JIT execution.
|
|
//
|
|
std::string cache_key(reinterpret_cast<const char *>(pSource), nSource);
|
|
auto cache_it = m_cache.find(cache_key);
|
|
if (cache_it != m_cache.end())
|
|
{
|
|
if (TryJIT(cache_it->second, executor, caller, enactor,
|
|
nullptr, 0, pResult, nResultMax, pnResultLen))
|
|
{
|
|
lua_pop(m_L, 1); // pop the chunk
|
|
return MUX_S_OK;
|
|
}
|
|
}
|
|
|
|
// Fall through to Lua VM execution.
|
|
//
|
|
ExecuteChunk(m_L, executor, caller, enactor, nullptr, 0,
|
|
pResult, nResultMax, pnResultLen);
|
|
return MUX_S_OK;
|
|
}
|
|
|
|
MUX_RESULT CLuaMod::GetStats(size_t *pnCalls, size_t *pnErrors,
|
|
size_t *pnInsnLimitHits, size_t *pnMemLimitHits,
|
|
size_t *pnBytesUsed,
|
|
size_t *pnCacheHits, size_t *pnCacheMisses,
|
|
size_t *pnCacheEntries)
|
|
{
|
|
*pnCalls = m_stats.calls;
|
|
*pnErrors = m_stats.errors;
|
|
*pnInsnLimitHits = m_stats.insn_limit_hits;
|
|
*pnMemLimitHits = m_stats.mem_limit_hits;
|
|
*pnBytesUsed = m_nMemUsed;
|
|
*pnCacheHits = m_stats.cache_hits;
|
|
*pnCacheMisses = m_stats.cache_misses;
|
|
*pnCacheEntries = m_cache.size();
|
|
return MUX_S_OK;
|
|
}
|
|
|
|
MUX_RESULT CLuaMod::SetLimits(int nInsnLimit, int nMemLimit)
|
|
{
|
|
if (nInsnLimit > 0)
|
|
{
|
|
m_nInsnLimit = nInsnLimit;
|
|
}
|
|
if (nMemLimit > 0)
|
|
{
|
|
m_nMemLimit = nMemLimit;
|
|
}
|
|
return MUX_S_OK;
|
|
}
|
|
|
|
// =========================================================================
|
|
// mux_IServerEventsSink stubs.
|
|
// =========================================================================
|
|
|
|
void CLuaMod::startup(void) { }
|
|
void CLuaMod::presync_database(void) { }
|
|
void CLuaMod::presync_database_sigsegv(void) { }
|
|
void CLuaMod::dump_database(int dump_type) { (void)dump_type; }
|
|
void CLuaMod::dump_complete_signal(void) { }
|
|
void CLuaMod::shutdown(void) { CacheClear(); DestroyLuaState(); }
|
|
void CLuaMod::dbck(void) { }
|
|
void CLuaMod::connect(dbref player, int isnew, int num) { (void)player; (void)isnew; (void)num; }
|
|
void CLuaMod::disconnect(dbref player, int num) { (void)player; (void)num; }
|
|
void CLuaMod::data_create(dbref object) { (void)object; }
|
|
void CLuaMod::data_clone(dbref clone, dbref source) { (void)clone; (void)source; }
|
|
void CLuaMod::data_free(dbref object) { (void)object; }
|
|
|
|
// =========================================================================
|
|
// =========================================================================
|
|
// Factory function — called from engine_com.cpp's CLuaModFactory.
|
|
// =========================================================================
|
|
|
|
MUX_RESULT lua_mod_create_instance(MUX_IID iid, void **ppv) {
|
|
CLuaMod *pLuaMod = nullptr;
|
|
try { pLuaMod = new CLuaMod; } catch (...) { ; }
|
|
if (nullptr == pLuaMod) return MUX_E_OUTOFMEMORY;
|
|
|
|
MUX_RESULT mr = pLuaMod->FinalConstruct();
|
|
if (MUX_SUCCEEDED(mr)) {
|
|
mr = pLuaMod->QueryInterface(iid, ppv);
|
|
}
|
|
pLuaMod->Release();
|
|
return mr;
|
|
}
|