mirror of
https://github.com/brazilofmux/tinymux
synced 2026-08-13 00:23:11 -04:00
fix(lua): honour the wall-clock alarm, the bound the rest of the server uses (#1591)
The Lua module bounded a chunk only by its own instruction and memory limits. Neither is what the rest of the server bounds on: softcode bounds on wall time. alarm_clock is armed per command (cque.cpp:303, alarm_clock.set(mudconf.max_cmdsecs)), the AST evaluator polls it and answers "#-1 CPU LIMITED", and the JIT is handed the same flag as dbt->alarm_flag. `grep -c alarm mux/modules/engine/lua_mod.cpp` was 0, so lua() ignored max_cmdsecs entirely. Joins the existing mechanism rather than adding another. Shape borrowed from the JIT's guest-loop budget (#1571): count cheaply, do the real check periodically. InsnCountHook now fires every LUA_ALARM_POLL_INSNS (2000) instructions instead of once at the limit, polls alarm_clock.alarmed first, and only then accounts instructions toward m_nInsnLimit -- so the instruction limit keeps its previous meaning while wall time becomes a bound Lua shares with everything else. On the alarm it answers "#-1 CPU LIMITED" exactly, not "#-1 LUA ERROR: ...". One budget, one message: the same text the AST evaluator and the JIT already produce, so a chunk that runs long reads the same whichever route evaluated it. jitstats-style accounting gains cpu_limit_hits alongside insn_limit_hits and mem_limit_hits. Half of #1591, deliberately. The hook cannot fire inside a C function, so this does not bound Lua's pattern matcher: 26 bytes of subject and a 51-byte pattern still run indefinitely, because min_expand iterates rather than recursing and nothing counts attempts. That wants the matcher itself to count, the way quick_wild already does with mudstate.wild_invk_ctr / mudconf.wild_invk_lim -- the same mechanism, which would also make a command's total match effort bounded once rather than per-engine. Left in the issue because it means patching mux/lua54, which carries no local modifications today. Not exercisable under muxscript, and worth recording why: alarm_clock.set appears only in the queued-command path, and mux/script/mux_main.cpp:1373 clears the alarm and never arms it. So the smoke suite cannot observe wall-clock bounding for Lua *or* softcode -- max_cmdsecs has no automated coverage on any route, which is plausibly why this gap survived. Filed separately. Verified here that the instruction limit still fires ("instruction limit exceeded" on a 3e8-iteration loop) and ordinary chunks are unaffected. make test green, both smoke routes 1560/1560, tests/luajit PASSED. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
bf215bfc09
commit
ce603e68e0
2 changed files with 95 additions and 1 deletions
|
|
@ -627,9 +627,50 @@ void *CLuaMod::LuaAlloc(void *ud, void *ptr, size_t osize, size_t nsize)
|
|||
// 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. Lua's pattern matcher can run unboundedly on a small input, which is
|
||||
// the remaining half of #1591 and needs the matcher itself to count, the way
|
||||
// quick_wild already does with mudstate.wild_invk_ctr.
|
||||
//
|
||||
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");
|
||||
}
|
||||
|
||||
|
|
@ -772,7 +813,19 @@ bool CLuaMod::ExecuteChunk(lua_State *L, dbref executor, dbref caller,
|
|||
// Set instruction count hook.
|
||||
//
|
||||
m_bMemExceeded = false;
|
||||
lua_sethook(L, InsnCountHook, LUA_MASKCOUNT, m_nInsnLimit);
|
||||
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);
|
||||
|
||||
// Call the chunk (it's below the mux table stuff we just popped).
|
||||
//
|
||||
|
|
@ -793,6 +846,25 @@ bool CLuaMod::ExecuteChunk(lua_State *L, dbref executor, dbref caller,
|
|||
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++;
|
||||
|
|
@ -868,6 +940,9 @@ CLuaMod::CLuaMod(void) : m_cRef(1),
|
|||
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),
|
||||
|
|
|
|||
|
|
@ -31,6 +31,12 @@ extern "C" {
|
|||
// 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
|
||||
|
||||
|
|
@ -42,6 +48,7 @@ struct lua_mod_stats
|
|||
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;
|
||||
|
|
@ -74,6 +81,18 @@ private:
|
|||
size_t m_nMemPeak;
|
||||
bool m_bMemExceeded;
|
||||
|
||||
// Wall-clock abort (#1591). Set by InsnCountHook 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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue