mirror of
https://github.com/brazilofmux/tinymux
synced 2026-08-13 00:23:11 -04:00
Part (1) of #1591 landed in ce603e68e: InsnCountHook now checks
alarm_clock. It cannot cover this case. lua_sethook(LUA_MASKCOUNT) counts
VM instructions and does not fire inside a C function, and a pathological
pattern spends all of its time inside one call to str_find_aux.
lstrlib.c's own MAXCCALLS guard bounds recursion DEPTH, which is what
stops a C stack overflow. It does not bound running time: 'a-a-a-...-b'
against a string of 'a's backtracks exponentially in BREADTH, so depth
stays under 200 while the number of match() calls goes to 2^n.
Measured on macOS arm64, max_cmdsecs 1, each probe on its own fresh server
behind a trivial control that had to answer first:
before nothing in 45s, server pinned at 100% CPU
after #-1 CPU LIMITED at t=1.00s, CPU back to 2.6%
Adds a step counter to MatchState alongside matchdepth, checked in the
same l_unlikely branch in match(). Every 65536 steps it calls
lua_match_interrupt if installed; the module installs it around the same
pcall that arms the count hook, and returns non-zero once alarm_clock has
fired. That routes through the existing m_bCpuLimited path, so the caller
sees "#-1 CPU LIMITED" -- the same answer the AST evaluator and the JIT
give. One budget, one message, all four routes.
The counter resets in prepstate but deliberately NOT in reprepstate: a
scan that retries from every position in a long subject is slow in
aggregate even when each attempt is cheap, and the budget bounds the whole
call.
mux/lua54 is the only vendored file touched, and the pointer defaults to
NULL, so that tree still builds standalone as stock Lua 5.4.
Ordinary patterns are unaffected -- find, match, gsub, gmatch, anchors, a
20000-character scan and a self-terminating backtracker all return in
0.00s with the budget active. An ordinary pattern never reaches 65536
steps, so the common path costs one increment and one predictable branch.
make test green: 1561/1561 on both smoke routes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
193 lines
6.5 KiB
C++
193 lines
6.5 KiB
C++
/*! \file lua_mod.h
|
|
* \brief Lua Module — Server-side Lua 5.4 scripting as a loadable module
|
|
*
|
|
* This module embeds a Lua 5.4 interpreter with a sandboxed environment.
|
|
* Scripts are stored as LUA_* attributes on objects and executed via the
|
|
* lua() softcode function or the @lua command.
|
|
*
|
|
* Core dependencies are accessed exclusively through COM interfaces:
|
|
* mux_INotify - player notification
|
|
* mux_IObjectInfo - object property queries
|
|
* mux_IAttributeAccess - attribute read/write
|
|
* mux_IEvaluator - softcode evaluation
|
|
* mux_IPermissions - permission checks
|
|
* mux_ILog - logging
|
|
*/
|
|
|
|
#ifndef LUA_MOD_H
|
|
#define LUA_MOD_H
|
|
|
|
#include <atomic>
|
|
#include <unordered_map>
|
|
#include <list>
|
|
#include <string>
|
|
|
|
extern "C" {
|
|
#include <lua.h>
|
|
#include <lauxlib.h>
|
|
#include <lualib.h>
|
|
}
|
|
|
|
// Default resource limits.
|
|
//
|
|
#define LUA_DEFAULT_INSN_LIMIT 100000
|
|
|
|
// How often the instruction hook fires so the wall-clock alarm is polled
|
|
// (#1591). Small enough that a chunk notices the alarm promptly, large enough
|
|
// that the hook is not the cost of running Lua. The instruction limit is
|
|
// accounted across polls, so this does not change what that limit means.
|
|
#define LUA_ALARM_POLL_INSNS 2000
|
|
#define LUA_DEFAULT_MEM_LIMIT 1048576 // 1 MB
|
|
#define LUA_DEFAULT_CACHE_SIZE 256
|
|
|
|
// Statistics.
|
|
//
|
|
struct lua_mod_stats
|
|
{
|
|
size_t calls;
|
|
size_t errors;
|
|
size_t insn_limit_hits;
|
|
size_t mem_limit_hits;
|
|
size_t cpu_limit_hits; // wall-clock aborts (#1591)
|
|
size_t peak_mem_bytes;
|
|
size_t cache_hits;
|
|
size_t cache_misses;
|
|
};
|
|
|
|
class CLuaMod : public mux_ILuaControl, public mux_IServerEventsSink
|
|
{
|
|
private:
|
|
mux_ILog *m_pILog;
|
|
mux_IServerEventsControl *m_pIServerEventsControl;
|
|
mux_INotify *m_pINotify;
|
|
mux_IObjectInfo *m_pIObjectInfo;
|
|
mux_IAttributeAccess *m_pIAttributeAccess;
|
|
mux_IEvaluator *m_pIEvaluator;
|
|
mux_IPermissions *m_pIPermissions;
|
|
mux_IJITCompile *m_pIJITCompile;
|
|
|
|
// Lua state - global, shared across all executions.
|
|
//
|
|
lua_State *m_L;
|
|
|
|
// Resource limits.
|
|
//
|
|
int m_nInsnLimit;
|
|
int m_nMemLimit;
|
|
|
|
// Memory tracking for custom allocator.
|
|
//
|
|
size_t m_nMemUsed;
|
|
size_t m_nMemPeak;
|
|
bool m_bMemExceeded;
|
|
|
|
// Wall-clock abort (#1591). Set by InsnCountHook (VM instructions) or
|
|
// MatchInterrupt (inside the pattern matcher, where the count hook cannot
|
|
// reach) when alarm_clock has fired, so Run() can answer
|
|
// "#-1 CPU LIMITED" -- the same text the AST evaluator and the JIT
|
|
// produce -- rather than wrapping it as a Lua error.
|
|
bool m_bCpuLimited;
|
|
|
|
// Instruction accounting for the hook. The hook now fires every
|
|
// m_nInsnPoll instructions rather than once at the limit, so that the
|
|
// alarm is polled periodically; m_nInsnUsed accumulates toward
|
|
// m_nInsnLimit so the instruction limit keeps its old meaning.
|
|
int m_nInsnPoll;
|
|
int m_nInsnUsed;
|
|
|
|
// Execution statistics.
|
|
//
|
|
lua_mod_stats m_stats;
|
|
|
|
// Bytecode cache — LRU keyed by source text.
|
|
// Values are Lua registry references to compiled chunks.
|
|
//
|
|
struct cache_entry
|
|
{
|
|
int lua_ref; // LUA_REGISTRYINDEX ref
|
|
std::list<std::string>::iterator lru_it; // Position in LRU list
|
|
uint64_t jit_key; // JIT compiled program key (0 = not compiled)
|
|
bool jit_eligible; // true if JIT compilation was attempted
|
|
};
|
|
std::unordered_map<std::string, cache_entry> m_cache;
|
|
std::list<std::string> m_cache_lru; // Front = most recent
|
|
int m_nCacheMaxSize;
|
|
|
|
// Internal helpers.
|
|
//
|
|
bool CreateLuaState(void);
|
|
void DestroyLuaState(void);
|
|
void RegisterBridgeFunctions(void);
|
|
bool SetupSandbox(lua_State *L);
|
|
bool LoadCached(const char *source, size_t nSource, const char *chunkname);
|
|
void CacheEvict(void);
|
|
void CacheClear(void);
|
|
bool TryJIT(cache_entry &entry, dbref executor, dbref caller,
|
|
dbref enactor, const UTF8 *pArgs[], int nArgs,
|
|
UTF8 *pResult, size_t nResultMax, size_t *pnResultLen);
|
|
bool ExecuteChunk(lua_State *L, dbref executor, dbref caller,
|
|
dbref enactor, const UTF8 *pArgs[], int nArgs,
|
|
UTF8 *pResult, size_t nResultMax, size_t *pnResultLen);
|
|
|
|
static void InsnCountHook(lua_State *L, lua_Debug *ar);
|
|
|
|
// The C-function half of the same budget (#1591). InsnCountHook cannot
|
|
// fire inside string.find, so lstrlib.c calls this instead; a static
|
|
// member for the same reason InsnCountHook is one -- it needs
|
|
// m_bCpuLimited to turn the abort into "#-1 CPU LIMITED".
|
|
static int MatchInterrupt(lua_State *L);
|
|
static void *LuaAlloc(void *ud, void *ptr, size_t osize, size_t nsize);
|
|
|
|
public:
|
|
// mux_IUnknown
|
|
//
|
|
MUX_RESULT QueryInterface(MUX_IID iid, void **ppv) override;
|
|
uint32_t AddRef(void) override;
|
|
uint32_t Release(void) override;
|
|
|
|
// mux_ILuaControl
|
|
//
|
|
MUX_RESULT 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) override;
|
|
|
|
MUX_RESULT Eval(dbref executor, dbref caller, dbref enactor,
|
|
const UTF8 *pSource, size_t nSource,
|
|
UTF8 *pResult, size_t nResultMax, size_t *pnResultLen) override;
|
|
|
|
MUX_RESULT 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) override;
|
|
|
|
MUX_RESULT SetLimits(int nInsnLimit, int nMemLimit) override;
|
|
|
|
// mux_IServerEventsSink
|
|
//
|
|
void startup(void) override;
|
|
void presync_database(void) override;
|
|
void presync_database_sigsegv(void) override;
|
|
void dump_database(int dump_type) override;
|
|
void dump_complete_signal(void) override;
|
|
void shutdown(void) override;
|
|
void dbck(void) override;
|
|
void connect(dbref player, int isnew, int num) override;
|
|
void disconnect(dbref player, int num) override;
|
|
void data_create(dbref object) override;
|
|
void data_clone(dbref clone, dbref source) override;
|
|
void data_free(dbref object) override;
|
|
|
|
CLuaMod(void);
|
|
MUX_RESULT FinalConstruct(void);
|
|
virtual ~CLuaMod();
|
|
|
|
private:
|
|
std::atomic<uint32_t> m_cRef;
|
|
};
|
|
|
|
// Factory function for engine_com.cpp registration.
|
|
MUX_RESULT lua_mod_create_instance(MUX_IID iid, void **ppv);
|
|
|
|
#endif // LUA_MOD_H
|