DO NOT MERGE ALONE -- see below.
Lua 5.4 encodes `int` with the same base-128 varint as `size_t`:
lundump.c defines loadInt() as cast_int(loadUnsigned(S, INT_MAX)),
sharing loadUnsigned() with loadSize(). read_int() instead consumed
four raw bytes, so `linedefined` ate 4 bytes where the stream held 1,
desynchronising every field after it.
Effect: load_proto() always failed, so lua_bc_load() always failed, so
CompileLuaBytecode() always failed, so the Lua JIT compiled nothing.
Ever. read_int() has exactly one commit in its history -- 21243e831
(2026-03-18), the commit that introduced the file -- so this is not a
regression: the pipeline has never executed a single chunk.
Traced by probe, header outward:
dump: status=0 size=106 bytes=1B 4C 75 61 54 00 19 93 0D 0A ...
(valid Lua 5.4: signature, version 0x54, format 0, LUAC_DATA)
bc_load: failed at proto
load_proto: failed at ncode
fields: src='@#1/LJ' linedef=16810112 lastline=5342978
nparams=0 vararg=0 maxstack=11 ncode=0 ok=0
`src` reads correctly -- read_string()/read_size() handle the varint
properly -- and the corruption starts precisely at the first read_int().
With this applied the loader parses, hir_lower_lua_proto is reached for
the first time, and `lua(me/LJ,5)` -> -5, `lua(me/LA,3,4)` -> 7.
WHY THIS MUST NOT LAND ON ITS OWN: it activates a large body of code
that has never executed. Full smoke then reports 35 failures, all in
lua_fn.mux, most `#-1 NO ATTRIBUTE SPECIFIED`. The lowering and codegen
are not correct yet; fixing the loader only reveals that.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Lua 5.4 bytecode deserializer (lua_bytecode.cpp) had two latent
memory-safety gaps in its reader:
1. bc_reader::has(n) computed pos + n <= len, which wraps when n is a
near-2^64 value. read_string() derives its size from read_size() (a varint
that can decode any 64-bit value), so a crafted/corrupt size makes pos + sz
overflow and falsely pass the bound, then std::string(data+pos, sz) reads /
allocates ~2^64 bytes. Verified: a 42-byte crafted dump (size = 2^64-41)
throws std::length_error -> terminate() with the old check; the fixed check
rejects it cleanly. Fix: test n <= len - pos (pos <= len is an invariant, so
no underflow), which can't overflow.
2. load_proto() -> load_protos() -> load_proto() recursion had no depth bound;
a pathologically nested dump overflows the C stack before the caller's
eligibility check (which rejects nested protos) runs. Fix: thread a depth
parameter and cap at MAX_PROTO_DEPTH (200; real compiler output nests far
below this).
Both are latent (the bytecode comes from the trusted lua_dump path, not
player-injectable through the text-only sandbox), so this is defense-in-depth
hardening of the deserialization boundary. Smoke 1252/1252.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
read_size() decoded a Lua base-128 size varint (lundump.c LoadUnsigned format:
continuation bytes have the high bit clear, the terminator sets it) with an
unbounded loop. A malformed bytecode stream with a long run of continuation
bytes would keep shifting the accumulator left by 7 per byte, overflowing size_t
and wrapping to a small bogus length that then slips past the caller's bounds
check.
Cap the byte count at sizeof(size_t)*8/7 + 1 (10 on a 64-bit build) — the most
groups a size_t can legitimately need — and fail the read (ok = false) past
that, matching how the upstream loader guards the same loop against overflow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
read_size() returns size_t but was cast to int, causing negative
values when the bytecode reader gets out of sync. The negative int
was then promoted back to a huge size_t by vector::resize(), throwing
std::length_error which escaped to std::terminate and killed the
server via SIGABRT on every smoke test run during lua() evaluation.
Use size_t throughout and add a 1M sanity cap on all deserialized
array sizes so malformed bytecode fails gracefully instead of crashing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Lua scripts compiled by lua_mod.so can now be JIT-compiled through
the existing HIR/RV64/x86-64 pipeline in engine.so. The bytecode
deserializer reads lua_dump() output without requiring Lua headers.
New COM interface mux_IJITCompile on engine.so with CompileLuaBytecode,
RunCompiled, IsCompiled, and Invalidate methods. lua_mod.so acquires
this interface and transparently attempts JIT before falling back to
the Lua VM.
Opcode coverage: data movement (MOVE/LOADI/LOADK/LOADNIL/etc.),
integer arithmetic (ADD/SUB/MUL/IDIV/MOD/UNM + immediate/constant
variants), comparisons (EQ/LT/LE/EQI/LTI/LEI/GTI/GEI), control flow
(JMP/TEST/TESTSET/FORPREP/FORLOOP), returns, and mux.* bridge calls
pattern-matched from GETTABUP+GETFIELD+CALL to engine API ECALLs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>